@immediately-run/preauth-core 0.1.7 → 0.1.9

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.
@@ -13,6 +13,56 @@ 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;
40
+ /** The doc-id delimiter between a qualifying principal and the spaceId. Safe: a
41
+ * named principal is lowercase-dotted/hyphenated (CA-3 reserves `~`) and a
42
+ * Firestore spaceId is alphanumeric, so `~` appears in NEITHER — a single,
43
+ * unambiguous split point. */
44
+ export declare const GRANT_DOCID_DELIM = "~";
45
+ /** Build a space-grant doc-id (design 05a §3.1 step 2). Pass the QUALIFYING named
46
+ * principal to get `${principal}~${spaceId}`; pass `undefined` (stage / legacy /
47
+ * no principal) for the bare `spaceId`. The caller resolves "does this principal
48
+ * qualify" (site-main maps stage/legacy → undefined) so this stays a pure string
49
+ * builder with no sentinel knowledge. */
50
+ export declare const grantDocId: (spaceId: string, qualifyingPrincipal?: string) => string;
51
+ /** A parsed grant doc-id — the §3.5 reader-parse discipline. `principal` is set
52
+ * only for a QUALIFIED (`${principal}~${spaceId}`) id; a bare id (a stage/legacy
53
+ * grant) yields `{ spaceId }` with `principal` undefined. */
54
+ export interface ParsedGrantDocId {
55
+ /** The qualifying principal, or undefined for a bare (stage/legacy) doc-id. */
56
+ principal?: string;
57
+ spaceId: string;
58
+ }
59
+ /** Parse a space-grant doc-id back into `{ principal?, spaceId }` — the §3.5
60
+ * reader-parse discipline every app-space-grant collection reader routes `d.id`
61
+ * through so it never mistakes `${principal}~${spaceId}` for a bare spaceId (which
62
+ * would corrupt the derived `mountId` and leak grants across principals). Splits
63
+ * on the FIRST delimiter; a named principal never contains `~`, so this recovers
64
+ * the exact principal + spaceId. A bare id (no delimiter) ⇒ `{ spaceId }`. */
65
+ export declare const parseGrantDocId: (docId: string) => ParsedGrantDocId;
16
66
  /** Durable elevated/app-scoped grants expire after 90 days WITHOUT USE; first
17
67
  * use after expiry re-prompts. Baseline needs no grant record, so this never
18
68
  * touches it. */
@@ -31,7 +81,12 @@ export declare const spacePath: (spaceId: string) => DocPath;
31
81
  export declare const memberPath: (spaceId: string, grantee: string) => DocPath;
32
82
  export declare const userSpacePath: (uid: string, spaceId: string) => DocPath;
33
83
  export declare const appKeyPath: (uid: string, appKey: string) => DocPath;
34
- export declare const appSpacePath: (uid: string, appKey: string, spaceId: string) => DocPath;
84
+ /** `user-app-spaces/{uid}/apps/{appKey}/spaces/{docId}` the durable §8.7 grant
85
+ * doc. R3-98 S5: the doc-id is principal-qualified — pass the QUALIFYING named
86
+ * principal for `${principal}~${spaceId}`, or omit it (stage / legacy) for the
87
+ * bare `spaceId`. Backward-compatible: a 3-arg call (no principal) yields exactly
88
+ * the pre-S5 path, so the backend/CLI stage mint is byte-identical. */
89
+ export declare const appSpacePath: (uid: string, appKey: string, spaceId: string, qualifyingPrincipal?: string) => DocPath;
35
90
  export declare const userCountPath: (uid: string) => DocPath;
36
91
  export declare const appCountPath: (uid: string, appKey: string) => DocPath;
37
92
  /** `spaces/{spaceId}` — the root doc (written WITHOUT merge). */
@@ -50,7 +105,7 @@ export declare const appKeyTouchFields: (s: MintSentinels) => Record<string, unk
50
105
  /** `user-app-spaces/{uid}/apps/{appKey}/spaces/{spaceId}` — the durable §8.7
51
106
  * grant doc (merge). `mintPath` defaults to `interactive`; `grantedAt`/`lastUsedAt`
52
107
  * 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>;
108
+ export declare const appSpaceGrantFields: (params: Pick<GrantSpaceParams, "name" | "subtree" | "mode" | "rules" | "declaredUri" | "mintPath" | "parentGrantId" | "principal">, s: MintSentinels) => Record<string, unknown>;
54
109
  /** Union net:fetch host rules by origin (incoming wins) — the "consent
55
110
  * accumulates" merge both adapters apply before writing the host set. */
56
111
  export declare const mergeNetFetchHosts: (existing: readonly NetFetchHost[], incoming: readonly NetFetchHost[]) => NetFetchHost[];
package/dist/docLayout.js CHANGED
@@ -16,12 +16,70 @@
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.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;
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.parseGrantDocId = exports.grantDocId = exports.GRANT_DOCID_DELIM = 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;
48
+ // --- R3-98 S5 — the principal-qualified space-grant doc-id (design 05a §3.1/§3.5) --
49
+ //
50
+ // A space grant's Firestore doc-id encodes the named principal it was minted
51
+ // under, so two principals granting the SAME space live at DIFFERENT docs and are
52
+ // invisible to each other (structural disjointness). The rule (design 05a §3.1
53
+ // step 2): a **qualifying** (real, named) principal → `${principal}~${spaceId}`;
54
+ // the **stage** principal, a **legacy** (no-principal) grant, or none → the bare
55
+ // `spaceId`, so no existing/stage doc ever moves. This module is GRAMMAR ONLY: the
56
+ // caller decides which principals qualify (site-main owns the stage/legacy
57
+ // sentinels — a principal it treats as non-qualifying is passed as `undefined`).
58
+ /** The doc-id delimiter between a qualifying principal and the spaceId. Safe: a
59
+ * named principal is lowercase-dotted/hyphenated (CA-3 reserves `~`) and a
60
+ * Firestore spaceId is alphanumeric, so `~` appears in NEITHER — a single,
61
+ * unambiguous split point. */
62
+ exports.GRANT_DOCID_DELIM = '~';
63
+ /** Build a space-grant doc-id (design 05a §3.1 step 2). Pass the QUALIFYING named
64
+ * principal to get `${principal}~${spaceId}`; pass `undefined` (stage / legacy /
65
+ * no principal) for the bare `spaceId`. The caller resolves "does this principal
66
+ * qualify" (site-main maps stage/legacy → undefined) so this stays a pure string
67
+ * builder with no sentinel knowledge. */
68
+ const grantDocId = (spaceId, qualifyingPrincipal) => qualifyingPrincipal ? `${qualifyingPrincipal}${exports.GRANT_DOCID_DELIM}${spaceId}` : spaceId;
69
+ exports.grantDocId = grantDocId;
70
+ /** Parse a space-grant doc-id back into `{ principal?, spaceId }` — the §3.5
71
+ * reader-parse discipline every app-space-grant collection reader routes `d.id`
72
+ * through so it never mistakes `${principal}~${spaceId}` for a bare spaceId (which
73
+ * would corrupt the derived `mountId` and leak grants across principals). Splits
74
+ * on the FIRST delimiter; a named principal never contains `~`, so this recovers
75
+ * the exact principal + spaceId. A bare id (no delimiter) ⇒ `{ spaceId }`. */
76
+ const parseGrantDocId = (docId) => {
77
+ const i = docId.indexOf(exports.GRANT_DOCID_DELIM);
78
+ return i === -1
79
+ ? { spaceId: docId }
80
+ : { principal: docId.slice(0, i), spaceId: docId.slice(i + 1) };
81
+ };
82
+ exports.parseGrantDocId = parseGrantDocId;
25
83
  /** Durable elevated/app-scoped grants expire after 90 days WITHOUT USE; first
26
84
  * use after expiry re-prompts. Baseline needs no grant record, so this never
27
85
  * touches it. */
@@ -62,13 +120,18 @@ const appKeyPath = (uid, appKey) => [
62
120
  appKey,
63
121
  ];
64
122
  exports.appKeyPath = appKeyPath;
65
- const appSpacePath = (uid, appKey, spaceId) => [
123
+ /** `user-app-spaces/{uid}/apps/{appKey}/spaces/{docId}` the durable §8.7 grant
124
+ * doc. R3-98 S5: the doc-id is principal-qualified — pass the QUALIFYING named
125
+ * principal for `${principal}~${spaceId}`, or omit it (stage / legacy) for the
126
+ * bare `spaceId`. Backward-compatible: a 3-arg call (no principal) yields exactly
127
+ * the pre-S5 path, so the backend/CLI stage mint is byte-identical. */
128
+ const appSpacePath = (uid, appKey, spaceId, qualifyingPrincipal) => [
66
129
  'user-app-spaces',
67
130
  uid,
68
131
  'apps',
69
132
  appKey,
70
133
  'spaces',
71
- spaceId,
134
+ (0, exports.grantDocId)(spaceId, qualifyingPrincipal),
72
135
  ];
73
136
  exports.appSpacePath = appSpacePath;
74
137
  const userCountPath = (uid) => ['space-counts', uid];
@@ -123,6 +186,11 @@ const appSpaceGrantFields = (params, s) => (0, exports.defined)({
123
186
  grantedAt: s.serverTimestamp(),
124
187
  lastUsedAt: s.serverTimestamp(),
125
188
  name: params.name,
189
+ // R3-98 S3/S4 — the named principal this grant was minted under (design 05a
190
+ // §3.1). `defined()` omits it when absent, so a legacy/unkeyed mint writes no
191
+ // `principal` field and is grandfathered at the gate (both adapters stamp it
192
+ // identically, keeping the byte-identical-doc guarantee).
193
+ principal: params.principal,
126
194
  // UI_AS_APPS_SPEC §8.7: `rules` is authoritative; `subtree`/`mode` are kept as the
127
195
  // deprecated `rules[0]` mirror for not-yet-migrated readers. When no rule-set
128
196
  // is given, derive a single-rule set from the legacy scope so the backend
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/preauth-core",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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": {