@immediately-run/preauth-core 0.1.9 → 0.1.11
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.
- package/dist/capabilities.d.ts +7 -7
- package/dist/capabilities.js +24 -6
- package/dist/docLayout.d.ts +13 -0
- package/dist/docLayout.js +72 -4
- package/package.json +1 -1
package/dist/capabilities.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type CapabilityKind = 'read' | 'action';
|
|
2
2
|
export type CapabilityTier = 'baseline' | 'elevated' | 'first-party-only';
|
|
3
|
-
export type Capability = 'theme:read' | 'theme:set' | 'auth:status' | 'auth:identity' | 'route:read' | 'formFactor:read' | 'mounts:read' | 'mounts:registry' | 'spaces:app' | 'spaces:user' | 'spaces:admin' | 'settings:app' | 'settings:fork' | 'settings:all' | 'contribute:self' | 'contribute:any' | 'contribute:direct' | 'editor:read' | 'editor:open' | 'editor:write' | 'editor:document' | 'editor:requestEdit' | 'vcs:read' | 'vcs:reset' | 'dnd:source' | 'catalog:read' | 'commands:read' | 'commands:run' | 'ipc' | 'task:invoke' | 'net:fetch' | 'secrets:add' | 'secrets:list' | 'secrets:revoke' | 'agent:session' | 'diagnostics:read' | 'llm:chat' | 'authoring:run';
|
|
3
|
+
export type Capability = 'theme:read' | 'theme:set' | 'auth:status' | 'auth:identity' | 'route:read' | 'formFactor:read' | 'mounts:read' | 'mounts:registry' | 'spaces:app' | 'spaces:user' | 'spaces:admin' | 'settings:app' | 'settings:fork' | 'settings:all' | 'contribute:self' | 'contribute:any' | 'contribute:direct' | 'editor:read' | 'editor:open' | 'editor:write' | 'editor:document' | 'editor:requestEdit' | 'vcs:read' | 'vcs:reset' | 'dnd:source' | 'catalog:read' | 'commands:read' | 'commands:run' | 'ipc' | 'task:invoke' | 'net:fetch' | 'secrets:add' | 'secrets:list' | 'secrets:revoke' | 'agent:session' | 'diagnostics:read' | 'llm:chat' | 'authoring:run' | 'analytics:emit';
|
|
4
4
|
export interface CapabilityDef {
|
|
5
5
|
kind: CapabilityKind;
|
|
6
6
|
tier: CapabilityTier;
|
|
@@ -31,12 +31,12 @@ export interface CapabilityDef {
|
|
|
31
31
|
maximallyExplicit?: boolean;
|
|
32
32
|
}
|
|
33
33
|
export declare const CAPABILITIES: Record<Capability, CapabilityDef>;
|
|
34
|
-
/** The current registry/vocabulary version (§5.11). Bumped to 1.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* 1.
|
|
38
|
-
* settings-space capabilities.) */
|
|
39
|
-
export declare const REGISTRY_VERSION = "1.
|
|
34
|
+
/** The current registry/vocabulary version (§5.11). Bumped to 1.6.0 with the
|
|
35
|
+
* elevated, app-scoped, parameterized `analytics:emit` (APP_ANALYTICS_SPEC §2 —
|
|
36
|
+
* R3-350), mirroring capabilities.json. (1.5.0 added the first-party-only
|
|
37
|
+
* `mounts:registry`; 1.4.0 added `authoring:run`; 1.3.0 added the provider-agnostic
|
|
38
|
+
* `llm:chat` slot; 1.2.0 added the per-user settings-space capabilities.) */
|
|
39
|
+
export declare const REGISTRY_VERSION = "1.6.0";
|
|
40
40
|
/** Is `cap` a known host-core capability? (Closed vocabulary — §5.12.) */
|
|
41
41
|
export declare function isKnownCapability(cap: string): cap is Capability;
|
|
42
42
|
export declare function tierOf(cap: Capability): CapabilityTier;
|
package/dist/capabilities.js
CHANGED
|
@@ -208,13 +208,31 @@ exports.CAPABILITIES = {
|
|
|
208
208
|
// authority with nothing to confine (§3a), and is bounded per call (timeout +
|
|
209
209
|
// input-size). Not app-scoped: it confers no authority over other apps/mounts.
|
|
210
210
|
'authoring:run': { kind: 'action', tier: 'baseline', since: '1.4.0' },
|
|
211
|
+
// APP_ANALYTICS_SPEC §2 (R3-350): emit an app's OWN usage analytics through the
|
|
212
|
+
// platform's pipeline, against a vocabulary the app declares in its manifest.
|
|
213
|
+
//
|
|
214
|
+
// **ELEVATED + appScoped, and NOT baseline.** The earlier spec draft wrote
|
|
215
|
+
// "baseline, consented", which is not a tier and not a thing: `baseline` IS the
|
|
216
|
+
// floor — `buildConsent` short-circuits `tier === 'baseline'` before generating any
|
|
217
|
+
// consent line, and a previewed frame is seeded with the baseline set directly. At
|
|
218
|
+
// baseline this would hand EVERY app on the platform an unconsented, unrevocable,
|
|
219
|
+
// un-journalled egress capability. CAPABILITY_REFERENCE had already closed the
|
|
220
|
+
// question: "'App-scoped' is NOT a fourth tier. It is a consent-path annotation on
|
|
221
|
+
// the elevated tier."
|
|
222
|
+
//
|
|
223
|
+
// **Parameterized**, because the grant is not "may emit" but "may emit THIS
|
|
224
|
+
// vocabulary": §2.1 hashes the manifest-declared vocabulary into the grant, and a
|
|
225
|
+
// changed hash invalidates it. `appKey` carries no ref, so without that binding a
|
|
226
|
+
// publisher could observe their aggregates and then ship an alphabet tuned to encode
|
|
227
|
+
// what they now want to read, under a grant the user gave for a different one.
|
|
228
|
+
'analytics:emit': { kind: 'action', tier: 'elevated', since: '1.6.0', parameterized: true, appScoped: true },
|
|
211
229
|
};
|
|
212
|
-
/** The current registry/vocabulary version (§5.11). Bumped to 1.
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* 1.
|
|
216
|
-
* settings-space capabilities.) */
|
|
217
|
-
exports.REGISTRY_VERSION = '1.
|
|
230
|
+
/** The current registry/vocabulary version (§5.11). Bumped to 1.6.0 with the
|
|
231
|
+
* elevated, app-scoped, parameterized `analytics:emit` (APP_ANALYTICS_SPEC §2 —
|
|
232
|
+
* R3-350), mirroring capabilities.json. (1.5.0 added the first-party-only
|
|
233
|
+
* `mounts:registry`; 1.4.0 added `authoring:run`; 1.3.0 added the provider-agnostic
|
|
234
|
+
* `llm:chat` slot; 1.2.0 added the per-user settings-space capabilities.) */
|
|
235
|
+
exports.REGISTRY_VERSION = '1.6.0';
|
|
218
236
|
/** Is `cap` a known host-core capability? (Closed vocabulary — §5.12.) */
|
|
219
237
|
function isKnownCapability(cap) {
|
|
220
238
|
return Object.prototype.hasOwnProperty.call(exports.CAPABILITIES, cap);
|
package/dist/docLayout.d.ts
CHANGED
|
@@ -77,6 +77,19 @@ export declare const granteeId: (uid: string) => string;
|
|
|
77
77
|
* each had their own copy of this; sharing it keeps the "omit absent optionals"
|
|
78
78
|
* rule identical on both sides. */
|
|
79
79
|
export declare const defined: <T extends Record<string, unknown>>(obj: T) => T;
|
|
80
|
+
/** Thrown when an `appKey` is not a single Firestore path segment. Carries a
|
|
81
|
+
* machine `code` so a caller can map it to its own error vocabulary. */
|
|
82
|
+
export declare class InvalidAppKeyError extends Error {
|
|
83
|
+
readonly code = "invalid-app-key";
|
|
84
|
+
constructor(appKey: string, why: string);
|
|
85
|
+
}
|
|
86
|
+
/** Is `appKey` usable as exactly one Firestore path segment? Empty, `/`-bearing,
|
|
87
|
+
* and the two relative-path doc-ids Firestore reserves are all refused. */
|
|
88
|
+
export declare const isAppKeySegment: (appKey: string) => boolean;
|
|
89
|
+
/** Refuse an `appKey` that is not one path segment — the shared chokepoint every
|
|
90
|
+
* grant-store path builder runs first (R3-285). Returns the key so it can wrap a
|
|
91
|
+
* segment in place. */
|
|
92
|
+
export declare const assertAppKeySegment: (appKey: string) => string;
|
|
80
93
|
export declare const spacePath: (spaceId: string) => DocPath;
|
|
81
94
|
export declare const memberPath: (spaceId: string, grantee: string) => DocPath;
|
|
82
95
|
export declare const userSpacePath: (uid: string, spaceId: string) => DocPath;
|
package/dist/docLayout.js
CHANGED
|
@@ -15,8 +15,18 @@
|
|
|
15
15
|
// `FieldValue.serverTimestamp()`/`FieldValue.increment()`). The raw
|
|
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
|
+
//
|
|
19
|
+
// HONESTY NOTE (R3-285): that guarantee holds TODAY for the FIELD builders only.
|
|
20
|
+
// The browser `FirestoreSpaceStore` builds its refs with the Web SDK's variadic
|
|
21
|
+
// `doc(db, 'user-app-spaces', uid, 'apps', appKey, …)` and does NOT call the
|
|
22
|
+
// `*Path` builders below — they are backend-only. The two constructions are
|
|
23
|
+
// equivalent (`doc()` joins with `/` and re-parses exactly as the backend's
|
|
24
|
+
// `segments.join('/')` does), but "one source for the paths" is an aspiration
|
|
25
|
+
// here, not a fact, and a bug fixed in a `*Path` builder does not reach the
|
|
26
|
+
// browser. What both sides DO share is `assertAppKeySegment` — the one property
|
|
27
|
+
// a wrong path would violate. Unifying the ref construction is tracked debt.
|
|
18
28
|
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.parseGrantDocId = exports.grantDocId = exports.GRANT_DOCID_DELIM = exports.parseGrantKey = exports.grantKeyWithPrincipal = exports.grantKey = void 0;
|
|
29
|
+
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.assertAppKeySegment = exports.isAppKeySegment = exports.InvalidAppKeyError = 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
30
|
/** Stable per-user identifier for a grant `(appKey, spaceId)`, used as the value
|
|
21
31
|
* of a delegated grant's `parentGrantId`. `::` is delimiter-safe: `appKey` uses
|
|
22
32
|
* `__` separators and a Firestore `spaceId` is alphanumeric. */
|
|
@@ -96,6 +106,64 @@ exports.granteeId = granteeId;
|
|
|
96
106
|
* rule identical on both sides. */
|
|
97
107
|
const defined = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
98
108
|
exports.defined = defined;
|
|
109
|
+
// --- the appKey grammar guard (R3-285) --------------------------------------
|
|
110
|
+
//
|
|
111
|
+
// An `appKey` is ONE Firestore path segment. The canonical grammar is
|
|
112
|
+
// site-main's `spaceId.appKey()` — `enc(provider)__enc(namespace)__enc(repository)`
|
|
113
|
+
// — which is punctuation-free by construction. The DANGER is the neighbouring
|
|
114
|
+
// grammar: a *binding id* (`provider:namespace/repository`) is a different
|
|
115
|
+
// identifier that also names a repo, and feeding one to the grant store is a
|
|
116
|
+
// silent catastrophe rather than a loud one:
|
|
117
|
+
//
|
|
118
|
+
// • one slash (`github:acme/notes`) → the joined path has an ODD segment
|
|
119
|
+
// count, so `doc()` throws `invalid-argument` — noisy, fails closed;
|
|
120
|
+
// • two slashes (`gitlab:g/sub/notes`, a nested namespace) → the path is EVEN
|
|
121
|
+
// and perfectly valid, so the grant is WRITTEN — to a document no reader
|
|
122
|
+
// looks at, that the §8.11 audit view does not enumerate, and that the
|
|
123
|
+
// §8.15 revoke cascade cannot reach. A durable, invisible, unrevokable grant.
|
|
124
|
+
//
|
|
125
|
+
// So the fix is NOT to encode: encoding would make the second case succeed
|
|
126
|
+
// quietly at the wrong key. It is to refuse a key that is not one segment, at
|
|
127
|
+
// the one place both adapters can share, and let the caller be corrected.
|
|
128
|
+
//
|
|
129
|
+
// This asserts the SEGMENT property (what Firestore requires), not the `__`
|
|
130
|
+
// grammar (which lives in site-main and may still gain components) — the widest
|
|
131
|
+
// check that still catches every wrong-grammar key we have seen.
|
|
132
|
+
/** Thrown when an `appKey` is not a single Firestore path segment. Carries a
|
|
133
|
+
* machine `code` so a caller can map it to its own error vocabulary. */
|
|
134
|
+
class InvalidAppKeyError extends Error {
|
|
135
|
+
code = 'invalid-app-key';
|
|
136
|
+
constructor(appKey, why) {
|
|
137
|
+
super(`appKey ${JSON.stringify(appKey)} is not one Firestore path segment (${why}). ` +
|
|
138
|
+
'Expected the grant-store key grammar (`provider__namespace__repository`), ' +
|
|
139
|
+
'not a binding id (`provider:namespace/repository`).');
|
|
140
|
+
this.name = 'InvalidAppKeyError';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.InvalidAppKeyError = InvalidAppKeyError;
|
|
144
|
+
/** Is `appKey` usable as exactly one Firestore path segment? Empty, `/`-bearing,
|
|
145
|
+
* and the two relative-path doc-ids Firestore reserves are all refused. */
|
|
146
|
+
const isAppKeySegment = (appKey) => typeof appKey === 'string' &&
|
|
147
|
+
appKey.length > 0 &&
|
|
148
|
+
!appKey.includes('/') &&
|
|
149
|
+
appKey !== '.' &&
|
|
150
|
+
appKey !== '..';
|
|
151
|
+
exports.isAppKeySegment = isAppKeySegment;
|
|
152
|
+
/** Refuse an `appKey` that is not one path segment — the shared chokepoint every
|
|
153
|
+
* grant-store path builder runs first (R3-285). Returns the key so it can wrap a
|
|
154
|
+
* segment in place. */
|
|
155
|
+
const assertAppKeySegment = (appKey) => {
|
|
156
|
+
if (typeof appKey !== 'string' || appKey.length === 0) {
|
|
157
|
+
throw new InvalidAppKeyError(String(appKey), 'empty');
|
|
158
|
+
}
|
|
159
|
+
if (appKey.includes('/'))
|
|
160
|
+
throw new InvalidAppKeyError(appKey, 'contains "/"');
|
|
161
|
+
if (appKey === '.' || appKey === '..') {
|
|
162
|
+
throw new InvalidAppKeyError(appKey, 'is a reserved relative path');
|
|
163
|
+
}
|
|
164
|
+
return appKey;
|
|
165
|
+
};
|
|
166
|
+
exports.assertAppKeySegment = assertAppKeySegment;
|
|
99
167
|
// --- document paths (pure, sentinel-free) -----------------------------------
|
|
100
168
|
const spacePath = (spaceId) => ['spaces', spaceId];
|
|
101
169
|
exports.spacePath = spacePath;
|
|
@@ -117,7 +185,7 @@ const appKeyPath = (uid, appKey) => [
|
|
|
117
185
|
'user-app-spaces',
|
|
118
186
|
uid,
|
|
119
187
|
'apps',
|
|
120
|
-
appKey,
|
|
188
|
+
(0, exports.assertAppKeySegment)(appKey),
|
|
121
189
|
];
|
|
122
190
|
exports.appKeyPath = appKeyPath;
|
|
123
191
|
/** `user-app-spaces/{uid}/apps/{appKey}/spaces/{docId}` — the durable §8.7 grant
|
|
@@ -129,7 +197,7 @@ const appSpacePath = (uid, appKey, spaceId, qualifyingPrincipal) => [
|
|
|
129
197
|
'user-app-spaces',
|
|
130
198
|
uid,
|
|
131
199
|
'apps',
|
|
132
|
-
appKey,
|
|
200
|
+
(0, exports.assertAppKeySegment)(appKey),
|
|
133
201
|
'spaces',
|
|
134
202
|
(0, exports.grantDocId)(spaceId, qualifyingPrincipal),
|
|
135
203
|
];
|
|
@@ -140,7 +208,7 @@ const appCountPath = (uid, appKey) => [
|
|
|
140
208
|
'space-counts',
|
|
141
209
|
uid,
|
|
142
210
|
'apps',
|
|
143
|
-
appKey,
|
|
211
|
+
(0, exports.assertAppKeySegment)(appKey),
|
|
144
212
|
];
|
|
145
213
|
exports.appCountPath = appCountPath;
|
|
146
214
|
// --- field objects (inject the timestamp/increment sentinels) ---------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@immediately-run/preauth-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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": {
|