@immediately-run/preauth-core 0.1.13 → 0.1.17
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/README.md +14 -0
- package/dist/capabilities.d.ts +55 -7
- package/dist/capabilities.js +197 -3
- package/dist/m1PreAuth.d.ts +34 -5
- package/dist/m1PreAuth.js +73 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,20 @@ Spec: `UI_AS_APPS_SPEC.md` §8.9 (target check), §8.15 (M1 pre-authorization),
|
|
|
22
22
|
| `bootConsent` | `mintConsentedGrants` — the ONE mint path. Environment-neutral: a caller passes `onError` instead of the core logging with a host-specific prefix. |
|
|
23
23
|
| `m1PreAuth` | `planPreAuthCapabilities` / `isPreAuthClean` (the pure §8.9 target check) + `applyPreAuth` (validate-then-mint, all-or-nothing). |
|
|
24
24
|
|
|
25
|
+
## Published versions
|
|
26
|
+
|
|
27
|
+
`ci.yml` publishes the **head-of-main** version on each push to `main` — one push, one
|
|
28
|
+
`npm publish` of whatever `package.json` reads at that commit. A branch that bumps the
|
|
29
|
+
version more than once publishes only its **last** bump; the intermediate numbers are
|
|
30
|
+
commits, not releases.
|
|
31
|
+
|
|
32
|
+
**0.1.15 and 0.1.16 were never published.** They are intermediate states of the
|
|
33
|
+
`roadmap-items-r3-407-426` branch — 0.1.15 (`auth:identity` becomes app-scoped) and
|
|
34
|
+
0.1.16 (`device:geolocation`) — and npm goes straight from 0.1.14 to **0.1.17**, which
|
|
35
|
+
carries all of that work. Cite 0.1.17 as the release for anything on that branch:
|
|
36
|
+
pinning 0.1.15 or 0.1.16 fails `npm ci` with `ETARGET`, and site-main pins this package
|
|
37
|
+
EXACTLY.
|
|
38
|
+
|
|
25
39
|
## Consuming it
|
|
26
40
|
|
|
27
41
|
Via the **`file:` sibling pattern** site-main already uses for the sandpack fork:
|
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' | 'chrome: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' | 'feed:fetch' | 'secrets:add' | 'secrets:list' | 'secrets:revoke' | 'agent:session' | 'diagnostics:read' | 'llm:chat' | 'authoring:run' | 'analytics:emit';
|
|
3
|
+
export type Capability = 'theme:read' | 'theme:set' | 'auth:status' | 'auth:identity' | 'route:read' | 'formFactor:read' | 'chrome: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:reveal' | 'editor:write' | 'editor:document' | 'editor:requestEdit' | 'vcs:read' | 'vcs:reset' | 'dnd:source' | 'catalog:read' | 'commands:read' | 'commands:run' | 'ipc' | 'task:invoke' | 'net:fetch' | 'feed:fetch' | 'secrets:add' | 'secrets:list' | 'secrets:revoke' | 'agent:session' | 'diagnostics:read' | 'llm:chat' | 'authoring:run' | 'analytics:emit' | 'device:geolocation' | 'device:camera' | 'device:microphone';
|
|
4
4
|
export interface CapabilityDef {
|
|
5
5
|
kind: CapabilityKind;
|
|
6
6
|
tier: CapabilityTier;
|
|
@@ -16,10 +16,26 @@ export interface CapabilityDef {
|
|
|
16
16
|
* (region binding only). In core_concepts §5 terms the consent-path is the
|
|
17
17
|
* "above-the-floor, up-to-the-ceiling → first-use consent" band: an app-scoped
|
|
18
18
|
* elevated cap sits in that band for the stage principal, a non-app-scoped one
|
|
19
|
-
* is above the stage ceiling (granted only by a slot's elevated principal). The
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* is above the stage ceiling (granted only by a slot's elevated principal). The
|
|
20
|
+
* durable grant participates in the §8.15 90-day expiry like any app-scoped grant.
|
|
21
|
+
*
|
|
22
|
+
* **The app-scoped set, in full** — this is a security boundary, so it is stated
|
|
23
|
+
* completely rather than by example. The authoritative form is DERIVED, never
|
|
24
|
+
* hand-maintained: `APP_SCOPED_CAPABILITIES` below filters this table on the flag,
|
|
25
|
+
* and `isAppScoped` is what every consumer branches on. As of registry 1.12.0 the
|
|
26
|
+
* eleven rows carrying it are:
|
|
27
|
+
*
|
|
28
|
+
* `auth:identity` (R3-407) · `contribute:self` (decision #1 — its
|
|
29
|
+
* baseline→elevated reclassification landed in R3-33d) · `task:invoke` ·
|
|
30
|
+
* `net:fetch` · `feed:fetch` (R3-227) · `diagnostics:read` (R3-74 / P3-72, D4) ·
|
|
31
|
+
* `llm:chat` (D5) · `analytics:emit` (R3-350) · `device:geolocation` (R3-424) ·
|
|
32
|
+
* `device:camera` and `device:microphone` (R3-425).
|
|
33
|
+
*
|
|
34
|
+
* Two of those — `net:fetch` and `feed:fetch` — are additionally HOST-PARAMETERIZED
|
|
35
|
+
* (see `HOST_PARAMETERIZED_CAPABILITIES`), so they are never earned as a bare on/off
|
|
36
|
+
* capability: the durable authority IS their parameter set. The remaining nine are
|
|
37
|
+
* plain on/off grants. That distinction bounds what a grant CONVEYS; it does not
|
|
38
|
+
* narrow who may earn one, which is what `appScoped` decides. */
|
|
23
39
|
appScoped?: boolean;
|
|
24
40
|
/** Render this capability's consent line with the platform's **maximally-
|
|
25
41
|
* explicit** (scariest) styling, never bundled into a combined prompt
|
|
@@ -31,7 +47,39 @@ export interface CapabilityDef {
|
|
|
31
47
|
maximallyExplicit?: boolean;
|
|
32
48
|
}
|
|
33
49
|
export declare const CAPABILITIES: Record<Capability, CapabilityDef>;
|
|
34
|
-
/** The current registry/vocabulary version (§5.11). Bumped to 1.
|
|
50
|
+
/** The current registry/vocabulary version (§5.11). Bumped to 1.12.0 for a change that
|
|
51
|
+
* adds no NAME: `auth:identity`'s reclassification to `appScoped` (R3-407). A version
|
|
52
|
+
* is spent here for the same reason it is spent on a new row — the gate can only
|
|
53
|
+
* compare versions, so a change of MEANING that does not move `since` is a change the
|
|
54
|
+
* gate cannot see, and the resulting mismatch fails silently rather than loudly. The
|
|
55
|
+
* full reasoning is on the `auth:identity` row.
|
|
56
|
+
*
|
|
57
|
+
* Note that R3-407 originally landed the reclassification INSIDE 1.9.0, which was
|
|
58
|
+
* already published (0.1.14) with the pre-reclassification meaning — so 1.9.0 briefly
|
|
59
|
+
* named two different vocabularies, the exact thing the `feed:fetch` and
|
|
60
|
+
* `device:geolocation` notes below each refused to allow. 1.12.0 is that mistake
|
|
61
|
+
* undone, not a second one: 1.10.0 and 1.11.0 keep the vocabularies the docs already
|
|
62
|
+
* record for them.
|
|
63
|
+
*
|
|
64
|
+
* Prior notes — bumped to 1.11.0 with the elevated,
|
|
65
|
+
* app-scoped `device:camera` and `device:microphone` — the two CAPTURE devices
|
|
66
|
+
* (`BROWSER_CAPABILITIES_SPEC` §2/§3, R3-425). They share one version because they
|
|
67
|
+
* ship together and a host either has the capture broker + the host-chrome indicator
|
|
68
|
+
* or it has neither; a host on 1.10.0 refuses a binding that requests either (T26)
|
|
69
|
+
* rather than mounting with a camera that can never open. `device:clipboard` is NOT
|
|
70
|
+
* in this version — see the note above the table.
|
|
71
|
+
*
|
|
72
|
+
* Prior notes — bumped to 1.10.0 with the elevated,
|
|
73
|
+
* app-scoped `device:geolocation` — the first host-brokered `device:*` row
|
|
74
|
+
* (`BROWSER_CAPABILITIES_SPEC` §2–§4, R3-424). It takes its own version for the same
|
|
75
|
+
* reason `feed:fetch` did: 1.9.0 is already published (**0.1.14**, with
|
|
76
|
+
* `editor:reveal` — commit `8f7ac42`, which is the release that bumped the package to
|
|
77
|
+
* 0.1.14; 0.1.15 is this branch's own unpublished commit, NOT a release), and a
|
|
78
|
+
* registry version that does not identify a vocabulary is not much of a version
|
|
79
|
+
* gate. A host older than 1.10.0 refuses a binding that requests `device:geolocation`
|
|
80
|
+
* (T26) rather than mounting with the sensor silently inert.
|
|
81
|
+
*
|
|
82
|
+
* Prior notes — bumped to 1.8.0 with the elevated,
|
|
35
83
|
* app-scoped, host-parameterized `feed:fetch` (`CONNECTOR_EGRESS_FIXING_SPEC` §2 —
|
|
36
84
|
* R3-227), mirroring capabilities.json. (1.7.0 added the baseline state read
|
|
37
85
|
* `chrome:read`; 1.6.0 added `analytics:emit`; 1.5.0 added the first-party-only
|
|
@@ -45,7 +93,7 @@ export declare const CAPABILITIES: Record<Capability, CapabilityDef>;
|
|
|
45
93
|
* A host older than 1.8.0 therefore refuses a binding that requests `feed:fetch` (T26)
|
|
46
94
|
* rather than mounting half-working, which is the right outcome: a host that cannot
|
|
47
95
|
* enforce target-fixing must not run a connector that assumes it. */
|
|
48
|
-
export declare const REGISTRY_VERSION = "1.
|
|
96
|
+
export declare const REGISTRY_VERSION = "1.12.0";
|
|
49
97
|
/** Is `cap` a known host-core capability? (Closed vocabulary — §5.12.) */
|
|
50
98
|
export declare function isKnownCapability(cap: string): cap is Capability;
|
|
51
99
|
export declare function tierOf(cap: Capability): CapabilityTier;
|
package/dist/capabilities.js
CHANGED
|
@@ -22,7 +22,50 @@ exports.CAPABILITIES = {
|
|
|
22
22
|
'theme:read': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
23
23
|
'theme:set': { kind: 'action', tier: 'elevated', since: '1.0.0' },
|
|
24
24
|
'auth:status': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
25
|
-
|
|
25
|
+
// R3-407: elevated read, app-scoped — a stage app EARNS the user's login/avatar
|
|
26
|
+
// through the ordinary declared-capability consent path ('See your account'),
|
|
27
|
+
// recorded per (app, principal) like every durable grant. Never baseline:
|
|
28
|
+
// identity is asked for, not taken (an identity-by-default stage would be a
|
|
29
|
+
// tracking/attribution leak to arbitrary third-party code).
|
|
30
|
+
//
|
|
31
|
+
// **`since` is 1.12.0, not 1.0.0 — a RECLASSIFICATION takes a registry version,
|
|
32
|
+
// exactly as a new name does.** The row is old: an elevated, region-binding-only
|
|
33
|
+
// identity read has existed since 1.0.0. What R3-407 changed is `appScoped`, and
|
|
34
|
+
// that flag is not decoration — it IS the admission rule. `planPreAuthCapabilities`
|
|
35
|
+
// below and site-main's `resolveGrantedFrameCaps` both branch on `isAppScoped`, so
|
|
36
|
+
// one capability NAME means "mintable for a URL-loaded appKey" on one side of this
|
|
37
|
+
// change and "region-binding-only, drop it" on the other.
|
|
38
|
+
//
|
|
39
|
+
// Left at 1.0.0 the change was invisible to the §5.11/T26 version gate, and what
|
|
40
|
+
// that produced was a SILENT failure — the R3-233 validate-then-drop class, across
|
|
41
|
+
// a version boundary instead of a code boundary. A minter holding the new registry
|
|
42
|
+
// classifies `auth:identity` as app-scoped, mints a durable grant and answers
|
|
43
|
+
// `ok: true`; a consumer holding the old registry reads that grant back, finds
|
|
44
|
+
// `appScoped` falsy in ITS copy of this table, and drops the capability from the
|
|
45
|
+
// frame — while `planMissingPlainCaps` skips it on the same predicate, so it is not
|
|
46
|
+
// offered on the consent screen either. The user cannot grant it and nothing
|
|
47
|
+
// anywhere reports a problem.
|
|
48
|
+
//
|
|
49
|
+
// The `device:*` rows do NOT have this problem, and the difference is the whole
|
|
50
|
+
// point: they are new NAMES. An old host does not know the name, `isKnownCapability`
|
|
51
|
+
// is false, and every path refuses by name — loudly (T26, "update immediately.run").
|
|
52
|
+
// A known name whose FLAGS moved is refused nowhere, because no path compares flags
|
|
53
|
+
// across versions; only `since` is comparable across versions at all. Moving it is
|
|
54
|
+
// what puts the change back where the gate can see it.
|
|
55
|
+
//
|
|
56
|
+
// This takes nothing away from an older host: `since` is read out of the reader's
|
|
57
|
+
// OWN table, so a consumer pinned below 1.12.0 keeps exactly the row it always had,
|
|
58
|
+
// and no binding it can serve today starts being refused. What changes is that from
|
|
59
|
+
// 1.12.0 on the version NUMBER identifies which of the two meanings is in play —
|
|
60
|
+
// the same rule `feed:fetch` and `device:geolocation` each wrote down for a new
|
|
61
|
+
// name, applied to a change of meaning.
|
|
62
|
+
//
|
|
63
|
+
// `since` alone is necessary but not sufficient, because two of the three consumers
|
|
64
|
+
// of this flag never read `since`: `resolveGrantedFrameCaps` (site-main) and, until
|
|
65
|
+
// R3-407's follow-up, `planPreAuthCapabilities`. The M1 gate is now version-aware
|
|
66
|
+
// (see `m1PreAuth.ts`) so the mint path — the one that produces the orphan grant —
|
|
67
|
+
// refuses rather than mints. The frame read-back stays a defence-in-depth filter.
|
|
68
|
+
'auth:identity': { kind: 'read', tier: 'elevated', since: '1.12.0', appScoped: true },
|
|
26
69
|
'route:read': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
27
70
|
'formFactor:read': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
28
71
|
'mounts:read': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
@@ -74,6 +117,23 @@ exports.CAPABILITIES = {
|
|
|
74
117
|
// validates the path and drives Sandpack. Elevated — it moves the host's focus,
|
|
75
118
|
// so a previewed app must not hold it; only a consented/build-default binding.
|
|
76
119
|
'editor:open': { kind: 'action', tier: 'elevated', since: '1.0.0' },
|
|
120
|
+
// Bring the user TO the editor across an activity boundary (R3-389,
|
|
121
|
+
// TOOLS_ACTIVITY_SPEC §5.2). Strictly more than `editor:open`, which opens a file
|
|
122
|
+
// in whatever column the editor already occupies: an activity that owns the main
|
|
123
|
+
// pane UNMOUNTS the editor, so a surface there must be able to switch the active
|
|
124
|
+
// activity for its click to be visible at all.
|
|
125
|
+
//
|
|
126
|
+
// That is an attention move, so it is the parameterized ESCALATION of `open`
|
|
127
|
+
// (§8.4): `open({ reveal: true })` requires THIS instead of `editor:open`, and a
|
|
128
|
+
// frame holding only `editor:open` is refused rather than silently downgraded to a
|
|
129
|
+
// no-op reveal (T11).
|
|
130
|
+
//
|
|
131
|
+
// Elevated, and it buys no new REACH — the caller names a path, never an activity;
|
|
132
|
+
// the host resolves which activity owns the editor from its own state, reads its
|
|
133
|
+
// OWN `navigator.userActivation` (an app cannot forge it, and the gate fails closed
|
|
134
|
+
// without it), and rate-limits. The authority is "may ask", not "may decide where
|
|
135
|
+
// the user goes".
|
|
136
|
+
'editor:reveal': { kind: 'action', tier: 'elevated', since: '1.9.0' },
|
|
77
137
|
// Mutate the editor session's working tree — create/delete/rename/upload a file
|
|
78
138
|
// (migrate-sidebars Phase 04, EDITOR_AS_APP_SPEC §5.2). A NARROW, per-path gated
|
|
79
139
|
// action: the file explorer NAMES a path and the HOST performs the COW write
|
|
@@ -260,8 +320,142 @@ exports.CAPABILITIES = {
|
|
|
260
320
|
// platform behavior may depend on an app consuming this channel, so an app that
|
|
261
321
|
// never reads it is indistinguishable from one that does.
|
|
262
322
|
'chrome:read': { kind: 'read', tier: 'baseline', since: '1.7.0' },
|
|
323
|
+
// BROWSER_CAPABILITIES_SPEC §2–§4 (R3-424) — the first `device:*` row: the HOST
|
|
324
|
+
// calls `navigator.geolocation` at ITS OWN origin and hands the app coordinates.
|
|
325
|
+
// It exists because the blocker is the ORIGIN, not policy: an app frame is
|
|
326
|
+
// opaque-origin, browsers key permission grants on an origin, and
|
|
327
|
+
// `getCurrentPosition` inside the frame never prompts — it just times out
|
|
328
|
+
// (`code=3`). Widening the sandbox with `allow-same-origin` is off the table
|
|
329
|
+
// (UI_AS_APPS G1/T1), so this follows the `net:fetch` shape: the app never gets
|
|
330
|
+
// the browser handle, the host performs the privileged call and returns a
|
|
331
|
+
// serialized result.
|
|
332
|
+
//
|
|
333
|
+
// **kind: 'action', not 'read'** — deliberately, and the same call the
|
|
334
|
+
// `diagnostics:read` row made. `kind` names the ENFORCEMENT POINT, not the
|
|
335
|
+
// English verb (see this file's header): a `read` is gated by a `view()`
|
|
336
|
+
// projection on a channel the host is already maintaining (§8.3), an `action` is
|
|
337
|
+
// gated before a handler runs (§8.4). There is no standing "position" state to
|
|
338
|
+
// project — the value does not exist until the app asks the host to acquire it,
|
|
339
|
+
// which turns on a sensor and (on first use per device) raises the browser's own
|
|
340
|
+
// prompt. That is a host operation invoked on request, so §8.4 is the chokepoint
|
|
341
|
+
// and `action` is what routes it there.
|
|
342
|
+
//
|
|
343
|
+
// **Elevated + appScoped**: above the stage floor and within the stage ceiling —
|
|
344
|
+
// first use shows the powerbox consent naming the app and the device, the grant
|
|
345
|
+
// persists on `(app, principal)` and is revocable from the same surfaces as a
|
|
346
|
+
// space grant. Above the M3 ceiling: the M3 stance delegates nothing, so a
|
|
347
|
+
// stranger's app is refused with no prompt at all (G-DEV-2). NOT parameterized:
|
|
348
|
+
// the grant is a plain on/off, so it mints through the plain-cap path. A
|
|
349
|
+
// coarse/precise split would make it parameterized; that stays an open question
|
|
350
|
+
// in the spec rather than a shape guessed at here.
|
|
351
|
+
'device:geolocation': { kind: 'action', tier: 'elevated', since: '1.10.0', appScoped: true },
|
|
352
|
+
// BROWSER_CAPABILITIES_SPEC §2/§3 (R3-425) — the two CAPTURE devices. They take
|
|
353
|
+
// `device:geolocation`'s shape exactly (`action` / `elevated` / `appScoped`, plain
|
|
354
|
+
// on/off), for the same reasons written out on that row, so only what is DIFFERENT
|
|
355
|
+
// about them is recorded here.
|
|
356
|
+
//
|
|
357
|
+
// The origin blocker is harsher than geolocation's, not softer: inside the app
|
|
358
|
+
// frame `getUserMedia({video})`/`({audio})` does not merely fail to prompt, it
|
|
359
|
+
// throws `SecurityError: Invalid security origin` outright (§1, measured). And the
|
|
360
|
+
// richer result cannot be handed over either — a `MediaStreamTrack` is not
|
|
361
|
+
// transferable between windows (`DataCloneError`), while `ImageBitmap`,
|
|
362
|
+
// `ArrayBuffer`, `MessagePort` and `ReadableStream` all are. So the host opens the
|
|
363
|
+
// device at its own origin and hands the app BYTES or FRAMES, never a handle.
|
|
364
|
+
//
|
|
365
|
+
// TWO THINGS FOLLOW FROM "capture", and they are the whole reason these are not
|
|
366
|
+
// just more geolocation rows:
|
|
367
|
+
//
|
|
368
|
+
// 1. A capture has a LIVE SESSION with a duration a bystander can be caught in.
|
|
369
|
+
// While one is open the host shows a persistent indicator in its own chrome
|
|
370
|
+
// that the stage app cannot cover or remove (G-DEV-5) — the same rule that
|
|
371
|
+
// keeps sign-in in host chrome. A position read has no such session and needs
|
|
372
|
+
// no such indicator.
|
|
373
|
+
// 2. The DEFAULT delivery is a one-shot capture task drawn by the HOST
|
|
374
|
+
// (`capture-photo@1` / `capture-audio@1`): the user frames the shot, taps Done,
|
|
375
|
+
// and the app receives bytes. The app is never in the loop while the device is
|
|
376
|
+
// live, so "nothing is recorded when the user cancels" is a property of the
|
|
377
|
+
// mechanism rather than a promise the app keeps.
|
|
378
|
+
//
|
|
379
|
+
// ONE capability per device, NOT one per delivery grade. The grade (one-shot bytes
|
|
380
|
+
// vs. a live frame stream) is a property of the durable grant and the consent line
|
|
381
|
+
// the user reads — never a per-call knob the app picks — which is the same rule
|
|
382
|
+
// R3-424 wrote down for accuracy. Splitting the vocabulary by grade would mint a
|
|
383
|
+
// name per grade and force a re-consent to add one; keeping the grade off the row
|
|
384
|
+
// leaves the frame-stream grade additive.
|
|
385
|
+
//
|
|
386
|
+
// `device:clipboard` is deliberately NOT here — see the note under the table.
|
|
387
|
+
'device:camera': { kind: 'action', tier: 'elevated', since: '1.11.0', appScoped: true },
|
|
388
|
+
'device:microphone': { kind: 'action', tier: 'elevated', since: '1.11.0', appScoped: true },
|
|
263
389
|
};
|
|
264
|
-
|
|
390
|
+
// `device:clipboard` — proposed in BROWSER_CAPABILITIES_SPEC §2, DELIBERATELY LEFT
|
|
391
|
+
// OUT of the vocabulary by R3-425.
|
|
392
|
+
//
|
|
393
|
+
// Not an oversight and not "no time": the spec's own open question is *"writes may be
|
|
394
|
+
// safe at the stage floor (the sandbox already allows copy via `execCommand`); reads
|
|
395
|
+
// should stay consented."* That question is about the row's TIER, and a single
|
|
396
|
+
// `device:clipboard` row cannot hold two tiers. Shipping one now would answer the
|
|
397
|
+
// question by accident — the exact failure R3-424 avoided by keeping the
|
|
398
|
+
// coarse/precise location split out of the call params.
|
|
399
|
+
//
|
|
400
|
+
// The vocabulary is CLOSED and VERSIONED, which makes a name expensive: if the
|
|
401
|
+
// answer turns out to be "write is baseline, read is elevated", the shape is two
|
|
402
|
+
// rows (`device:clipboard-read` / `device:clipboard-write`) and the single name
|
|
403
|
+
// shipped today would have to be deprecated — a published name that no longer means
|
|
404
|
+
// anything, in a registry whose whole value is that a version identifies a
|
|
405
|
+
// vocabulary. Nothing is lost by waiting: no capability is required to decide, and
|
|
406
|
+
// adding a row is additive.
|
|
407
|
+
//
|
|
408
|
+
// It is also outside this item. R3-425 is camera and microphone; clipboard has no
|
|
409
|
+
// capture session, no host-drawn capture surface and nothing for the G-DEV-5
|
|
410
|
+
// indicator to indicate, so it would arrive with none of the machinery that makes
|
|
411
|
+
// the two rows above enforceable.
|
|
412
|
+
// WHICH PACKAGE VERSIONS ACTUALLY EXIST. `ci.yml` publishes the HEAD-OF-MAIN version
|
|
413
|
+
// only — one push to main, one `npm publish` of whatever `package.json` says at that
|
|
414
|
+
// commit. A branch that bumps the version several times therefore publishes exactly its
|
|
415
|
+
// LAST bump, and the intermediate numbers never reach npm.
|
|
416
|
+
//
|
|
417
|
+
// On this branch that makes **0.1.15 and 0.1.16 intermediate branch states that are
|
|
418
|
+
// never published**: 0.1.15 (`72c4217`, `auth:identity` app-scoped) and 0.1.16
|
|
419
|
+
// (`edd6c16`, `device:geolocation`) exist only as commits. npm goes 0.1.14 -> 0.1.17,
|
|
420
|
+
// and **0.1.17 is the release that carries all three changes**. Cite 0.1.17 as the
|
|
421
|
+
// version anything on this branch ships in — a consumer that pins 0.1.15 or 0.1.16
|
|
422
|
+
// fails `npm ci` with `ETARGET`, and this repo's own consumer (site-main) pins
|
|
423
|
+
// EXACTLY, so it would be the one to hit it. The bumps are left in history rather than
|
|
424
|
+
// collapsed; this note is what stops the next reader hunting for a release that is not
|
|
425
|
+
// there.
|
|
426
|
+
/** The current registry/vocabulary version (§5.11). Bumped to 1.12.0 for a change that
|
|
427
|
+
* adds no NAME: `auth:identity`'s reclassification to `appScoped` (R3-407). A version
|
|
428
|
+
* is spent here for the same reason it is spent on a new row — the gate can only
|
|
429
|
+
* compare versions, so a change of MEANING that does not move `since` is a change the
|
|
430
|
+
* gate cannot see, and the resulting mismatch fails silently rather than loudly. The
|
|
431
|
+
* full reasoning is on the `auth:identity` row.
|
|
432
|
+
*
|
|
433
|
+
* Note that R3-407 originally landed the reclassification INSIDE 1.9.0, which was
|
|
434
|
+
* already published (0.1.14) with the pre-reclassification meaning — so 1.9.0 briefly
|
|
435
|
+
* named two different vocabularies, the exact thing the `feed:fetch` and
|
|
436
|
+
* `device:geolocation` notes below each refused to allow. 1.12.0 is that mistake
|
|
437
|
+
* undone, not a second one: 1.10.0 and 1.11.0 keep the vocabularies the docs already
|
|
438
|
+
* record for them.
|
|
439
|
+
*
|
|
440
|
+
* Prior notes — bumped to 1.11.0 with the elevated,
|
|
441
|
+
* app-scoped `device:camera` and `device:microphone` — the two CAPTURE devices
|
|
442
|
+
* (`BROWSER_CAPABILITIES_SPEC` §2/§3, R3-425). They share one version because they
|
|
443
|
+
* ship together and a host either has the capture broker + the host-chrome indicator
|
|
444
|
+
* or it has neither; a host on 1.10.0 refuses a binding that requests either (T26)
|
|
445
|
+
* rather than mounting with a camera that can never open. `device:clipboard` is NOT
|
|
446
|
+
* in this version — see the note above the table.
|
|
447
|
+
*
|
|
448
|
+
* Prior notes — bumped to 1.10.0 with the elevated,
|
|
449
|
+
* app-scoped `device:geolocation` — the first host-brokered `device:*` row
|
|
450
|
+
* (`BROWSER_CAPABILITIES_SPEC` §2–§4, R3-424). It takes its own version for the same
|
|
451
|
+
* reason `feed:fetch` did: 1.9.0 is already published (**0.1.14**, with
|
|
452
|
+
* `editor:reveal` — commit `8f7ac42`, which is the release that bumped the package to
|
|
453
|
+
* 0.1.14; 0.1.15 is this branch's own unpublished commit, NOT a release), and a
|
|
454
|
+
* registry version that does not identify a vocabulary is not much of a version
|
|
455
|
+
* gate. A host older than 1.10.0 refuses a binding that requests `device:geolocation`
|
|
456
|
+
* (T26) rather than mounting with the sensor silently inert.
|
|
457
|
+
*
|
|
458
|
+
* Prior notes — bumped to 1.8.0 with the elevated,
|
|
265
459
|
* app-scoped, host-parameterized `feed:fetch` (`CONNECTOR_EGRESS_FIXING_SPEC` §2 —
|
|
266
460
|
* R3-227), mirroring capabilities.json. (1.7.0 added the baseline state read
|
|
267
461
|
* `chrome:read`; 1.6.0 added `analytics:emit`; 1.5.0 added the first-party-only
|
|
@@ -275,7 +469,7 @@ exports.CAPABILITIES = {
|
|
|
275
469
|
* A host older than 1.8.0 therefore refuses a binding that requests `feed:fetch` (T26)
|
|
276
470
|
* rather than mounting half-working, which is the right outcome: a host that cannot
|
|
277
471
|
* enforce target-fixing must not run a connector that assumes it. */
|
|
278
|
-
exports.REGISTRY_VERSION = '1.
|
|
472
|
+
exports.REGISTRY_VERSION = '1.12.0';
|
|
279
473
|
/** Is `cap` a known host-core capability? (Closed vocabulary — §5.12.) */
|
|
280
474
|
function isKnownCapability(cap) {
|
|
281
475
|
return Object.prototype.hasOwnProperty.call(exports.CAPABILITIES, cap);
|
package/dist/m1PreAuth.d.ts
CHANGED
|
@@ -5,7 +5,12 @@ export type PreAuthRefusalReason =
|
|
|
5
5
|
/** A non-app-scoped elevated cap — region-binding-only authority (§8.9). */
|
|
6
6
|
'broad-elevated'
|
|
7
7
|
/** Not in the closed capability vocabulary (§5.12) — fail-closed. */
|
|
8
|
-
| 'unknown'
|
|
8
|
+
| 'unknown'
|
|
9
|
+
/** Known here, but declared at a `since` NEWER than the host that will consume the
|
|
10
|
+
* grant, so that host cannot enforce it (§5.11 / T26). Minting it anyway is a
|
|
11
|
+
* validate-then-drop: the mint answers `ok` and the consumer silently discards the
|
|
12
|
+
* capability. Refusing is the loud alternative. */
|
|
13
|
+
| 'unsupported';
|
|
9
14
|
export interface PreAuthRefusal {
|
|
10
15
|
capability: string;
|
|
11
16
|
reason: PreAuthRefusalReason;
|
|
@@ -21,8 +26,27 @@ export interface PreAuthPlan {
|
|
|
21
26
|
/**
|
|
22
27
|
* The pure §8.9 target check: partition requested capability names into
|
|
23
28
|
* {grantable app-scoped, baseline no-op, refused}. Order-independent; total.
|
|
29
|
+
*
|
|
30
|
+
* `hostVersion` is the registry version of the host that will CONSUME the grant, and
|
|
31
|
+
* it defaults to this build's own `REGISTRY_VERSION` — so every existing caller keeps
|
|
32
|
+
* its exact behaviour (at that default, every known capability is supported). Pass it
|
|
33
|
+
* when the minter and the consumer can be on different vocabularies, which is the
|
|
34
|
+
* normal case: the backend floats its `preauth-core` range while site-main PINS one,
|
|
35
|
+
* so the minter routinely holds a newer table than the host that reads the grant back.
|
|
36
|
+
*
|
|
37
|
+
* Why the check belongs here and not only on `since`: a capability's `since` is read
|
|
38
|
+
* out of the reader's OWN table, so advancing it makes a reclassification VISIBLE to
|
|
39
|
+
* the gate but does not make the gate CONSULTED on this path — this function decided
|
|
40
|
+
* grantability from `isAppScoped` alone, with no version anywhere in it. That is how a
|
|
41
|
+
* reclassified capability (`auth:identity`, R3-407) could be minted for a host whose
|
|
42
|
+
* vocabulary still calls it region-binding-only, which then drops it from the frame and
|
|
43
|
+
* omits it from the consent screen with nothing reported: the R3-233 validate-then-drop
|
|
44
|
+
* failure, reached across a version boundary. The version check is placed before the
|
|
45
|
+
* tier branches, and so applies to baseline caps too, matching the registry merge —
|
|
46
|
+
* which likewise runs `unsupportedCapabilities` over a region's whole effective set
|
|
47
|
+
* regardless of tier, because "this host cannot enforce it" is prior to what tier it is.
|
|
24
48
|
*/
|
|
25
|
-
export declare function planPreAuthCapabilities(requested: readonly string[]): PreAuthPlan;
|
|
49
|
+
export declare function planPreAuthCapabilities(requested: readonly string[], hostVersion?: string): PreAuthPlan;
|
|
26
50
|
/** Safe to apply iff the §8.9 check refused nothing (fail-closed, all-or-nothing). */
|
|
27
51
|
export declare const isPreAuthClean: (plan: PreAuthPlan) => boolean;
|
|
28
52
|
/** What a policy/settings surface asks M1 to pre-authorize for `(uid, appKey)` —
|
|
@@ -59,7 +83,12 @@ export interface PreAuthResult {
|
|
|
59
83
|
* unbounded).
|
|
60
84
|
*
|
|
61
85
|
* Refusal is terminal and silent of side effects: when any requested capability
|
|
62
|
-
* is broad-elevated
|
|
63
|
-
* refusals — the caller surfaces them (the policy is
|
|
86
|
+
* is broad-elevated, unknown, or unsupported by the consuming host, the function
|
|
87
|
+
* mints NOTHING and returns the refusals — the caller surfaces them (the policy is
|
|
88
|
+
* malformed, over-broad, or aimed at a host too old to enforce what it asks for).
|
|
89
|
+
*
|
|
90
|
+
* `hostVersion` is forwarded to the target check: pass the registry version of the
|
|
91
|
+
* host that will consume these grants when it may differ from this build's. It
|
|
92
|
+
* defaults to this build's own, which preserves every existing caller's behaviour.
|
|
64
93
|
*/
|
|
65
|
-
export declare function applyPreAuth(store: MintStore, uid: string, appKey: string, request: PreAuthRequest, onError?: MintErrorSink): Promise<PreAuthResult>;
|
|
94
|
+
export declare function applyPreAuth(store: MintStore, uid: string, appKey: string, request: PreAuthRequest, onError?: MintErrorSink, hostVersion?: string): Promise<PreAuthResult>;
|
package/dist/m1PreAuth.js
CHANGED
|
@@ -17,13 +17,44 @@
|
|
|
17
17
|
// `mintPath:'policy'`) and cannot drift from M3.
|
|
18
18
|
//
|
|
19
19
|
// THE SECURITY INVARIANT — the §8.9 target check. A pre-auth for a URL-loaded
|
|
20
|
-
// `appKey` may only cover **app-scoped** elevated capabilities
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// `editor:
|
|
25
|
-
//
|
|
26
|
-
//
|
|
20
|
+
// `appKey` may only cover **app-scoped** elevated capabilities — the set an ordinary
|
|
21
|
+
// previewed/forked app can EARN per §8.9/§8.15 — plus mounts (app-scoped by
|
|
22
|
+
// construction). A **broad-elevated** capability — any non-app-scoped elevated cap
|
|
23
|
+
// (`spaces:user`/`spaces:admin`, `editor:write`, `contribute:direct`/`contribute:any`,
|
|
24
|
+
// `editor:open`, …) — is REFUSED: M1 cannot mint it for a URL-loaded appKey. Unknown
|
|
25
|
+
// capabilities are refused (fail-closed), as are capabilities the CONSUMING host's
|
|
26
|
+
// registry version is too old to enforce. Baseline capabilities need no grant and are
|
|
27
|
+
// dropped.
|
|
28
|
+
//
|
|
29
|
+
// WHAT THAT SET ACTUALLY CONTAINS, stated in full because this is the boundary M1
|
|
30
|
+
// pre-grants across WITHOUT A PROMPT. The check reads `isAppScoped`, so the list is
|
|
31
|
+
// derived from the capability table and cannot drift from it; as of registry 1.12.0
|
|
32
|
+
// the eleven app-scoped rows are:
|
|
33
|
+
//
|
|
34
|
+
// `auth:identity` (R3-407) · `contribute:self` · `task:invoke` · `net:fetch` ·
|
|
35
|
+
// `feed:fetch` (R3-227) · `diagnostics:read` · `llm:chat` · `analytics:emit`
|
|
36
|
+
// (R3-350) · `device:geolocation` (R3-424) · `device:camera` and
|
|
37
|
+
// `device:microphone` (R3-425).
|
|
38
|
+
//
|
|
39
|
+
// So M1 can pre-grant, with no consent modal at boot, not only the original egress and
|
|
40
|
+
// delegation caps but the user's IDENTITY, their LOCATION, and their CAMERA and
|
|
41
|
+
// MICROPHONE. That is the shipped invariant and it is deliberate — a policy/settings
|
|
42
|
+
// write path is an operator- or user-tier decision recorded ahead of time, and the
|
|
43
|
+
// same §8.15 grant, expiry and revocation apply as if the modal had drawn it. It is
|
|
44
|
+
// stated here so the boundary is read rather than inferred from a stale example list.
|
|
45
|
+
//
|
|
46
|
+
// Not overstating it, in three directions:
|
|
47
|
+
// - `net:fetch` and `feed:fetch` are HOST-PARAMETERIZED, so a pre-auth conveys their
|
|
48
|
+
// parameter set (hosts / compiled templates), never a bare "may fetch". The other
|
|
49
|
+
// nine are plain on/off grants. `applyPreAuth` filters the parameterized ones out
|
|
50
|
+
// of the plain-cap mint below.
|
|
51
|
+
// - M1 clamps WHO may hold a capability, not what the capability then does. A
|
|
52
|
+
// pre-granted `device:camera` still opens the device through the host's own capture
|
|
53
|
+
// surface, under the host-chrome indicator the app cannot cover (G-DEV-5); it does
|
|
54
|
+
// not hand the app a device handle.
|
|
55
|
+
// - Pre-auth is not the M3 stance. A stranger's app under M3 is refused these with no
|
|
56
|
+
// prompt at all (G-DEV-2); M1 is the path where an operator or the user has already
|
|
57
|
+
// decided, not a way around that refusal.
|
|
27
58
|
//
|
|
28
59
|
// The check is **all-or-nothing**: if a policy names ANY refused capability the
|
|
29
60
|
// whole pre-auth is rejected and NOTHING is minted — a partial apply would
|
|
@@ -40,8 +71,27 @@ const bootConsent_1 = require("./bootConsent");
|
|
|
40
71
|
/**
|
|
41
72
|
* The pure §8.9 target check: partition requested capability names into
|
|
42
73
|
* {grantable app-scoped, baseline no-op, refused}. Order-independent; total.
|
|
74
|
+
*
|
|
75
|
+
* `hostVersion` is the registry version of the host that will CONSUME the grant, and
|
|
76
|
+
* it defaults to this build's own `REGISTRY_VERSION` — so every existing caller keeps
|
|
77
|
+
* its exact behaviour (at that default, every known capability is supported). Pass it
|
|
78
|
+
* when the minter and the consumer can be on different vocabularies, which is the
|
|
79
|
+
* normal case: the backend floats its `preauth-core` range while site-main PINS one,
|
|
80
|
+
* so the minter routinely holds a newer table than the host that reads the grant back.
|
|
81
|
+
*
|
|
82
|
+
* Why the check belongs here and not only on `since`: a capability's `since` is read
|
|
83
|
+
* out of the reader's OWN table, so advancing it makes a reclassification VISIBLE to
|
|
84
|
+
* the gate but does not make the gate CONSULTED on this path — this function decided
|
|
85
|
+
* grantability from `isAppScoped` alone, with no version anywhere in it. That is how a
|
|
86
|
+
* reclassified capability (`auth:identity`, R3-407) could be minted for a host whose
|
|
87
|
+
* vocabulary still calls it region-binding-only, which then drops it from the frame and
|
|
88
|
+
* omits it from the consent screen with nothing reported: the R3-233 validate-then-drop
|
|
89
|
+
* failure, reached across a version boundary. The version check is placed before the
|
|
90
|
+
* tier branches, and so applies to baseline caps too, matching the registry merge —
|
|
91
|
+
* which likewise runs `unsupportedCapabilities` over a region's whole effective set
|
|
92
|
+
* regardless of tier, because "this host cannot enforce it" is prior to what tier it is.
|
|
43
93
|
*/
|
|
44
|
-
function planPreAuthCapabilities(requested) {
|
|
94
|
+
function planPreAuthCapabilities(requested, hostVersion = capabilities_1.REGISTRY_VERSION) {
|
|
45
95
|
const grantable = [];
|
|
46
96
|
const baseline = [];
|
|
47
97
|
const refused = [];
|
|
@@ -50,6 +100,12 @@ function planPreAuthCapabilities(requested) {
|
|
|
50
100
|
refused.push({ capability: cap, reason: 'unknown' });
|
|
51
101
|
continue;
|
|
52
102
|
}
|
|
103
|
+
// Known to THIS registry but not to the consuming host's (§5.11/T26). Refuse
|
|
104
|
+
// rather than mint a grant that host will silently discard.
|
|
105
|
+
if (!(0, capabilities_1.isSupportedCapability)(cap, hostVersion)) {
|
|
106
|
+
refused.push({ capability: cap, reason: 'unsupported' });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
53
109
|
if ((0, capabilities_1.isBaseline)(cap)) {
|
|
54
110
|
baseline.push(cap);
|
|
55
111
|
continue;
|
|
@@ -82,11 +138,16 @@ exports.isPreAuthClean = isPreAuthClean;
|
|
|
82
138
|
* unbounded).
|
|
83
139
|
*
|
|
84
140
|
* Refusal is terminal and silent of side effects: when any requested capability
|
|
85
|
-
* is broad-elevated
|
|
86
|
-
* refusals — the caller surfaces them (the policy is
|
|
141
|
+
* is broad-elevated, unknown, or unsupported by the consuming host, the function
|
|
142
|
+
* mints NOTHING and returns the refusals — the caller surfaces them (the policy is
|
|
143
|
+
* malformed, over-broad, or aimed at a host too old to enforce what it asks for).
|
|
144
|
+
*
|
|
145
|
+
* `hostVersion` is forwarded to the target check: pass the registry version of the
|
|
146
|
+
* host that will consume these grants when it may differ from this build's. It
|
|
147
|
+
* defaults to this build's own, which preserves every existing caller's behaviour.
|
|
87
148
|
*/
|
|
88
|
-
async function applyPreAuth(store, uid, appKey, request, onError) {
|
|
89
|
-
const plan = planPreAuthCapabilities(request.capabilities);
|
|
149
|
+
async function applyPreAuth(store, uid, appKey, request, onError, hostVersion) {
|
|
150
|
+
const plan = planPreAuthCapabilities(request.capabilities, hostVersion);
|
|
90
151
|
if (!(0, exports.isPreAuthClean)(plan)) {
|
|
91
152
|
return { ok: false, refused: plan.refused };
|
|
92
153
|
}
|
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.17",
|
|
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": {
|