@immediately-run/preauth-core 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,11 @@ export interface MintResult {
20
20
  * requested) — the post-boot caller lifts the frame cap on this alone, even
21
21
  * if a mount selection failed (matching its historical behavior). */
22
22
  netFetchOk: boolean;
23
+ /** Whether the plain app-scoped capability grant succeeded (vacuously true when
24
+ * none was requested), R3-233. False when caps were requested but the store has
25
+ * no `grantAppCapabilities` (fail-loud, never validate-then-drop) or the write
26
+ * threw — the caller lifts those frame caps only on `true`. */
27
+ capabilitiesOk: boolean;
23
28
  /** Successfully minted per-selection space ids (for post-boot provisioning). */
24
29
  minted: {
25
30
  selection: ConsentSelection;
@@ -39,4 +44,10 @@ export type MintErrorSink = (ctx: string, err: unknown) => void;
39
44
  * records which §11.4 declared mount it satisfies, so a later boot re-provisions
40
45
  * it without re-consent.
41
46
  */
42
- export declare function mintConsentedGrants(store: MintStore, uid: string, appKey: string, selections: readonly ConsentSelection[], netFetchHosts: readonly NetFetchHost[], mintPath?: MintPath, onError?: MintErrorSink): Promise<MintResult>;
47
+ export declare function mintConsentedGrants(store: MintStore, uid: string, appKey: string, selections: readonly ConsentSelection[], netFetchHosts: readonly NetFetchHost[], mintPath?: MintPath, onError?: MintErrorSink,
48
+ /** PLAIN app-scoped on/off capabilities to grant (R3-233) — `task:invoke`,
49
+ * `llm:chat`, `contribute:self`, `diagnostics:read`. NOT `net:fetch` (host-
50
+ * parameterized — granted via `netFetchHosts` above); the caller (`applyPreAuth`)
51
+ * filters host-parameterized caps out. Defaults to none, so existing callers that
52
+ * only mint mounts + hosts are unaffected. */
53
+ capabilities?: readonly string[]): Promise<MintResult>;
@@ -28,7 +28,13 @@ exports.mintConsentedGrants = mintConsentedGrants;
28
28
  * records which §11.4 declared mount it satisfies, so a later boot re-provisions
29
29
  * it without re-consent.
30
30
  */
31
- async function mintConsentedGrants(store, uid, appKey, selections, netFetchHosts, mintPath = 'interactive', onError) {
31
+ async function mintConsentedGrants(store, uid, appKey, selections, netFetchHosts, mintPath = 'interactive', onError,
32
+ /** PLAIN app-scoped on/off capabilities to grant (R3-233) — `task:invoke`,
33
+ * `llm:chat`, `contribute:self`, `diagnostics:read`. NOT `net:fetch` (host-
34
+ * parameterized — granted via `netFetchHosts` above); the caller (`applyPreAuth`)
35
+ * filters host-parameterized caps out. Defaults to none, so existing callers that
36
+ * only mint mounts + hosts are unaffected. */
37
+ capabilities = []) {
32
38
  let ok = true;
33
39
  let netFetchOk = true;
34
40
  if (netFetchHosts.length > 0) {
@@ -41,6 +47,27 @@ async function mintConsentedGrants(store, uid, appKey, selections, netFetchHosts
41
47
  netFetchOk = false;
42
48
  }
43
49
  }
50
+ // Plain app-scoped capability grants (R3-233). Fail LOUD if asked to mint caps but
51
+ // the adapter has no `grantAppCapabilities` — a silent skip would resurrect the
52
+ // exact validate-then-drop bug this fixes.
53
+ let capabilitiesOk = true;
54
+ if (capabilities.length > 0) {
55
+ if (!store.grantAppCapabilities) {
56
+ onError?.('capability grant unsupported by this store', new Error('grantAppCapabilities not implemented'));
57
+ ok = false;
58
+ capabilitiesOk = false;
59
+ }
60
+ else {
61
+ try {
62
+ await store.grantAppCapabilities({ uid, appKey, capabilities, mintPath });
63
+ }
64
+ catch (err) {
65
+ onError?.('capability grant failed', err);
66
+ ok = false;
67
+ capabilitiesOk = false;
68
+ }
69
+ }
70
+ }
44
71
  const minted = [];
45
72
  for (const sel of selections) {
46
73
  try {
@@ -63,5 +90,5 @@ async function mintConsentedGrants(store, uid, appKey, selections, netFetchHosts
63
90
  ok = false;
64
91
  }
65
92
  }
66
- return { ok, netFetchOk, minted };
93
+ return { ok, netFetchOk, capabilitiesOk, minted };
67
94
  }
@@ -49,6 +49,14 @@ export declare function isBaseline(cap: Capability): boolean;
49
49
  * per decision #1.) */
50
50
  export declare const APP_SCOPED_CAPABILITIES: readonly Capability[];
51
51
  export declare function isAppScoped(cap: Capability): boolean;
52
+ /** App-scoped caps whose durable authority is a PARAMETER SET minted on its own
53
+ * path — today only `net:fetch` (its granted host set, §5.11). These are granted
54
+ * by that path, never as a bare on/off capability: a bare `net:fetch` grant would
55
+ * be UNBOUNDED (every origin), so the plain-capability mint (R3-233) MUST exclude
56
+ * them. `task:invoke` is `parameterized` too but its bound is the app's manifest
57
+ * `invokes` (§5.8), not a durable grant param, so it IS a plain on/off grant. */
58
+ export declare const HOST_PARAMETERIZED_CAPABILITIES: readonly Capability[];
59
+ export declare function isHostParameterized(cap: Capability): boolean;
52
60
  /** Compare dotted numeric versions: <0 if a<b, 0 if equal, >0 if a>b. Missing
53
61
  * segments are treated as 0 ("1.2" === "1.2.0"); non-numeric segments as 0. */
54
62
  export declare function compareVersions(a: string, b: string): number;
@@ -9,11 +9,12 @@
9
9
  // with a view() projection on a channel (§8.3); actions are gated before the
10
10
  // handler (§8.4). Parameterized capabilities additionally bound an argument set.
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.APP_SCOPED_CAPABILITIES = exports.BASELINE_CAPABILITIES = exports.REGISTRY_VERSION = exports.CAPABILITIES = void 0;
12
+ exports.HOST_PARAMETERIZED_CAPABILITIES = exports.APP_SCOPED_CAPABILITIES = exports.BASELINE_CAPABILITIES = exports.REGISTRY_VERSION = exports.CAPABILITIES = void 0;
13
13
  exports.isKnownCapability = isKnownCapability;
14
14
  exports.tierOf = tierOf;
15
15
  exports.isBaseline = isBaseline;
16
16
  exports.isAppScoped = isAppScoped;
17
+ exports.isHostParameterized = isHostParameterized;
17
18
  exports.compareVersions = compareVersions;
18
19
  exports.isSupportedCapability = isSupportedCapability;
19
20
  exports.unsupportedCapabilities = unsupportedCapabilities;
@@ -234,6 +235,16 @@ exports.APP_SCOPED_CAPABILITIES = Object.keys(exports.CAPABILITIES).filter((c) =
234
235
  function isAppScoped(cap) {
235
236
  return exports.CAPABILITIES[cap].appScoped === true;
236
237
  }
238
+ /** App-scoped caps whose durable authority is a PARAMETER SET minted on its own
239
+ * path — today only `net:fetch` (its granted host set, §5.11). These are granted
240
+ * by that path, never as a bare on/off capability: a bare `net:fetch` grant would
241
+ * be UNBOUNDED (every origin), so the plain-capability mint (R3-233) MUST exclude
242
+ * them. `task:invoke` is `parameterized` too but its bound is the app's manifest
243
+ * `invokes` (§5.8), not a durable grant param, so it IS a plain on/off grant. */
244
+ exports.HOST_PARAMETERIZED_CAPABILITIES = ['net:fetch'];
245
+ function isHostParameterized(cap) {
246
+ return exports.HOST_PARAMETERIZED_CAPABILITIES.includes(cap);
247
+ }
237
248
  // ── §5.11 capability version gate (threat T26) ──────────────────────────────
238
249
  //
239
250
  // Each capability declares the lowest registry version that knows it (`since`).
@@ -13,6 +13,30 @@ export interface MintSentinels {
13
13
  * of a delegated grant's `parentGrantId`. `::` is delimiter-safe: `appKey` uses
14
14
  * `__` separators and a Firestore `spaceId` is alphanumeric. */
15
15
  export declare const grantKey: (appKey: string, spaceId: string) => string;
16
+ /** R3-98 S4 — the principal-aware grant key `(appKey, principal, spaceId)` (design
17
+ * 05a §3.1/§3.2). Additive: {@link grantKey} is retained for the legacy 2-field
18
+ * form. `::` stays delimiter-safe — `appKey` uses `__`, a `spaceId` is alphanumeric,
19
+ * and a named principal is lowercase-dotted/hyphenated (CA-3), none containing `::`. */
20
+ export declare const grantKeyWithPrincipal: (appKey: string, principal: string, spaceId: string) => string;
21
+ /** A parsed `parentGrantId` — the pieces the §8.15 revoke cascade reconstructs a
22
+ * grant doc path from. `principal` is present only for a 3-field (S4+) key. */
23
+ export interface ParsedGrantKey {
24
+ appKey: string;
25
+ spaceId: string;
26
+ /** The named principal for a 3-field {@link grantKeyWithPrincipal} key; undefined
27
+ * for a legacy 2-field {@link grantKey} (the caller defaults to its grandfather
28
+ * sentinel). */
29
+ principal?: string;
30
+ }
31
+ /** R3-98 S4 — ARITY-DETECTING parse of a grant key (design 05a §3.1 step 3 /
32
+ * MEDIUM-6). A 3-field key is `appKey::principal::spaceId`; a legacy 2-field key is
33
+ * `appKey::spaceId` (principal undefined). This lets the revoke cascade keep
34
+ * resolving BOTH legacy and keyed `parentGrantId`s after the re-key — a positional
35
+ * `split('::')` would mis-assign a legacy key's `spaceId` to `principal`. A
36
+ * malformed key (≠2/≠3 segments) degrades to best-effort `appKey::…::spaceId`
37
+ * (first + last), so the cascade fails safe (child self-revokes) rather than
38
+ * crashing. */
39
+ export declare const parseGrantKey: (key: string) => ParsedGrantKey;
16
40
  /** Durable elevated/app-scoped grants expire after 90 days WITHOUT USE; first
17
41
  * use after expiry re-prompts. Baseline needs no grant record, so this never
18
42
  * touches it. */
@@ -50,7 +74,7 @@ export declare const appKeyTouchFields: (s: MintSentinels) => Record<string, unk
50
74
  /** `user-app-spaces/{uid}/apps/{appKey}/spaces/{spaceId}` — the durable §8.7
51
75
  * grant doc (merge). `mintPath` defaults to `interactive`; `grantedAt`/`lastUsedAt`
52
76
  * drive the §8.15 90-day-unused expiry. */
53
- export declare const appSpaceGrantFields: (params: Pick<GrantSpaceParams, "name" | "subtree" | "mode" | "rules" | "declaredUri" | "mintPath" | "parentGrantId">, s: MintSentinels) => Record<string, unknown>;
77
+ export declare const appSpaceGrantFields: (params: Pick<GrantSpaceParams, "name" | "subtree" | "mode" | "rules" | "declaredUri" | "mintPath" | "parentGrantId" | "principal">, s: MintSentinels) => Record<string, unknown>;
54
78
  /** Union net:fetch host rules by origin (incoming wins) — the "consent
55
79
  * accumulates" merge both adapters apply before writing the host set. */
56
80
  export declare const mergeNetFetchHosts: (existing: readonly NetFetchHost[], incoming: readonly NetFetchHost[]) => NetFetchHost[];
@@ -59,3 +83,13 @@ export declare const mergeNetFetchHosts: (existing: readonly NetFetchHost[], inc
59
83
  * grant time is stamped ONCE, on first mint, and `netFetchLastUsedAt` refreshes
60
84
  * on every (re-)consent). */
61
85
  export declare const netFetchGrantFields: (mergedHosts: readonly NetFetchHost[], hadGrantedAt: boolean, s: MintSentinels) => Record<string, unknown>;
86
+ /** Union granted PLAIN app-scoped capability names (set semantics; sorted for a
87
+ * stable, byte-faithful document) — the "consent accumulates" merge for the
88
+ * R3-233 capability grant, mirroring {@link mergeNetFetchHosts}. */
89
+ export declare const mergeCapabilities: (existing: readonly string[], incoming: readonly string[]) => string[];
90
+ /** `user-app-spaces/{uid}/apps/{appKey}` — the durable granted PLAIN app-scoped
91
+ * capability set (merge), R3-233. Lives on the SAME appKey doc as the net:fetch
92
+ * grant so one read (`getAppGrantDoc`) yields both. `capabilitiesGrantedAt` is
93
+ * stamped ONCE (first mint); `capabilitiesLastUsedAt` refreshes on every
94
+ * (re-)consent — the §8.15 90-day-unused expiry clock, identical to net:fetch. */
95
+ export declare const appCapabilitiesGrantFields: (mergedCaps: readonly string[], hadGrantedAt: boolean, s: MintSentinels) => Record<string, unknown>;
package/dist/docLayout.js CHANGED
@@ -16,12 +16,35 @@
16
16
  // `.set()`/`.update()` is the only thing each adapter does itself. Drift is then
17
17
  // impossible without editing a helper both consume.
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.netFetchGrantFields = exports.mergeNetFetchHosts = exports.appSpaceGrantFields = exports.appKeyTouchFields = exports.appCountFields = exports.userCountFields = exports.ownerUserSpaceFields = exports.ownerMemberFields = exports.spaceDocFields = exports.appCountPath = exports.userCountPath = exports.appSpacePath = exports.appKeyPath = exports.userSpacePath = exports.memberPath = exports.spacePath = exports.defined = exports.granteeId = exports.GRANT_EXPIRY_MS = exports.grantKey = void 0;
19
+ exports.appCapabilitiesGrantFields = exports.mergeCapabilities = exports.netFetchGrantFields = exports.mergeNetFetchHosts = exports.appSpaceGrantFields = exports.appKeyTouchFields = exports.appCountFields = exports.userCountFields = exports.ownerUserSpaceFields = exports.ownerMemberFields = exports.spaceDocFields = exports.appCountPath = exports.userCountPath = exports.appSpacePath = exports.appKeyPath = exports.userSpacePath = exports.memberPath = exports.spacePath = exports.defined = exports.granteeId = exports.GRANT_EXPIRY_MS = exports.parseGrantKey = exports.grantKeyWithPrincipal = exports.grantKey = void 0;
20
20
  /** Stable per-user identifier for a grant `(appKey, spaceId)`, used as the value
21
21
  * of a delegated grant's `parentGrantId`. `::` is delimiter-safe: `appKey` uses
22
22
  * `__` separators and a Firestore `spaceId` is alphanumeric. */
23
23
  const grantKey = (appKey, spaceId) => `${appKey}::${spaceId}`;
24
24
  exports.grantKey = grantKey;
25
+ /** R3-98 S4 — the principal-aware grant key `(appKey, principal, spaceId)` (design
26
+ * 05a §3.1/§3.2). Additive: {@link grantKey} is retained for the legacy 2-field
27
+ * form. `::` stays delimiter-safe — `appKey` uses `__`, a `spaceId` is alphanumeric,
28
+ * and a named principal is lowercase-dotted/hyphenated (CA-3), none containing `::`. */
29
+ const grantKeyWithPrincipal = (appKey, principal, spaceId) => `${appKey}::${principal}::${spaceId}`;
30
+ exports.grantKeyWithPrincipal = grantKeyWithPrincipal;
31
+ /** R3-98 S4 — ARITY-DETECTING parse of a grant key (design 05a §3.1 step 3 /
32
+ * MEDIUM-6). A 3-field key is `appKey::principal::spaceId`; a legacy 2-field key is
33
+ * `appKey::spaceId` (principal undefined). This lets the revoke cascade keep
34
+ * resolving BOTH legacy and keyed `parentGrantId`s after the re-key — a positional
35
+ * `split('::')` would mis-assign a legacy key's `spaceId` to `principal`. A
36
+ * malformed key (≠2/≠3 segments) degrades to best-effort `appKey::…::spaceId`
37
+ * (first + last), so the cascade fails safe (child self-revokes) rather than
38
+ * crashing. */
39
+ const parseGrantKey = (key) => {
40
+ const parts = key.split('::');
41
+ if (parts.length === 3) {
42
+ return { appKey: parts[0], principal: parts[1], spaceId: parts[2] };
43
+ }
44
+ // Legacy 2-field, or malformed → first segment is the appKey, last the spaceId.
45
+ return { appKey: parts[0], spaceId: parts[parts.length - 1] };
46
+ };
47
+ exports.parseGrantKey = parseGrantKey;
25
48
  /** Durable elevated/app-scoped grants expire after 90 days WITHOUT USE; first
26
49
  * use after expiry re-prompts. Baseline needs no grant record, so this never
27
50
  * touches it. */
@@ -123,6 +146,11 @@ const appSpaceGrantFields = (params, s) => (0, exports.defined)({
123
146
  grantedAt: s.serverTimestamp(),
124
147
  lastUsedAt: s.serverTimestamp(),
125
148
  name: params.name,
149
+ // R3-98 S3/S4 — the named principal this grant was minted under (design 05a
150
+ // §3.1). `defined()` omits it when absent, so a legacy/unkeyed mint writes no
151
+ // `principal` field and is grandfathered at the gate (both adapters stamp it
152
+ // identically, keeping the byte-identical-doc guarantee).
153
+ principal: params.principal,
126
154
  // UI_AS_APPS_SPEC §8.7: `rules` is authoritative; `subtree`/`mode` are kept as the
127
155
  // deprecated `rules[0]` mirror for not-yet-migrated readers. When no rule-set
128
156
  // is given, derive a single-rule set from the legacy scope so the backend
@@ -158,3 +186,19 @@ const netFetchGrantFields = (mergedHosts, hadGrantedAt, s) => (0, exports.define
158
186
  netFetchLastUsedAt: s.serverTimestamp(),
159
187
  });
160
188
  exports.netFetchGrantFields = netFetchGrantFields;
189
+ /** Union granted PLAIN app-scoped capability names (set semantics; sorted for a
190
+ * stable, byte-faithful document) — the "consent accumulates" merge for the
191
+ * R3-233 capability grant, mirroring {@link mergeNetFetchHosts}. */
192
+ const mergeCapabilities = (existing, incoming) => [...new Set([...existing, ...incoming])].sort();
193
+ exports.mergeCapabilities = mergeCapabilities;
194
+ /** `user-app-spaces/{uid}/apps/{appKey}` — the durable granted PLAIN app-scoped
195
+ * capability set (merge), R3-233. Lives on the SAME appKey doc as the net:fetch
196
+ * grant so one read (`getAppGrantDoc`) yields both. `capabilitiesGrantedAt` is
197
+ * stamped ONCE (first mint); `capabilitiesLastUsedAt` refreshes on every
198
+ * (re-)consent — the §8.15 90-day-unused expiry clock, identical to net:fetch. */
199
+ const appCapabilitiesGrantFields = (mergedCaps, hadGrantedAt, s) => (0, exports.defined)({
200
+ grantedCapabilities: [...mergedCaps],
201
+ capabilitiesGrantedAt: hadGrantedAt ? undefined : s.serverTimestamp(),
202
+ capabilitiesLastUsedAt: s.serverTimestamp(),
203
+ });
204
+ exports.appCapabilitiesGrantFields = appCapabilitiesGrantFields;
@@ -46,8 +46,17 @@ export interface PreAuthResult {
46
46
  }
47
47
  /**
48
48
  * The M1 write path: validate the requested capabilities against the §8.9 target
49
- * check, then — only if clean — mint the mounts + net:fetch hosts as durable
50
- * grants with `policy` provenance, through the same `mintConsentedGrants` M3 uses.
49
+ * check, then — only if clean — mint the mounts, net:fetch hosts, AND the plain
50
+ * app-scoped on/off capabilities (`task:invoke`, `llm:chat`, `contribute:self`,
51
+ * `diagnostics:read`) as durable grants with `policy` provenance, through the same
52
+ * `mintConsentedGrants` M3 uses.
53
+ *
54
+ * R3-233: the `grantable` app-scoped caps used to be VALIDATED and then silently
55
+ * DROPPED (only mounts + hosts were minted), so pre-authorizing `task:invoke` /
56
+ * `llm:chat` reported success but granted nothing and the gate kept refusing. They
57
+ * are now actually minted. `net:fetch` is excluded from the plain-cap mint — it is
58
+ * host-parameterized and granted via `netFetchHosts` (a bare grant would be
59
+ * unbounded).
51
60
  *
52
61
  * Refusal is terminal and silent of side effects: when any requested capability
53
62
  * is broad-elevated or unknown, the function mints NOTHING and returns the
package/dist/m1PreAuth.js CHANGED
@@ -69,8 +69,17 @@ const isPreAuthClean = (plan) => plan.refused.length === 0;
69
69
  exports.isPreAuthClean = isPreAuthClean;
70
70
  /**
71
71
  * The M1 write path: validate the requested capabilities against the §8.9 target
72
- * check, then — only if clean — mint the mounts + net:fetch hosts as durable
73
- * grants with `policy` provenance, through the same `mintConsentedGrants` M3 uses.
72
+ * check, then — only if clean — mint the mounts, net:fetch hosts, AND the plain
73
+ * app-scoped on/off capabilities (`task:invoke`, `llm:chat`, `contribute:self`,
74
+ * `diagnostics:read`) as durable grants with `policy` provenance, through the same
75
+ * `mintConsentedGrants` M3 uses.
76
+ *
77
+ * R3-233: the `grantable` app-scoped caps used to be VALIDATED and then silently
78
+ * DROPPED (only mounts + hosts were minted), so pre-authorizing `task:invoke` /
79
+ * `llm:chat` reported success but granted nothing and the gate kept refusing. They
80
+ * are now actually minted. `net:fetch` is excluded from the plain-cap mint — it is
81
+ * host-parameterized and granted via `netFetchHosts` (a bare grant would be
82
+ * unbounded).
74
83
  *
75
84
  * Refusal is terminal and silent of side effects: when any requested capability
76
85
  * is broad-elevated or unknown, the function mints NOTHING and returns the
@@ -81,6 +90,9 @@ async function applyPreAuth(store, uid, appKey, request, onError) {
81
90
  if (!(0, exports.isPreAuthClean)(plan)) {
82
91
  return { ok: false, refused: plan.refused };
83
92
  }
84
- const mint = await (0, bootConsent_1.mintConsentedGrants)(store, uid, appKey, request.mounts, request.netFetchHosts, 'policy', onError);
93
+ // The plain on/off caps to mint: every grantable cap EXCEPT the host-parameterized
94
+ // ones (net:fetch), which are minted as their host set via `netFetchHosts`.
95
+ const plainCaps = plan.grantable.filter((c) => !(0, capabilities_1.isHostParameterized)(c));
96
+ const mint = await (0, bootConsent_1.mintConsentedGrants)(store, uid, appKey, request.mounts, request.netFetchHosts, 'policy', onError, plainCaps);
85
97
  return { ok: mint.ok, refused: [], mint };
86
98
  }
package/dist/port.d.ts CHANGED
@@ -60,6 +60,11 @@ export interface GrantSpaceParams {
60
60
  mintPath?: MintPath;
61
61
  /** §8.15 — parent `grantKey` for an M2 `delegated` grant. */
62
62
  parentGrantId?: string;
63
+ /** R3-98 S3/S4 — the **named principal** this grant is minted under (design 05a
64
+ * §3.1). Written as the `principal` field so the mount-admission gate re-checks
65
+ * it (a grant fires only under the principal it was minted with). Optional +
66
+ * additive: omitted ⇒ no field ⇒ a legacy/grandfathered grant. */
67
+ principal?: string;
63
68
  }
64
69
  /** Parameters for `MintStore.grantNetFetchHosts` — the per-(user, app) granted
65
70
  * host set, the grant half of the `manifest ∩ grant` net:fetch allowlist. */
@@ -68,6 +73,19 @@ export interface GrantNetFetchParams {
68
73
  appKey: string;
69
74
  hosts: readonly NetFetchHost[];
70
75
  }
76
+ /** Parameters for `MintStore.grantAppCapabilities` — the durable §8.7 grant of a
77
+ * set of PLAIN (on/off) app-scoped elevated capabilities for one (user, app):
78
+ * `task:invoke`, `llm:chat`, `contribute:self`, `diagnostics:read` (R3-233). NOT
79
+ * `net:fetch` — that is host-parameterized and granted via {@link GrantNetFetchParams};
80
+ * a bare `net:fetch` grant would be unbounded. The set unions into the app's
81
+ * granted-capability set (consent accumulates), mirroring net:fetch hosts. */
82
+ export interface GrantAppCapabilitiesParams {
83
+ uid: string;
84
+ appKey: string;
85
+ capabilities: readonly string[];
86
+ /** §8.15 provenance; defaults to `interactive` when omitted. */
87
+ mintPath?: MintPath;
88
+ }
71
89
  /**
72
90
  * The mint port: exactly the methods `mintConsentedGrants` calls. Every backend
73
91
  * (browser Firestore, admin Firestore, the in-memory test double) implements
@@ -81,4 +99,11 @@ export interface MintStore {
81
99
  grantSpaceToApp(params: GrantSpaceParams): Promise<void>;
82
100
  /** Union the given net:fetch hosts into the app's consented host set. */
83
101
  grantNetFetchHosts(params: GrantNetFetchParams): Promise<void>;
102
+ /** Union the given PLAIN app-scoped capabilities into the app's granted set
103
+ * (R3-233). **Optional** so existing {@link MintStore} implementers (the backend
104
+ * `AdminMintStore`) keep compiling; when a caller asks to mint plain caps and an
105
+ * adapter does not implement this, `mintConsentedGrants` fails LOUD (reports
106
+ * `capabilitiesOk:false`) rather than silently dropping them — the exact
107
+ * validate-then-drop bug R3-233 fixes. */
108
+ grantAppCapabilities?(params: GrantAppCapabilitiesParams): Promise<void>;
84
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/preauth-core",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "The shared §8.9 pre-auth target check + the single grant-mint path (mintConsentedGrants) + the capability vocabulary + the byte-faithful grant/space/net-fetch document layout. Consumed by site-main (browser Firestore) and the backend (admin Firestore) so there is ONE gate, ONE mint path, ONE wire layout.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {