@immediately-run/preauth-core 0.1.6 → 0.1.7

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`).
@@ -59,3 +59,13 @@ export declare const mergeNetFetchHosts: (existing: readonly NetFetchHost[], inc
59
59
  * grant time is stamped ONCE, on first mint, and `netFetchLastUsedAt` refreshes
60
60
  * on every (re-)consent). */
61
61
  export declare const netFetchGrantFields: (mergedHosts: readonly NetFetchHost[], hadGrantedAt: boolean, s: MintSentinels) => Record<string, unknown>;
62
+ /** Union granted PLAIN app-scoped capability names (set semantics; sorted for a
63
+ * stable, byte-faithful document) — the "consent accumulates" merge for the
64
+ * R3-233 capability grant, mirroring {@link mergeNetFetchHosts}. */
65
+ export declare const mergeCapabilities: (existing: readonly string[], incoming: readonly string[]) => string[];
66
+ /** `user-app-spaces/{uid}/apps/{appKey}` — the durable granted PLAIN app-scoped
67
+ * capability set (merge), R3-233. Lives on the SAME appKey doc as the net:fetch
68
+ * grant so one read (`getAppGrantDoc`) yields both. `capabilitiesGrantedAt` is
69
+ * stamped ONCE (first mint); `capabilitiesLastUsedAt` refreshes on every
70
+ * (re-)consent — the §8.15 90-day-unused expiry clock, identical to net:fetch. */
71
+ export declare const appCapabilitiesGrantFields: (mergedCaps: readonly string[], hadGrantedAt: boolean, s: MintSentinels) => Record<string, unknown>;
package/dist/docLayout.js CHANGED
@@ -16,7 +16,7 @@
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.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. */
@@ -158,3 +158,19 @@ const netFetchGrantFields = (mergedHosts, hadGrantedAt, s) => (0, exports.define
158
158
  netFetchLastUsedAt: s.serverTimestamp(),
159
159
  });
160
160
  exports.netFetchGrantFields = netFetchGrantFields;
161
+ /** Union granted PLAIN app-scoped capability names (set semantics; sorted for a
162
+ * stable, byte-faithful document) — the "consent accumulates" merge for the
163
+ * R3-233 capability grant, mirroring {@link mergeNetFetchHosts}. */
164
+ const mergeCapabilities = (existing, incoming) => [...new Set([...existing, ...incoming])].sort();
165
+ exports.mergeCapabilities = mergeCapabilities;
166
+ /** `user-app-spaces/{uid}/apps/{appKey}` — the durable granted PLAIN app-scoped
167
+ * capability set (merge), R3-233. Lives on the SAME appKey doc as the net:fetch
168
+ * grant so one read (`getAppGrantDoc`) yields both. `capabilitiesGrantedAt` is
169
+ * stamped ONCE (first mint); `capabilitiesLastUsedAt` refreshes on every
170
+ * (re-)consent — the §8.15 90-day-unused expiry clock, identical to net:fetch. */
171
+ const appCapabilitiesGrantFields = (mergedCaps, hadGrantedAt, s) => (0, exports.defined)({
172
+ grantedCapabilities: [...mergedCaps],
173
+ capabilitiesGrantedAt: hadGrantedAt ? undefined : s.serverTimestamp(),
174
+ capabilitiesLastUsedAt: s.serverTimestamp(),
175
+ });
176
+ 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
@@ -68,6 +68,19 @@ export interface GrantNetFetchParams {
68
68
  appKey: string;
69
69
  hosts: readonly NetFetchHost[];
70
70
  }
71
+ /** Parameters for `MintStore.grantAppCapabilities` — the durable §8.7 grant of a
72
+ * set of PLAIN (on/off) app-scoped elevated capabilities for one (user, app):
73
+ * `task:invoke`, `llm:chat`, `contribute:self`, `diagnostics:read` (R3-233). NOT
74
+ * `net:fetch` — that is host-parameterized and granted via {@link GrantNetFetchParams};
75
+ * a bare `net:fetch` grant would be unbounded. The set unions into the app's
76
+ * granted-capability set (consent accumulates), mirroring net:fetch hosts. */
77
+ export interface GrantAppCapabilitiesParams {
78
+ uid: string;
79
+ appKey: string;
80
+ capabilities: readonly string[];
81
+ /** §8.15 provenance; defaults to `interactive` when omitted. */
82
+ mintPath?: MintPath;
83
+ }
71
84
  /**
72
85
  * The mint port: exactly the methods `mintConsentedGrants` calls. Every backend
73
86
  * (browser Firestore, admin Firestore, the in-memory test double) implements
@@ -81,4 +94,11 @@ export interface MintStore {
81
94
  grantSpaceToApp(params: GrantSpaceParams): Promise<void>;
82
95
  /** Union the given net:fetch hosts into the app's consented host set. */
83
96
  grantNetFetchHosts(params: GrantNetFetchParams): Promise<void>;
97
+ /** Union the given PLAIN app-scoped capabilities into the app's granted set
98
+ * (R3-233). **Optional** so existing {@link MintStore} implementers (the backend
99
+ * `AdminMintStore`) keep compiling; when a caller asks to mint plain caps and an
100
+ * adapter does not implement this, `mintConsentedGrants` fails LOUD (reports
101
+ * `capabilitiesOk:false`) rather than silently dropping them — the exact
102
+ * validate-then-drop bug R3-233 fixes. */
103
+ grantAppCapabilities?(params: GrantAppCapabilitiesParams): Promise<void>;
84
104
  }
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.7",
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": {