@immediately-run/preauth-core 0.1.0
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 +41 -0
- package/dist/bootConsent.d.ts +42 -0
- package/dist/bootConsent.js +67 -0
- package/dist/capabilities.d.ts +57 -0
- package/dist/capabilities.js +217 -0
- package/dist/docLayout.d.ts +57 -0
- package/dist/docLayout.js +156 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +29 -0
- package/dist/m1PreAuth.d.ts +56 -0
- package/dist/m1PreAuth.js +80 -0
- package/dist/port.d.ts +84 -0
- package/dist/port.js +13 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @immediately-run/preauth-core
|
|
2
|
+
|
|
3
|
+
The shared **pre-authorization core** for immediately.run. It exists to make one
|
|
4
|
+
sentence a structural fact:
|
|
5
|
+
|
|
6
|
+
> Every surface that pre-authorizes or mints a durable capability grant —
|
|
7
|
+
> site-main's M3 consent screen, its M1 settings pre-auth, the backend
|
|
8
|
+
> `POST /preauth` executor, and the `immediately-run preauth` CLI — drives the
|
|
9
|
+
> **same** §8.9 target check and the **same** mint path, writing **byte-identical**
|
|
10
|
+
> Firestore documents. Not a second copy of any of them.
|
|
11
|
+
|
|
12
|
+
Spec: `UI_AS_APPS_SPEC.md` §8.9 (target check), §8.15 (M1 pre-authorization),
|
|
13
|
+
§8.6/§8.7 (the durable grant set). Plan: `docs/plans/cli-preauth-shared-core.md`.
|
|
14
|
+
|
|
15
|
+
## What's in here (pure TS, zero runtime deps, no React, no Firebase)
|
|
16
|
+
|
|
17
|
+
| Module | Surface |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `capabilities` | The capability vocabulary — the **single source of truth** (`CAPABILITIES`, `isAppScoped` / `isBaseline` / `isKnownCapability`, the version gate). site-main re-exports it; the backend imports the same predicates. The §8.9 gate's correctness IS this classification, so there is exactly one. |
|
|
20
|
+
| `port` | `MintStore` — the 3-method persistence port `mintConsentedGrants` calls (`createSpace`, `grantSpaceToApp`, `grantNetFetchHosts`) + its param/domain types (`GrantMode`, `MintPath`, `NetFetchHost`, …). |
|
|
21
|
+
| `docLayout` | The byte-faithful Firestore **paths + field builders** (`grantKey`, `GRANT_EXPIRY_MS`, the grant/space/net-fetch document builders). Each store adapter injects only its SDK's timestamp/increment sentinels and does the raw `.set()`; drift is impossible without editing a helper both consume. |
|
|
22
|
+
| `bootConsent` | `mintConsentedGrants` — the ONE mint path. Environment-neutral: a caller passes `onError` instead of the core logging with a host-specific prefix. |
|
|
23
|
+
| `m1PreAuth` | `planPreAuthCapabilities` / `isPreAuthClean` (the pure §8.9 target check) + `applyPreAuth` (validate-then-mint, all-or-nothing). |
|
|
24
|
+
|
|
25
|
+
## Consuming it
|
|
26
|
+
|
|
27
|
+
Via the **`file:` sibling pattern** site-main already uses for the sandpack fork:
|
|
28
|
+
|
|
29
|
+
```jsonc
|
|
30
|
+
// consumer package.json
|
|
31
|
+
"@immediately-run/preauth-core": "file:../immediately-run-preauth-core"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The built `dist/` is committed so consumers resolve the package without a separate
|
|
35
|
+
build step. After editing `src/`, rebuild:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run build # tsc -> dist/ (JS + .d.ts)
|
|
39
|
+
npm test # jest — the §8.9 gate, the hostile-policy property, the wire layout
|
|
40
|
+
npm run lint
|
|
41
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { GrantMode, MintPath, MintStore, NetFetchHost } from './port';
|
|
2
|
+
/** A consent-screen selection (structurally `MountConsentSelection` from
|
|
3
|
+
* site-main's editor layer; redefined here so this layer stays UI-free). */
|
|
4
|
+
export type ConsentSelection = {
|
|
5
|
+
uri: string;
|
|
6
|
+
mode: GrantMode;
|
|
7
|
+
kind: 'pick';
|
|
8
|
+
spaceId: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
} | {
|
|
11
|
+
uri: string;
|
|
12
|
+
mode: GrantMode;
|
|
13
|
+
kind: 'create';
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
16
|
+
export interface MintResult {
|
|
17
|
+
/** False if ANY grant failed to mint — the caller treats the start as failed. */
|
|
18
|
+
ok: boolean;
|
|
19
|
+
/** Whether the net:fetch host grant succeeded (vacuously true when none was
|
|
20
|
+
* requested) — the post-boot caller lifts the frame cap on this alone, even
|
|
21
|
+
* if a mount selection failed (matching its historical behavior). */
|
|
22
|
+
netFetchOk: boolean;
|
|
23
|
+
/** Successfully minted per-selection space ids (for post-boot provisioning). */
|
|
24
|
+
minted: {
|
|
25
|
+
selection: ConsentSelection;
|
|
26
|
+
spaceId: string;
|
|
27
|
+
}[];
|
|
28
|
+
}
|
|
29
|
+
/** Observe a per-item mint failure (logging only — authority is unaffected). The
|
|
30
|
+
* `ctx` is a stable English phrase; the host decides how/whether to log it. */
|
|
31
|
+
export type MintErrorSink = (ctx: string, err: unknown) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Turn an Allow choice into durable grants: net:fetch hosts (all-or-nothing —
|
|
34
|
+
* the screen showed exactly these), then per mount selection create-or-bind the
|
|
35
|
+
* space to its slot and record the §8.7 grant. Never throws; per-item failures
|
|
36
|
+
* are surfaced through `ok`/`netFetchOk` (and `onError`, if provided).
|
|
37
|
+
*
|
|
38
|
+
* The durable §8.7 grant IS the binding now (no separate slot): `declaredUri`
|
|
39
|
+
* records which §11.4 declared mount it satisfies, so a later boot re-provisions
|
|
40
|
+
* it without re-consent.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mintConsentedGrants(store: MintStore, uid: string, appKey: string, selections: readonly ConsentSelection[], netFetchHosts: readonly NetFetchHost[], mintPath?: MintPath, onError?: MintErrorSink): Promise<MintResult>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The ONE grant-mint path (UI_AS_APPS_SPEC §8.15). `mintConsentedGrants` turns a
|
|
3
|
+
// consent choice into durable §8.7/§8.15 grants. It is shared by EVERY surface
|
|
4
|
+
// that records a grant so they cannot drift:
|
|
5
|
+
// - site-main's before-boot gate + the post-boot fallback (SandboxListener) at
|
|
6
|
+
// `interactive` (M3) provenance,
|
|
7
|
+
// - the M1 pre-auth write path (`applyPreAuth`) at `policy` provenance, in both
|
|
8
|
+
// the browser (settings UI) and the backend `POST /preauth` executor.
|
|
9
|
+
//
|
|
10
|
+
// `mintPath` only stamps the §8.7 grant's PROVENANCE (the §8.11 audit label +
|
|
11
|
+
// revoke-cascade key); it does not change WHAT is minted — the §8.9 target check
|
|
12
|
+
// that bounds M1 lives in its caller (`m1PreAuth.ts`).
|
|
13
|
+
//
|
|
14
|
+
// Pure orchestration over the narrow `MintStore` port (3 methods). No React, no
|
|
15
|
+
// Firebase, no environment-specific logging: a caller passes `onError` to observe
|
|
16
|
+
// per-item failures (site-main wraps it with its `[Main-iframe]` logger; the
|
|
17
|
+
// backend logs with its own prefix), so this module never masquerades as a
|
|
18
|
+
// particular runtime.
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.mintConsentedGrants = mintConsentedGrants;
|
|
21
|
+
/**
|
|
22
|
+
* Turn an Allow choice into durable grants: net:fetch hosts (all-or-nothing —
|
|
23
|
+
* the screen showed exactly these), then per mount selection create-or-bind the
|
|
24
|
+
* space to its slot and record the §8.7 grant. Never throws; per-item failures
|
|
25
|
+
* are surfaced through `ok`/`netFetchOk` (and `onError`, if provided).
|
|
26
|
+
*
|
|
27
|
+
* The durable §8.7 grant IS the binding now (no separate slot): `declaredUri`
|
|
28
|
+
* records which §11.4 declared mount it satisfies, so a later boot re-provisions
|
|
29
|
+
* it without re-consent.
|
|
30
|
+
*/
|
|
31
|
+
async function mintConsentedGrants(store, uid, appKey, selections, netFetchHosts, mintPath = 'interactive', onError) {
|
|
32
|
+
let ok = true;
|
|
33
|
+
let netFetchOk = true;
|
|
34
|
+
if (netFetchHosts.length > 0) {
|
|
35
|
+
try {
|
|
36
|
+
await store.grantNetFetchHosts({ uid, appKey, hosts: netFetchHosts });
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
onError?.('net:fetch grant failed', err);
|
|
40
|
+
ok = false;
|
|
41
|
+
netFetchOk = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const minted = [];
|
|
45
|
+
for (const sel of selections) {
|
|
46
|
+
try {
|
|
47
|
+
const spaceId = sel.kind === 'create'
|
|
48
|
+
? await store.createSpace({ owner: uid, name: sel.name, appKey })
|
|
49
|
+
: sel.spaceId;
|
|
50
|
+
await store.grantSpaceToApp({
|
|
51
|
+
uid,
|
|
52
|
+
appKey,
|
|
53
|
+
spaceId,
|
|
54
|
+
name: sel.name,
|
|
55
|
+
mode: sel.mode,
|
|
56
|
+
declaredUri: sel.uri,
|
|
57
|
+
mintPath,
|
|
58
|
+
});
|
|
59
|
+
minted.push({ selection: sel, spaceId });
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
onError?.('consent grant minting failed', err);
|
|
63
|
+
ok = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { ok, netFetchOk, minted };
|
|
67
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type CapabilityKind = 'read' | 'action';
|
|
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' | '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';
|
|
4
|
+
export interface CapabilityDef {
|
|
5
|
+
kind: CapabilityKind;
|
|
6
|
+
tier: CapabilityTier;
|
|
7
|
+
/** Lowest platform/registry version that knows this capability (§5.11). */
|
|
8
|
+
since: string;
|
|
9
|
+
/** Carries a bounded argument set (host gate checks verb AND argument). */
|
|
10
|
+
parameterized?: boolean;
|
|
11
|
+
/** **App-scoped** consent-path annotation on the ELEVATED tier (NOT a fourth
|
|
12
|
+
* tier — CAPABILITY_REFERENCE §"How to read this", §6a CR-1). An app-scoped
|
|
13
|
+
* elevated capability can be EARNED by a URL-loaded/previewed app via lazy
|
|
14
|
+
* first-use or manifest-`requests` consent and recorded as a per-`(user,
|
|
15
|
+
* appKey)` grant; non-app-scoped elevated caps are never earnable that way
|
|
16
|
+
* (region binding only). The app-scoped set is `net:fetch`, `task:invoke`, and
|
|
17
|
+
* `contribute:self` (decision #1 — its baseline→elevated reclassification landed
|
|
18
|
+
* in R3-33d; the durable grant participates in the §8.15 90-day expiry like any
|
|
19
|
+
* app-scoped grant). */
|
|
20
|
+
appScoped?: boolean;
|
|
21
|
+
/** Render this capability's consent line with the platform's **maximally-
|
|
22
|
+
* explicit** (scariest) styling, never bundled into a combined prompt
|
|
23
|
+
* (decision #2). The most dangerous writes carry it: `contribute:direct`
|
|
24
|
+
* (commit without review) and `editor:write` (mutate the working tree).
|
|
25
|
+
* Independent of tier — it governs HOW the line is shown, not WHO may hold the
|
|
26
|
+
* capability (a first-party-only cap is still refused to a fork regardless). */
|
|
27
|
+
maximallyExplicit?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare const CAPABILITIES: Record<Capability, CapabilityDef>;
|
|
30
|
+
/** The current registry/vocabulary version (§5.11). Bumped to 1.2.0 with the
|
|
31
|
+
* per-user settings-space capabilities (`settings:app`/`settings:fork`/
|
|
32
|
+
* `settings:all`), mirroring capabilities.json. */
|
|
33
|
+
export declare const REGISTRY_VERSION = "1.2.0";
|
|
34
|
+
/** Is `cap` a known kernel capability? (Closed vocabulary — §5.12.) */
|
|
35
|
+
export declare function isKnownCapability(cap: string): cap is Capability;
|
|
36
|
+
export declare function tierOf(cap: Capability): CapabilityTier;
|
|
37
|
+
/** Baseline = what the previewed app and any unconsented binding may hold. */
|
|
38
|
+
export declare const BASELINE_CAPABILITIES: readonly Capability[];
|
|
39
|
+
export declare function isBaseline(cap: Capability): boolean;
|
|
40
|
+
/** App-scoped consentables — the elevated caps a previewed/forked app may EARN
|
|
41
|
+
* via lazy first-use or manifest-`requests` consent (§8.9/§8.15), as opposed to
|
|
42
|
+
* region-binding-only elevated caps. (`contribute:self` joined this set in R3-33d
|
|
43
|
+
* per decision #1.) */
|
|
44
|
+
export declare const APP_SCOPED_CAPABILITIES: readonly Capability[];
|
|
45
|
+
export declare function isAppScoped(cap: Capability): boolean;
|
|
46
|
+
/** Compare dotted numeric versions: <0 if a<b, 0 if equal, >0 if a>b. Missing
|
|
47
|
+
* segments are treated as 0 ("1.2" === "1.2.0"); non-numeric segments as 0. */
|
|
48
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
49
|
+
/** A capability is supported iff it is known AND its `since` ≤ the host version. */
|
|
50
|
+
export declare function isSupportedCapability(cap: string, hostVersion?: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The subset of `caps` this host cannot enforce — unknown to its vocabulary, or
|
|
53
|
+
* declared at a `since` newer than `hostVersion`. A non-empty result means the
|
|
54
|
+
* region must refuse to mount with "update immediately.run (missing: …)" (T26).
|
|
55
|
+
* `hostVersion` is injectable so an older host can be simulated in tests.
|
|
56
|
+
*/
|
|
57
|
+
export declare function unsupportedCapabilities(caps: readonly string[], hostVersion?: string): string[];
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The capability-definition registry — the KERNEL source of truth
|
|
3
|
+
// (UI_AS_APPS_SPEC §5.11 / §8.2). Capabilities are a CLOSED vocabulary: apps
|
|
4
|
+
// cannot mint one, only be granted one. This module mirrors docs/capabilities.json
|
|
5
|
+
// (the machine-readable companion); the host build is authoritative.
|
|
6
|
+
//
|
|
7
|
+
// Two enforcement points consume this (later slices): reads are gated per-grant
|
|
8
|
+
// with a view() projection on a channel (§8.3); actions are gated before the
|
|
9
|
+
// handler (§8.4). Parameterized capabilities additionally bound an argument set.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.APP_SCOPED_CAPABILITIES = exports.BASELINE_CAPABILITIES = exports.REGISTRY_VERSION = exports.CAPABILITIES = void 0;
|
|
12
|
+
exports.isKnownCapability = isKnownCapability;
|
|
13
|
+
exports.tierOf = tierOf;
|
|
14
|
+
exports.isBaseline = isBaseline;
|
|
15
|
+
exports.isAppScoped = isAppScoped;
|
|
16
|
+
exports.compareVersions = compareVersions;
|
|
17
|
+
exports.isSupportedCapability = isSupportedCapability;
|
|
18
|
+
exports.unsupportedCapabilities = unsupportedCapabilities;
|
|
19
|
+
exports.CAPABILITIES = {
|
|
20
|
+
'theme:read': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
21
|
+
'theme:set': { kind: 'action', tier: 'elevated', since: '1.0.0' },
|
|
22
|
+
'auth:status': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
23
|
+
'auth:identity': { kind: 'read', tier: 'elevated', since: '1.0.0' },
|
|
24
|
+
'route:read': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
25
|
+
'formFactor:read': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
26
|
+
'mounts:read': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
27
|
+
'spaces:app': { kind: 'action', tier: 'baseline', since: '1.0.0', parameterized: true },
|
|
28
|
+
'spaces:user': { kind: 'action', tier: 'elevated', since: '1.0.0' },
|
|
29
|
+
'spaces:admin': { kind: 'action', tier: 'elevated', since: '1.0.0' },
|
|
30
|
+
// Per-user settings space (UI_AS_APPS_SPEC §3.3/§3.5/§8.2; settings-space plan).
|
|
31
|
+
// The app's OWN `~/.config`-style subdir, auto-provisioned + chroot'd by appKey.
|
|
32
|
+
// Baseline: every app may open its own config with no consent (the host derives
|
|
33
|
+
// the appKey from the caller, so a different `settings:` locator can't be named).
|
|
34
|
+
'settings:app': { kind: 'action', tier: 'baseline', since: '1.2.0' },
|
|
35
|
+
// One-time SEED of the declared `forkOf` parent's settings into the app's own
|
|
36
|
+
// subdir (§3.4 lineage). Baseline action — the target is locked to the manifest
|
|
37
|
+
// `forkOf`, so it can never name another app — but the HANDLER gates each call on
|
|
38
|
+
// a user confirm (full explicit consent when cross-owner, a light confirm when
|
|
39
|
+
// the same owner published both apps). The consent is a per-action prompt, not a
|
|
40
|
+
// durable §8.15 grant (the copy lands in the app's own dir; nothing to revoke).
|
|
41
|
+
'settings:fork': { kind: 'action', tier: 'baseline', since: '1.2.0' },
|
|
42
|
+
// Mount ANY app's settings subdir / enumerate the whole `settings-store/{uid}`
|
|
43
|
+
// tree — the filesystem-manager ("file commander") surface. Permanently
|
|
44
|
+
// first-party-only: cross-app config is an activity oracle (like a future
|
|
45
|
+
// `mounts:registry`), so a fork/preview can never hold it.
|
|
46
|
+
'settings:all': { kind: 'action', tier: 'first-party-only', since: '1.2.0' },
|
|
47
|
+
'contribute:self': { kind: 'action', tier: 'elevated', since: '1.0.0', appScoped: true },
|
|
48
|
+
'contribute:any': { kind: 'action', tier: 'elevated', since: '1.0.0', parameterized: true },
|
|
49
|
+
// Decision #2 (R3-33d, landed): contribute:direct is the platform's scariest
|
|
50
|
+
// write, rendered maximally-explicit. The tier is now **elevated/consentable**
|
|
51
|
+
// (no longer first-party-only) so source-control panels stay forkable (value 4) —
|
|
52
|
+
// a fork CAN hold it, but only behind the distinct scary consent line, never
|
|
53
|
+
// bundled. `maximallyExplicit` (not the tier) is what keeps the line scary.
|
|
54
|
+
'contribute:direct': { kind: 'action', tier: 'elevated', since: '1.0.0', parameterized: true, maximallyExplicit: true },
|
|
55
|
+
'editor:read': { kind: 'read', tier: 'elevated', since: '1.0.0' },
|
|
56
|
+
// Ask the host to open a working-tree file in the CodeMirror editor (§4 — the
|
|
57
|
+
// file explorer's click-to-open). An INTENT, not editor ownership (§2): the host
|
|
58
|
+
// validates the path and drives Sandpack. Elevated — it moves the host's focus,
|
|
59
|
+
// so a previewed app must not hold it; only a consented/build-default binding.
|
|
60
|
+
'editor:open': { kind: 'action', tier: 'elevated', since: '1.0.0' },
|
|
61
|
+
// Mutate the editor session's working tree — create/delete/rename/upload a file
|
|
62
|
+
// (migrate-sidebars Phase 04, EDITOR_AS_APP_SPEC §5.2). A NARROW, per-path gated
|
|
63
|
+
// action: the file explorer NAMES a path and the HOST performs the COW write
|
|
64
|
+
// (and notifies Sandpack) — the COW/journal stays in the kernel (§2/§4). The
|
|
65
|
+
// explorer holds no working-tree write PORT (that broad authority is the
|
|
66
|
+
// editor app's `editor:document`); it must ask. **Elevated, not first-party-only**
|
|
67
|
+
// (EDITOR_AS_APP_SPEC §5.1 forkability rule): mutating the user's OWN working copy
|
|
68
|
+
// does not escape the user's session (no push, no host token, no cross-user reach —
|
|
69
|
+
// saves stay separately gated, the diff is host-computed), so it is a consentable
|
|
70
|
+
// grant a user may extend to a fork of the file explorer. `first-party-only` is
|
|
71
|
+
// reserved for session-ESCAPING authority (e.g. `contribute:direct`).
|
|
72
|
+
// Decision #2: editor:write is consentable (elevated, forkable — mutating the
|
|
73
|
+
// user's OWN working copy doesn't escape their session), but behind the
|
|
74
|
+
// maximally-explicit scary line.
|
|
75
|
+
'editor:write': { kind: 'action', tier: 'elevated', since: '1.0.0', maximallyExplicit: true },
|
|
76
|
+
// The editor APP's own session-management capability (EDITOR_AS_APP_SPEC §5.1;
|
|
77
|
+
// editor-as-app plan Phase 03). Gates the `protocol-editor close`/`setActive`
|
|
78
|
+
// intents — mutating the editor's OWN open-tab set + active file, which only the
|
|
79
|
+
// bound editor should drive (NOT the file explorer, which holds `editor:open` to
|
|
80
|
+
// *ask* the host to open a file, a distinct cross-app intent). Elevated and
|
|
81
|
+
// forkable via consent: managing the user's own editor view does not escape their
|
|
82
|
+
// session (no push, no token, no cross-user reach), so a user may extend it to a
|
|
83
|
+
// forked editor. It will additionally gate the editor app's session/diagnostics
|
|
84
|
+
// channels as those land (plan Phase 02). NOT the rw working-tree port — that is
|
|
85
|
+
// the region property `exposesWorkingTree:'rw'`, not this capability.
|
|
86
|
+
'editor:document': { kind: 'action', tier: 'elevated', since: '1.1.0' },
|
|
87
|
+
// Enter the EDIT EXPERIENCE for the running app — the present→edit transition
|
|
88
|
+
// (`/present/...` → `/edit/...`) an app cannot make itself (EDITOR_FIRST_EDITING_SPEC
|
|
89
|
+
// §6 Delta A). An INTENT (§2): the host performs the visible, user-observable
|
|
90
|
+
// navigation and draws all editor chrome; the app never navigates or paints chrome.
|
|
91
|
+
//
|
|
92
|
+
// Tier — BASELINE, deliberately, and only safe because the transition is STRICTLY
|
|
93
|
+
// self-scoped: the HANDLER reads only a same-repo, traversal-free `path` and
|
|
94
|
+
// navigates within the CURRENT route (it cannot be pointed at another repo — the
|
|
95
|
+
// gate validates the `path` shape, T4). Unlike `editor:open` (elevated — it can
|
|
96
|
+
// yank focus to an ARBITRARY file from a hostile preview), entering edit on the
|
|
97
|
+
// repo the user is ALREADY viewing is no escalation: edit mode adds host-drawn
|
|
98
|
+
// chrome around the same sandboxed app, granting it NO new capability, and "view
|
|
99
|
+
// your own source" is already the §0 promise. Baseline is also the ONLY tier that
|
|
100
|
+
// lets a standalone present-mode app (which holds only baseline) offer an "edit
|
|
101
|
+
// this" affordance instead of shipping a bespoke editor — the whole point of the
|
|
102
|
+
// delta. **PROPOSED tier, pending capability-owner sign-off (EDITOR_FIRST_EDITING_
|
|
103
|
+
// SPEC §8 open question #1):** flipping to elevated is a one-token change here.
|
|
104
|
+
'editor:requestEdit': { kind: 'action', tier: 'baseline', since: '1.1.0' },
|
|
105
|
+
// Source-control state read surface (migrate-sidebars Phase 05) — the diff
|
|
106
|
+
// summary + branch info + open-PR list the `panel.contribute` app needs to reach
|
|
107
|
+
// parity with the native `SourceControlPanel`. Elevated: it exposes the repo's
|
|
108
|
+
// branch/PR/diff state (no token ever crosses — derived host-side), so a
|
|
109
|
+
// baseline/previewed frame gets an empty `VcsState`, never a leak.
|
|
110
|
+
'vcs:read': { kind: 'read', tier: 'elevated', since: '1.1.0' },
|
|
111
|
+
// Discard the working tree — `resetWorkingTree()` wipes the COW writable layer +
|
|
112
|
+
// clears the journal, destroying the user's UNSAVED work irreversibly and
|
|
113
|
+
// UNREVIEWABLY. **First-party-only** (the first cap to re-enter this tier after
|
|
114
|
+
// R3-33d emptied it): only a pinned build-default `panel.contribute` binding may
|
|
115
|
+
// hold it — a fork/preview/third-party binding can NEVER discard the user's work,
|
|
116
|
+
// enforced by tier (`buildConsent` refuses it to a non-first-party binding,
|
|
117
|
+
// `overridePolicy` strips it on a repoint). Marked maximally-explicit so the one
|
|
118
|
+
// first-party line that does carry it renders with the scariest styling.
|
|
119
|
+
'vcs:reset': { kind: 'action', tier: 'first-party-only', since: '1.1.0', maximallyExplicit: true },
|
|
120
|
+
// Initiate a host-mediated cross-app DRAG-OUT into the previewed app
|
|
121
|
+
// (FILE_EXPLORER_SPEC §7, R3-83). The source app calls `startItemDrag(item)`;
|
|
122
|
+
// the host draws the trusted drag ghost, tracks the pointer across the
|
|
123
|
+
// cross-origin iframe boundary (which native HTML5 DnD cannot cross), and on a
|
|
124
|
+
// drop over the preview delivers `{ item, from, position }` to a SUBSCRIBED
|
|
125
|
+
// receiver. Synthesizing a drag INTO a sibling app is an injection / clickjacking
|
|
126
|
+
// primitive (FE-DND-1), so this is **first-party-only**: only a pinned
|
|
127
|
+
// build-default chrome binding (the file explorer) may hold it — a fork / preview
|
|
128
|
+
// / third-party binding can NEVER initiate a cross-app drag, enforced by tier
|
|
129
|
+
// (`buildConsent` refuses it to a non-first-party binding, never offering a
|
|
130
|
+
// consent line, exactly like `vcs:reset`). Marked maximally-explicit so the one
|
|
131
|
+
// first-party line that carries it renders with the scariest styling (same tier
|
|
132
|
+
// as `editor:open`/`editor:write`/`vcs:reset`). Receiving a drop needs NO new
|
|
133
|
+
// grant — the previewed app opts in by subscribing (`onItemDrop`).
|
|
134
|
+
'dnd:source': { kind: 'action', tier: 'first-party-only', since: '1.2.0', maximallyExplicit: true },
|
|
135
|
+
// The §5.5 method catalog (the app's own filtered RPC surface) — baseline:
|
|
136
|
+
// every app may discover what IT can call; the list is grant-filtered so it
|
|
137
|
+
// reveals nothing the app couldn't already invoke.
|
|
138
|
+
'catalog:read': { kind: 'read', tier: 'baseline', since: '1.0.0' },
|
|
139
|
+
'commands:read': { kind: 'read', tier: 'elevated', since: '1.0.0' },
|
|
140
|
+
'commands:run': { kind: 'action', tier: 'elevated', since: '1.0.0' },
|
|
141
|
+
ipc: { kind: 'action', tier: 'elevated', since: '1.0.0', parameterized: true },
|
|
142
|
+
// Invoke another app via a task contract (§5.7). Elevated: summoning overlays +
|
|
143
|
+
// delegating file caps is real authority. Parameterized — the task set is bounded
|
|
144
|
+
// by the app's manifest `invokes` declaration (§5.8), enforced in the handler.
|
|
145
|
+
'task:invoke': { kind: 'action', tier: 'elevated', since: '1.0.0', parameterized: true, appScoped: true },
|
|
146
|
+
'net:fetch': { kind: 'action', tier: 'elevated', since: '1.0.0', parameterized: true, appScoped: true },
|
|
147
|
+
// Host-owned secret store (SECRETS_SPEC §4). All elevated; the value is never
|
|
148
|
+
// readable by any app (`secrets:list` exposes metadata only). `secrets:add`
|
|
149
|
+
// opens a host-drawn modal; `secrets:revoke` deletes + cascades use-grants. The
|
|
150
|
+
// per-(app,secret) USE grant is NOT a capability row — it is minted via the
|
|
151
|
+
// `requestSecret` powerbox (gated by `net:fetch`, since a secret is only usable
|
|
152
|
+
// through §6 injection).
|
|
153
|
+
'secrets:add': { kind: 'action', tier: 'elevated', since: '1.1.0' },
|
|
154
|
+
'secrets:list': { kind: 'read', tier: 'elevated', since: '1.1.0' },
|
|
155
|
+
'secrets:revoke': { kind: 'action', tier: 'elevated', since: '1.1.0' },
|
|
156
|
+
'agent:session': { kind: 'action', tier: 'elevated', since: '1.0.0' },
|
|
157
|
+
};
|
|
158
|
+
/** The current registry/vocabulary version (§5.11). Bumped to 1.2.0 with the
|
|
159
|
+
* per-user settings-space capabilities (`settings:app`/`settings:fork`/
|
|
160
|
+
* `settings:all`), mirroring capabilities.json. */
|
|
161
|
+
exports.REGISTRY_VERSION = '1.2.0';
|
|
162
|
+
/** Is `cap` a known kernel capability? (Closed vocabulary — §5.12.) */
|
|
163
|
+
function isKnownCapability(cap) {
|
|
164
|
+
return Object.prototype.hasOwnProperty.call(exports.CAPABILITIES, cap);
|
|
165
|
+
}
|
|
166
|
+
function tierOf(cap) {
|
|
167
|
+
return exports.CAPABILITIES[cap].tier;
|
|
168
|
+
}
|
|
169
|
+
/** Baseline = what the previewed app and any unconsented binding may hold. */
|
|
170
|
+
exports.BASELINE_CAPABILITIES = Object.keys(exports.CAPABILITIES).filter((c) => exports.CAPABILITIES[c].tier === 'baseline');
|
|
171
|
+
function isBaseline(cap) {
|
|
172
|
+
return exports.CAPABILITIES[cap].tier === 'baseline';
|
|
173
|
+
}
|
|
174
|
+
/** App-scoped consentables — the elevated caps a previewed/forked app may EARN
|
|
175
|
+
* via lazy first-use or manifest-`requests` consent (§8.9/§8.15), as opposed to
|
|
176
|
+
* region-binding-only elevated caps. (`contribute:self` joined this set in R3-33d
|
|
177
|
+
* per decision #1.) */
|
|
178
|
+
exports.APP_SCOPED_CAPABILITIES = Object.keys(exports.CAPABILITIES).filter((c) => exports.CAPABILITIES[c].appScoped === true);
|
|
179
|
+
function isAppScoped(cap) {
|
|
180
|
+
return exports.CAPABILITIES[cap].appScoped === true;
|
|
181
|
+
}
|
|
182
|
+
// ── §5.11 capability version gate (threat T26) ──────────────────────────────
|
|
183
|
+
//
|
|
184
|
+
// Each capability declares the lowest registry version that knows it (`since`).
|
|
185
|
+
// A binding may reference a capability this host is too old to enforce (an
|
|
186
|
+
// override / synced binding authored against a newer immediately.run). Mounting
|
|
187
|
+
// it would mount-then-break, so the loader must refuse with an actionable
|
|
188
|
+
// "update immediately.run" message (§6.3) — never a half-working region.
|
|
189
|
+
/** Compare dotted numeric versions: <0 if a<b, 0 if equal, >0 if a>b. Missing
|
|
190
|
+
* segments are treated as 0 ("1.2" === "1.2.0"); non-numeric segments as 0. */
|
|
191
|
+
function compareVersions(a, b) {
|
|
192
|
+
const pa = a.split('.');
|
|
193
|
+
const pb = b.split('.');
|
|
194
|
+
const n = Math.max(pa.length, pb.length);
|
|
195
|
+
for (let i = 0; i < n; i++) {
|
|
196
|
+
const x = Number.parseInt(pa[i] ?? '0', 10) || 0;
|
|
197
|
+
const y = Number.parseInt(pb[i] ?? '0', 10) || 0;
|
|
198
|
+
if (x !== y)
|
|
199
|
+
return x < y ? -1 : 1;
|
|
200
|
+
}
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
/** A capability is supported iff it is known AND its `since` ≤ the host version. */
|
|
204
|
+
function isSupportedCapability(cap, hostVersion = exports.REGISTRY_VERSION) {
|
|
205
|
+
if (!isKnownCapability(cap))
|
|
206
|
+
return false;
|
|
207
|
+
return compareVersions(exports.CAPABILITIES[cap].since, hostVersion) <= 0;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The subset of `caps` this host cannot enforce — unknown to its vocabulary, or
|
|
211
|
+
* declared at a `since` newer than `hostVersion`. A non-empty result means the
|
|
212
|
+
* region must refuse to mount with "update immediately.run (missing: …)" (T26).
|
|
213
|
+
* `hostVersion` is injectable so an older host can be simulated in tests.
|
|
214
|
+
*/
|
|
215
|
+
function unsupportedCapabilities(caps, hostVersion = exports.REGISTRY_VERSION) {
|
|
216
|
+
return caps.filter((c) => !isSupportedCapability(c, hostVersion));
|
|
217
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CreateSpaceParams, GrantSpaceParams, NetFetchHost } from './port';
|
|
2
|
+
/** A Firestore document path as alternating collection/doc segments, e.g.
|
|
3
|
+
* `['user-app-spaces', uid, 'apps', appKey, 'spaces', spaceId]`. */
|
|
4
|
+
export type DocPath = string[];
|
|
5
|
+
/** The environment-specific Firestore sentinels the field builders inject. The
|
|
6
|
+
* Web SDK passes `{ serverTimestamp, increment }` from `firebase/firestore`; the
|
|
7
|
+
* admin SDK passes the `FieldValue.*` equivalents. */
|
|
8
|
+
export interface MintSentinels {
|
|
9
|
+
serverTimestamp(): unknown;
|
|
10
|
+
increment(n: number): unknown;
|
|
11
|
+
}
|
|
12
|
+
/** Stable per-user identifier for a grant `(appKey, spaceId)`, used as the value
|
|
13
|
+
* of a delegated grant's `parentGrantId`. `::` is delimiter-safe: `appKey` uses
|
|
14
|
+
* `__` separators and a Firestore `spaceId` is alphanumeric. */
|
|
15
|
+
export declare const grantKey: (appKey: string, spaceId: string) => string;
|
|
16
|
+
/** Durable elevated/app-scoped grants expire after 90 days WITHOUT USE; first
|
|
17
|
+
* use after expiry re-prompts. Baseline needs no grant record, so this never
|
|
18
|
+
* touches it. */
|
|
19
|
+
export declare const GRANT_EXPIRY_MS: number;
|
|
20
|
+
/** A principal that can be granted access to a space. */
|
|
21
|
+
export declare const userPrincipal: (uid: string) => string;
|
|
22
|
+
/** Drop undefined values — Firestore rejects them. The two adapters historically
|
|
23
|
+
* each had their own copy of this; sharing it keeps the "omit absent optionals"
|
|
24
|
+
* rule identical on both sides. */
|
|
25
|
+
export declare const defined: <T extends Record<string, unknown>>(obj: T) => T;
|
|
26
|
+
export declare const spacePath: (spaceId: string) => DocPath;
|
|
27
|
+
export declare const memberPath: (spaceId: string, principal: string) => DocPath;
|
|
28
|
+
export declare const userSpacePath: (uid: string, spaceId: string) => DocPath;
|
|
29
|
+
export declare const appKeyPath: (uid: string, appKey: string) => DocPath;
|
|
30
|
+
export declare const appSpacePath: (uid: string, appKey: string, spaceId: string) => DocPath;
|
|
31
|
+
export declare const userCountPath: (uid: string) => DocPath;
|
|
32
|
+
export declare const appCountPath: (uid: string, appKey: string) => DocPath;
|
|
33
|
+
/** `spaces/{spaceId}` — the root doc (written WITHOUT merge). */
|
|
34
|
+
export declare const spaceDocFields: (params: Pick<CreateSpaceParams, "owner" | "name" | "createdInNamespace" | "createdInRepository">, s: MintSentinels) => Record<string, unknown>;
|
|
35
|
+
/** `spaces/{spaceId}/members/{user:owner}` — the owner membership (no merge). */
|
|
36
|
+
export declare const ownerMemberFields: (s: MintSentinels) => Record<string, unknown>;
|
|
37
|
+
/** `user-spaces/{owner}/spaces/{spaceId}` — EFFECTIVE access (no merge). */
|
|
38
|
+
export declare const ownerUserSpaceFields: (params: Pick<CreateSpaceParams, "owner" | "name">) => Record<string, unknown>;
|
|
39
|
+
/** `space-counts/{uid}` — per-user owned counter (merge). */
|
|
40
|
+
export declare const userCountFields: (s: MintSentinels) => Record<string, unknown>;
|
|
41
|
+
/** `space-counts/{uid}/apps/{appKey}` — per-app created counter (merge). */
|
|
42
|
+
export declare const appCountFields: (s: MintSentinels) => Record<string, unknown>;
|
|
43
|
+
/** `user-app-spaces/{uid}/apps/{appKey}` — the enumerable app-key marker doc
|
|
44
|
+
* touched when a grant is written (merge). */
|
|
45
|
+
export declare const appKeyTouchFields: (s: MintSentinels) => Record<string, unknown>;
|
|
46
|
+
/** `user-app-spaces/{uid}/apps/{appKey}/spaces/{spaceId}` — the durable §8.7
|
|
47
|
+
* grant doc (merge). `mintPath` defaults to `interactive`; `grantedAt`/`lastUsedAt`
|
|
48
|
+
* drive the §8.15 90-day-unused expiry. */
|
|
49
|
+
export declare const appSpaceGrantFields: (params: Pick<GrantSpaceParams, "name" | "subtree" | "mode" | "rules" | "declaredUri" | "mintPath" | "parentGrantId">, s: MintSentinels) => Record<string, unknown>;
|
|
50
|
+
/** Union net:fetch host rules by origin (incoming wins) — the "consent
|
|
51
|
+
* accumulates" merge both adapters apply before writing the host set. */
|
|
52
|
+
export declare const mergeNetFetchHosts: (existing: readonly NetFetchHost[], incoming: readonly NetFetchHost[]) => NetFetchHost[];
|
|
53
|
+
/** `user-app-spaces/{uid}/apps/{appKey}` — the net:fetch host grant (merge).
|
|
54
|
+
* `hadGrantedAt` is whether the doc already carried a `netFetchGrantedAt` (so the
|
|
55
|
+
* grant time is stamped ONCE, on first mint, and `netFetchLastUsedAt` refreshes
|
|
56
|
+
* on every (re-)consent). */
|
|
57
|
+
export declare const netFetchGrantFields: (mergedHosts: readonly NetFetchHost[], hadGrantedAt: boolean, s: MintSentinels) => Record<string, unknown>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The byte-faithful document layout (UI_AS_APPS_SPEC §8.6/§8.7/§8.15) — the
|
|
3
|
+
// SINGLE source of the Firestore paths and field objects every grant-mint write
|
|
4
|
+
// produces. The browser `FirestoreSpaceStore` (Web SDK) and the backend
|
|
5
|
+
// `AdminMintStore` (admin SDK) write against two different Firestore client APIs
|
|
6
|
+
// and therefore cannot share *call* code — but they MUST write byte-identical
|
|
7
|
+
// documents (same collection paths, same field names, same `grantKey` /
|
|
8
|
+
// `mintPath` / expiry stamping). If they drift, grants minted by the
|
|
9
|
+
// CLI/backend would not be the grants site-main's boot gate reads — a silent,
|
|
10
|
+
// security-relevant failure.
|
|
11
|
+
//
|
|
12
|
+
// The guarantee: both adapters compute their paths and assemble their field
|
|
13
|
+
// objects HERE, injecting only the environment-specific timestamp/increment
|
|
14
|
+
// SENTINELS (Web `serverTimestamp()`/`increment()` vs admin
|
|
15
|
+
// `FieldValue.serverTimestamp()`/`FieldValue.increment()`). The raw
|
|
16
|
+
// `.set()`/`.update()` is the only thing each adapter does itself. Drift is then
|
|
17
|
+
// impossible without editing a helper both consume.
|
|
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.userPrincipal = exports.GRANT_EXPIRY_MS = exports.grantKey = void 0;
|
|
20
|
+
/** Stable per-user identifier for a grant `(appKey, spaceId)`, used as the value
|
|
21
|
+
* of a delegated grant's `parentGrantId`. `::` is delimiter-safe: `appKey` uses
|
|
22
|
+
* `__` separators and a Firestore `spaceId` is alphanumeric. */
|
|
23
|
+
const grantKey = (appKey, spaceId) => `${appKey}::${spaceId}`;
|
|
24
|
+
exports.grantKey = grantKey;
|
|
25
|
+
/** Durable elevated/app-scoped grants expire after 90 days WITHOUT USE; first
|
|
26
|
+
* use after expiry re-prompts. Baseline needs no grant record, so this never
|
|
27
|
+
* touches it. */
|
|
28
|
+
exports.GRANT_EXPIRY_MS = 90 * 24 * 60 * 60 * 1000;
|
|
29
|
+
/** A principal that can be granted access to a space. */
|
|
30
|
+
const userPrincipal = (uid) => `user:${uid}`;
|
|
31
|
+
exports.userPrincipal = userPrincipal;
|
|
32
|
+
/** Drop undefined values — Firestore rejects them. The two adapters historically
|
|
33
|
+
* each had their own copy of this; sharing it keeps the "omit absent optionals"
|
|
34
|
+
* rule identical on both sides. */
|
|
35
|
+
const defined = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
36
|
+
exports.defined = defined;
|
|
37
|
+
// --- document paths (pure, sentinel-free) -----------------------------------
|
|
38
|
+
const spacePath = (spaceId) => ['spaces', spaceId];
|
|
39
|
+
exports.spacePath = spacePath;
|
|
40
|
+
const memberPath = (spaceId, principal) => [
|
|
41
|
+
'spaces',
|
|
42
|
+
spaceId,
|
|
43
|
+
'members',
|
|
44
|
+
principal,
|
|
45
|
+
];
|
|
46
|
+
exports.memberPath = memberPath;
|
|
47
|
+
const userSpacePath = (uid, spaceId) => [
|
|
48
|
+
'user-spaces',
|
|
49
|
+
uid,
|
|
50
|
+
'spaces',
|
|
51
|
+
spaceId,
|
|
52
|
+
];
|
|
53
|
+
exports.userSpacePath = userSpacePath;
|
|
54
|
+
const appKeyPath = (uid, appKey) => [
|
|
55
|
+
'user-app-spaces',
|
|
56
|
+
uid,
|
|
57
|
+
'apps',
|
|
58
|
+
appKey,
|
|
59
|
+
];
|
|
60
|
+
exports.appKeyPath = appKeyPath;
|
|
61
|
+
const appSpacePath = (uid, appKey, spaceId) => [
|
|
62
|
+
'user-app-spaces',
|
|
63
|
+
uid,
|
|
64
|
+
'apps',
|
|
65
|
+
appKey,
|
|
66
|
+
'spaces',
|
|
67
|
+
spaceId,
|
|
68
|
+
];
|
|
69
|
+
exports.appSpacePath = appSpacePath;
|
|
70
|
+
const userCountPath = (uid) => ['space-counts', uid];
|
|
71
|
+
exports.userCountPath = userCountPath;
|
|
72
|
+
const appCountPath = (uid, appKey) => [
|
|
73
|
+
'space-counts',
|
|
74
|
+
uid,
|
|
75
|
+
'apps',
|
|
76
|
+
appKey,
|
|
77
|
+
];
|
|
78
|
+
exports.appCountPath = appCountPath;
|
|
79
|
+
// --- field objects (inject the timestamp/increment sentinels) ---------------
|
|
80
|
+
/** `spaces/{spaceId}` — the root doc (written WITHOUT merge). */
|
|
81
|
+
const spaceDocFields = (params, s) => (0, exports.defined)({
|
|
82
|
+
owner: params.owner,
|
|
83
|
+
createdAt: s.serverTimestamp(),
|
|
84
|
+
name: params.name,
|
|
85
|
+
createdInNamespace: params.createdInNamespace,
|
|
86
|
+
createdInRepository: params.createdInRepository,
|
|
87
|
+
});
|
|
88
|
+
exports.spaceDocFields = spaceDocFields;
|
|
89
|
+
/** `spaces/{spaceId}/members/{user:owner}` — the owner membership (no merge). */
|
|
90
|
+
const ownerMemberFields = (s) => ({
|
|
91
|
+
role: 'owner',
|
|
92
|
+
addedAt: s.serverTimestamp(),
|
|
93
|
+
});
|
|
94
|
+
exports.ownerMemberFields = ownerMemberFields;
|
|
95
|
+
/** `user-spaces/{owner}/spaces/{spaceId}` — EFFECTIVE access (no merge). */
|
|
96
|
+
const ownerUserSpaceFields = (params) => (0, exports.defined)({ role: 'owner', name: params.name, owner: params.owner });
|
|
97
|
+
exports.ownerUserSpaceFields = ownerUserSpaceFields;
|
|
98
|
+
/** `space-counts/{uid}` — per-user owned counter (merge). */
|
|
99
|
+
const userCountFields = (s) => ({
|
|
100
|
+
owned: s.increment(1),
|
|
101
|
+
});
|
|
102
|
+
exports.userCountFields = userCountFields;
|
|
103
|
+
/** `space-counts/{uid}/apps/{appKey}` — per-app created counter (merge). */
|
|
104
|
+
const appCountFields = (s) => ({
|
|
105
|
+
created: s.increment(1),
|
|
106
|
+
});
|
|
107
|
+
exports.appCountFields = appCountFields;
|
|
108
|
+
/** `user-app-spaces/{uid}/apps/{appKey}` — the enumerable app-key marker doc
|
|
109
|
+
* touched when a grant is written (merge). */
|
|
110
|
+
const appKeyTouchFields = (s) => ({
|
|
111
|
+
touchedAt: s.serverTimestamp(),
|
|
112
|
+
});
|
|
113
|
+
exports.appKeyTouchFields = appKeyTouchFields;
|
|
114
|
+
/** `user-app-spaces/{uid}/apps/{appKey}/spaces/{spaceId}` — the durable §8.7
|
|
115
|
+
* grant doc (merge). `mintPath` defaults to `interactive`; `grantedAt`/`lastUsedAt`
|
|
116
|
+
* drive the §8.15 90-day-unused expiry. */
|
|
117
|
+
const appSpaceGrantFields = (params, s) => (0, exports.defined)({
|
|
118
|
+
boundAt: s.serverTimestamp(),
|
|
119
|
+
grantedAt: s.serverTimestamp(),
|
|
120
|
+
lastUsedAt: s.serverTimestamp(),
|
|
121
|
+
name: params.name,
|
|
122
|
+
// Plan 12 §8.7: `rules` is authoritative; `subtree`/`mode` are kept as the
|
|
123
|
+
// deprecated `rules[0]` mirror for not-yet-migrated readers. When no rule-set
|
|
124
|
+
// is given, derive a single-rule set from the legacy scope so the backend
|
|
125
|
+
// single-scope mint path still emits `rules` (byte-identical with site-main).
|
|
126
|
+
subtree: params.subtree,
|
|
127
|
+
mode: params.mode,
|
|
128
|
+
rules: params.rules && params.rules.length > 0
|
|
129
|
+
? params.rules
|
|
130
|
+
: [{ subtree: params.subtree ?? '/', mode: params.mode ?? 'rw' }],
|
|
131
|
+
declaredUri: params.declaredUri,
|
|
132
|
+
mintPath: params.mintPath ?? 'interactive',
|
|
133
|
+
parentGrantId: params.parentGrantId,
|
|
134
|
+
});
|
|
135
|
+
exports.appSpaceGrantFields = appSpaceGrantFields;
|
|
136
|
+
/** Union net:fetch host rules by origin (incoming wins) — the "consent
|
|
137
|
+
* accumulates" merge both adapters apply before writing the host set. */
|
|
138
|
+
const mergeNetFetchHosts = (existing, incoming) => {
|
|
139
|
+
const byOrigin = new Map();
|
|
140
|
+
for (const h of existing)
|
|
141
|
+
byOrigin.set(h.origin, h);
|
|
142
|
+
for (const h of incoming)
|
|
143
|
+
byOrigin.set(h.origin, h);
|
|
144
|
+
return [...byOrigin.values()];
|
|
145
|
+
};
|
|
146
|
+
exports.mergeNetFetchHosts = mergeNetFetchHosts;
|
|
147
|
+
/** `user-app-spaces/{uid}/apps/{appKey}` — the net:fetch host grant (merge).
|
|
148
|
+
* `hadGrantedAt` is whether the doc already carried a `netFetchGrantedAt` (so the
|
|
149
|
+
* grant time is stamped ONCE, on first mint, and `netFetchLastUsedAt` refreshes
|
|
150
|
+
* on every (re-)consent). */
|
|
151
|
+
const netFetchGrantFields = (mergedHosts, hadGrantedAt, s) => (0, exports.defined)({
|
|
152
|
+
netFetch: [...mergedHosts],
|
|
153
|
+
netFetchGrantedAt: hadGrantedAt ? undefined : s.serverTimestamp(),
|
|
154
|
+
netFetchLastUsedAt: s.serverTimestamp(),
|
|
155
|
+
});
|
|
156
|
+
exports.netFetchGrantFields = netFetchGrantFields;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @immediately-run/preauth-core — the shared pre-auth core.
|
|
3
|
+
//
|
|
4
|
+
// ONE §8.9 gate (`planPreAuthCapabilities`), ONE mint path (`mintConsentedGrants`,
|
|
5
|
+
// `applyPreAuth`), ONE capability vocabulary (`./capabilities`), ONE wire layout
|
|
6
|
+
// (`./docLayout`). site-main (browser Firestore) and the backend (admin Firestore)
|
|
7
|
+
// both consume this so a CLI/backend-minted grant is byte-identical to one
|
|
8
|
+
// site-main mints, and no surface can mint a capability the in-browser gate would
|
|
9
|
+
// have refused.
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
22
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
__exportStar(require("./capabilities"), exports);
|
|
26
|
+
__exportStar(require("./port"), exports);
|
|
27
|
+
__exportStar(require("./docLayout"), exports);
|
|
28
|
+
__exportStar(require("./bootConsent"), exports);
|
|
29
|
+
__exportStar(require("./m1PreAuth"), exports);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type Capability } from './capabilities';
|
|
2
|
+
import { type ConsentSelection, type MintErrorSink, type MintResult } from './bootConsent';
|
|
3
|
+
import type { MintStore, NetFetchHost } from './port';
|
|
4
|
+
export type PreAuthRefusalReason =
|
|
5
|
+
/** A non-app-scoped elevated cap — region-binding-only authority (§8.9). */
|
|
6
|
+
'broad-elevated'
|
|
7
|
+
/** Not in the closed capability vocabulary (§5.12) — fail-closed. */
|
|
8
|
+
| 'unknown';
|
|
9
|
+
export interface PreAuthRefusal {
|
|
10
|
+
capability: string;
|
|
11
|
+
reason: PreAuthRefusalReason;
|
|
12
|
+
}
|
|
13
|
+
export interface PreAuthPlan {
|
|
14
|
+
/** App-scoped elevated caps a policy MAY pre-authorize for a URL-loaded appKey. */
|
|
15
|
+
grantable: Capability[];
|
|
16
|
+
/** Baseline caps requested — auto-held, no grant needed (dropped silently). */
|
|
17
|
+
baseline: Capability[];
|
|
18
|
+
/** Refused by the §8.9 target check — these block the whole pre-auth. */
|
|
19
|
+
refused: PreAuthRefusal[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The pure §8.9 target check: partition requested capability names into
|
|
23
|
+
* {grantable app-scoped, baseline no-op, refused}. Order-independent; total.
|
|
24
|
+
*/
|
|
25
|
+
export declare function planPreAuthCapabilities(requested: readonly string[]): PreAuthPlan;
|
|
26
|
+
/** Safe to apply iff the §8.9 check refused nothing (fail-closed, all-or-nothing). */
|
|
27
|
+
export declare const isPreAuthClean: (plan: PreAuthPlan) => boolean;
|
|
28
|
+
/** What a policy/settings surface asks M1 to pre-authorize for `(uid, appKey)` —
|
|
29
|
+
* structurally the same shape M3's consent screen produces (the declared
|
|
30
|
+
* `requests`), so the two paths mint identical grants. */
|
|
31
|
+
export interface PreAuthRequest {
|
|
32
|
+
/** Capability names being pre-authorized — validated by the §8.9 target check. */
|
|
33
|
+
capabilities: readonly string[];
|
|
34
|
+
/** Mount selections (create/bind a space per slot), mirroring the M3 screen. */
|
|
35
|
+
mounts: readonly ConsentSelection[];
|
|
36
|
+
/** net:fetch hosts to pre-grant (the headless/BYOK case). */
|
|
37
|
+
netFetchHosts: readonly NetFetchHost[];
|
|
38
|
+
}
|
|
39
|
+
export interface PreAuthResult {
|
|
40
|
+
/** True iff the pre-auth passed the §8.9 check AND every grant minted. */
|
|
41
|
+
ok: boolean;
|
|
42
|
+
/** §8.9 refusals — non-empty ⇒ NOTHING was minted (all-or-nothing). */
|
|
43
|
+
refused: PreAuthRefusal[];
|
|
44
|
+
/** The mint outcome, when the §8.9 check passed (absent on refusal). */
|
|
45
|
+
mint?: MintResult;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
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.
|
|
51
|
+
*
|
|
52
|
+
* Refusal is terminal and silent of side effects: when any requested capability
|
|
53
|
+
* is broad-elevated or unknown, the function mints NOTHING and returns the
|
|
54
|
+
* refusals — the caller surfaces them (the policy is malformed/over-broad).
|
|
55
|
+
*/
|
|
56
|
+
export declare function applyPreAuth(store: MintStore, uid: string, appKey: string, request: PreAuthRequest, onError?: MintErrorSink): Promise<PreAuthResult>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// M1 — programmatic / policy pre-authorization (UI_AS_APPS_SPEC §8.15 "M1 —
|
|
3
|
+
// Pre-authorization / policy", clamped by the §8.9 target check).
|
|
4
|
+
//
|
|
5
|
+
// M1 lets a policy (operator-tier) or settings (user-tier) write path record the
|
|
6
|
+
// SAME durable consent-grant M3 writes — ahead of time, instead of at a modal —
|
|
7
|
+
// so a headless/CI/cron/`immediately-run dev` run finds the grant already present
|
|
8
|
+
// and boots with NO prompt. It is not a region-repointing registry layer (§3.3):
|
|
9
|
+
// it only writes the §8.6/§8.7 grant set the gate already reads, so M1 minting
|
|
10
|
+
// flows through the ONE existing mint path (`mintConsentedGrants`, stamped
|
|
11
|
+
// `mintPath:'policy'`) and cannot drift from M3.
|
|
12
|
+
//
|
|
13
|
+
// THE SECURITY INVARIANT — the §8.9 target check. A pre-auth for a URL-loaded
|
|
14
|
+
// `appKey` may only cover **app-scoped** elevated capabilities (`net:fetch`,
|
|
15
|
+
// `task:invoke`, `contribute:self` — the set an ordinary previewed/forked app can
|
|
16
|
+
// EARN per §8.9/§8.15) plus mounts (app-scoped by construction). A **broad-elevated**
|
|
17
|
+
// capability — any non-app-scoped elevated cap (`spaces:user`/`spaces:admin`,
|
|
18
|
+
// `editor:write`, `contribute:direct`/`contribute:any`, `editor:open`, …) — is
|
|
19
|
+
// REFUSED: M1 cannot mint it for a URL-loaded appKey. Unknown capabilities are
|
|
20
|
+
// refused (fail-closed). Baseline capabilities need no grant and are dropped.
|
|
21
|
+
//
|
|
22
|
+
// The check is **all-or-nothing**: if a policy names ANY refused capability the
|
|
23
|
+
// whole pre-auth is rejected and NOTHING is minted — a partial apply would
|
|
24
|
+
// silently drop the scary capability and look like it had been honored.
|
|
25
|
+
//
|
|
26
|
+
// Pure decision (`planPreAuthCapabilities`/`isPreAuthClean`) + a thin store-glue
|
|
27
|
+
// write path (`applyPreAuth`) that reuses `mintConsentedGrants`. No React, no UI.
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.isPreAuthClean = void 0;
|
|
30
|
+
exports.planPreAuthCapabilities = planPreAuthCapabilities;
|
|
31
|
+
exports.applyPreAuth = applyPreAuth;
|
|
32
|
+
const capabilities_1 = require("./capabilities");
|
|
33
|
+
const bootConsent_1 = require("./bootConsent");
|
|
34
|
+
/**
|
|
35
|
+
* The pure §8.9 target check: partition requested capability names into
|
|
36
|
+
* {grantable app-scoped, baseline no-op, refused}. Order-independent; total.
|
|
37
|
+
*/
|
|
38
|
+
function planPreAuthCapabilities(requested) {
|
|
39
|
+
const grantable = [];
|
|
40
|
+
const baseline = [];
|
|
41
|
+
const refused = [];
|
|
42
|
+
for (const cap of requested) {
|
|
43
|
+
if (!(0, capabilities_1.isKnownCapability)(cap)) {
|
|
44
|
+
refused.push({ capability: cap, reason: 'unknown' });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if ((0, capabilities_1.isBaseline)(cap)) {
|
|
48
|
+
baseline.push(cap);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if ((0, capabilities_1.isAppScoped)(cap)) {
|
|
52
|
+
grantable.push(cap);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Known + elevated + NOT app-scoped ⇒ broad-elevated: region-binding-only,
|
|
56
|
+
// never minted for a URL-loaded appKey (the §8.9 clamp).
|
|
57
|
+
refused.push({ capability: cap, reason: 'broad-elevated' });
|
|
58
|
+
}
|
|
59
|
+
return { grantable, baseline, refused };
|
|
60
|
+
}
|
|
61
|
+
/** Safe to apply iff the §8.9 check refused nothing (fail-closed, all-or-nothing). */
|
|
62
|
+
const isPreAuthClean = (plan) => plan.refused.length === 0;
|
|
63
|
+
exports.isPreAuthClean = isPreAuthClean;
|
|
64
|
+
/**
|
|
65
|
+
* The M1 write path: validate the requested capabilities against the §8.9 target
|
|
66
|
+
* check, then — only if clean — mint the mounts + net:fetch hosts as durable
|
|
67
|
+
* grants with `policy` provenance, through the same `mintConsentedGrants` M3 uses.
|
|
68
|
+
*
|
|
69
|
+
* Refusal is terminal and silent of side effects: when any requested capability
|
|
70
|
+
* is broad-elevated or unknown, the function mints NOTHING and returns the
|
|
71
|
+
* refusals — the caller surfaces them (the policy is malformed/over-broad).
|
|
72
|
+
*/
|
|
73
|
+
async function applyPreAuth(store, uid, appKey, request, onError) {
|
|
74
|
+
const plan = planPreAuthCapabilities(request.capabilities);
|
|
75
|
+
if (!(0, exports.isPreAuthClean)(plan)) {
|
|
76
|
+
return { ok: false, refused: plan.refused };
|
|
77
|
+
}
|
|
78
|
+
const mint = await (0, bootConsent_1.mintConsentedGrants)(store, uid, appKey, request.mounts, request.netFetchHosts, 'policy', onError);
|
|
79
|
+
return { ok: mint.ok, refused: [], mint };
|
|
80
|
+
}
|
package/dist/port.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** The filesystem scope a grant confers (UI_AS_APPS_SPEC §8.7). Absent fields
|
|
2
|
+
* mean whole-space, read-write. */
|
|
3
|
+
export type GrantMode = 'ro' | 'rw';
|
|
4
|
+
/** One granted scope within a mount (UI_AS_APPS_SPEC §8.7, plan 12): an absolute
|
|
5
|
+
* `subtree` in the backing fs and the access `mode` there. A grant carries a SET
|
|
6
|
+
* of these (`rules`); the most specific (longest-prefix) rule governs a path. The
|
|
7
|
+
* wire shape, so both mint adapters write byte-identical `rules` arrays. */
|
|
8
|
+
export interface ScopeRule {
|
|
9
|
+
subtree: string;
|
|
10
|
+
mode: GrantMode;
|
|
11
|
+
}
|
|
12
|
+
/** How a durable grant was minted (UI_AS_APPS_SPEC §8.15 provenance). `interactive`
|
|
13
|
+
* = the M3 consent screen; `policy` = M1 pre-authorization; `delegated` = M2
|
|
14
|
+
* attenuated delegation from a parent grant. Drives the §8.11 audit view and, for
|
|
15
|
+
* `delegated`, the revoke cascade (see `parentGrantId`). */
|
|
16
|
+
export type MintPath = 'interactive' | 'policy' | 'delegated';
|
|
17
|
+
/** A consented `net:fetch` host rule (§5.11) — structurally the registry's
|
|
18
|
+
* `FetchRule`, redefined here so this backend-agnostic port stays import-free. */
|
|
19
|
+
export interface NetFetchHost {
|
|
20
|
+
origin: string;
|
|
21
|
+
paths?: string[];
|
|
22
|
+
methods?: string[];
|
|
23
|
+
/** §8.15 provenance — how this host was granted (default `interactive`). For an
|
|
24
|
+
* M2-`delegated` host this is the attenuated subset of a caller's net:fetch grant. */
|
|
25
|
+
mintPath?: MintPath;
|
|
26
|
+
/** §8.15 — for an M2 `delegated` host, the `netFetchGrantKey` of the caller's
|
|
27
|
+
* net:fetch grant it was attenuated from; revoking that parent cascades here. */
|
|
28
|
+
parentGrantId?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Parameters for `MintStore.createSpace` (named, `db`-free — the adapter holds
|
|
31
|
+
* the connection). */
|
|
32
|
+
export interface CreateSpaceParams {
|
|
33
|
+
owner: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
/** Informational breadcrumbs only — never used for identity or access. */
|
|
36
|
+
createdInNamespace?: string;
|
|
37
|
+
createdInRepository?: string;
|
|
38
|
+
/** Record the new space in this app's binding list for the owner (the durable
|
|
39
|
+
* app↔space link the grant scopes). No longer a slot pointer. */
|
|
40
|
+
appKey?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Parameters for `MintStore.grantSpaceToApp` — the durable §8.7 grant for one
|
|
43
|
+
* (app, mount): what the app may mount and how. */
|
|
44
|
+
export interface GrantSpaceParams {
|
|
45
|
+
uid: string;
|
|
46
|
+
appKey: string;
|
|
47
|
+
spaceId: string;
|
|
48
|
+
subtree?: string;
|
|
49
|
+
mode?: GrantMode;
|
|
50
|
+
/** Plan 12 §8.7: the FULL rule-set to write (≥1). When given it is authoritative
|
|
51
|
+
* (site-main's read-modify-merge passes the merged set); when omitted the grant
|
|
52
|
+
* doc derives a single-rule `[{ subtree ?? '/', mode ?? 'rw' }]` from the legacy
|
|
53
|
+
* `subtree`/`mode` (the backend single-scope path), so both adapters emit `rules`. */
|
|
54
|
+
rules?: ScopeRule[];
|
|
55
|
+
name?: string;
|
|
56
|
+
/** §11.4 — the declared required-mount uri this grant satisfies, so a later
|
|
57
|
+
* boot re-provisions it without re-consent (the durable slot replacement). */
|
|
58
|
+
declaredUri?: string;
|
|
59
|
+
/** §8.15 provenance; defaults to `interactive` when omitted. */
|
|
60
|
+
mintPath?: MintPath;
|
|
61
|
+
/** §8.15 — parent `grantKey` for an M2 `delegated` grant. */
|
|
62
|
+
parentGrantId?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Parameters for `MintStore.grantNetFetchHosts` — the per-(user, app) granted
|
|
65
|
+
* host set, the grant half of the `manifest ∩ grant` net:fetch allowlist. */
|
|
66
|
+
export interface GrantNetFetchParams {
|
|
67
|
+
uid: string;
|
|
68
|
+
appKey: string;
|
|
69
|
+
hosts: readonly NetFetchHost[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The mint port: exactly the methods `mintConsentedGrants` calls. Every backend
|
|
73
|
+
* (browser Firestore, admin Firestore, the in-memory test double) implements
|
|
74
|
+
* these three the SAME way — serializing through the shared `docLayout` builders
|
|
75
|
+
* so the documents are byte-identical regardless of which SDK wrote them.
|
|
76
|
+
*/
|
|
77
|
+
export interface MintStore {
|
|
78
|
+
/** Create a new space owned by `owner`, returning its opaque generated id. */
|
|
79
|
+
createSpace(params: CreateSpaceParams): Promise<string>;
|
|
80
|
+
/** Record the durable §8.7 grant binding `spaceId` to `appKey` for `uid`. */
|
|
81
|
+
grantSpaceToApp(params: GrantSpaceParams): Promise<void>;
|
|
82
|
+
/** Union the given net:fetch hosts into the app's consented host set. */
|
|
83
|
+
grantNetFetchHosts(params: GrantNetFetchParams): Promise<void>;
|
|
84
|
+
}
|
package/dist/port.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The mint PORT — the narrow persistence interface the ONE grant-mint path
|
|
3
|
+
// (`mintConsentedGrants`) depends on, plus the shared domain types its params
|
|
4
|
+
// carry. Both site-main's `FirestoreSpaceStore` (Firebase Web SDK) and the
|
|
5
|
+
// backend's `AdminMintStore` (firebase-admin) structurally satisfy `MintStore`,
|
|
6
|
+
// so the same orchestration drives both environments.
|
|
7
|
+
//
|
|
8
|
+
// This is a deliberate 3-method SUBSET of site-main's ~30-method `SpaceStore`:
|
|
9
|
+
// `mintConsentedGrants` only ever calls `grantNetFetchHosts`, `createSpace`, and
|
|
10
|
+
// `grantSpaceToApp` (the binding/grant doc IS the binding now — there is no
|
|
11
|
+
// separate `bindSpaceToApp` slot write). The broad `SpaceStore` (subscriptions,
|
|
12
|
+
// sharing, soft-delete, audit reads, …) stays in site-main and is NOT extracted.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@immediately-run/preauth-core",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./capabilities": {
|
|
15
|
+
"types": "./dist/capabilities.d.ts",
|
|
16
|
+
"default": "./dist/capabilities.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.build.json",
|
|
24
|
+
"test": "jest",
|
|
25
|
+
"lint": "eslint src test",
|
|
26
|
+
"prepare": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@eslint/js": "^9.18.0",
|
|
30
|
+
"@types/jest": "^29.5.14",
|
|
31
|
+
"@types/node": "^20.17.0",
|
|
32
|
+
"eslint": "^9.18.0",
|
|
33
|
+
"jest": "^29.7.0",
|
|
34
|
+
"ts-jest": "^29.2.5",
|
|
35
|
+
"typescript": "^5.9.3",
|
|
36
|
+
"typescript-eslint": "^8.20.0"
|
|
37
|
+
}
|
|
38
|
+
}
|