@cotal-ai/core 0.16.0 → 0.18.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/dist/acls.d.ts +21 -0
- package/dist/acls.d.ts.map +1 -1
- package/dist/acls.js +44 -1
- package/dist/acls.js.map +1 -1
- package/dist/artifact.d.ts +45 -0
- package/dist/artifact.d.ts.map +1 -0
- package/dist/artifact.js +82 -0
- package/dist/artifact.js.map +1 -0
- package/dist/backup-config.d.ts +9 -2
- package/dist/backup-config.d.ts.map +1 -1
- package/dist/backup-config.js +6 -2
- package/dist/backup-config.js.map +1 -1
- package/dist/broker-tls.d.ts +70 -0
- package/dist/broker-tls.d.ts.map +1 -0
- package/dist/broker-tls.js +195 -0
- package/dist/broker-tls.js.map +1 -0
- package/dist/canonical.d.ts +14 -1
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +21 -1
- package/dist/canonical.js.map +1 -1
- package/dist/channels.js +4 -4
- package/dist/channels.js.map +1 -1
- package/dist/endpoint-action.d.ts +108 -3
- package/dist/endpoint-action.d.ts.map +1 -1
- package/dist/endpoint-action.js +209 -20
- package/dist/endpoint-action.js.map +1 -1
- package/dist/endpoint-binding.d.ts +138 -8
- package/dist/endpoint-binding.d.ts.map +1 -1
- package/dist/endpoint-binding.js +233 -26
- package/dist/endpoint-binding.js.map +1 -1
- package/dist/endpoint-contract-store.d.ts +30 -1
- package/dist/endpoint-contract-store.d.ts.map +1 -1
- package/dist/endpoint-contract-store.js +100 -29
- package/dist/endpoint-contract-store.js.map +1 -1
- package/dist/endpoint-envelope.d.ts +15 -37
- package/dist/endpoint-envelope.d.ts.map +1 -1
- package/dist/endpoint-envelope.js +93 -44
- package/dist/endpoint-envelope.js.map +1 -1
- package/dist/endpoint-error.d.ts +33 -0
- package/dist/endpoint-error.d.ts.map +1 -0
- package/dist/endpoint-error.js +34 -0
- package/dist/endpoint-error.js.map +1 -0
- package/dist/endpoint-grants.d.ts +63 -7
- package/dist/endpoint-grants.d.ts.map +1 -1
- package/dist/endpoint-grants.js +118 -9
- package/dist/endpoint-grants.js.map +1 -1
- package/dist/endpoint-invoke.d.ts +138 -0
- package/dist/endpoint-invoke.d.ts.map +1 -0
- package/dist/endpoint-invoke.js +393 -0
- package/dist/endpoint-invoke.js.map +1 -0
- package/dist/endpoint-records.d.ts.map +1 -1
- package/dist/endpoint-records.js +17 -0
- package/dist/endpoint-records.js.map +1 -1
- package/dist/endpoint-serve-kv.d.ts +97 -0
- package/dist/endpoint-serve-kv.d.ts.map +1 -0
- package/dist/endpoint-serve-kv.js +336 -0
- package/dist/endpoint-serve-kv.js.map +1 -0
- package/dist/endpoint-serve.d.ts.map +1 -1
- package/dist/endpoint-serve.js +21 -1
- package/dist/endpoint-serve.js.map +1 -1
- package/dist/endpoint-service.d.ts +1 -1
- package/dist/endpoint-service.d.ts.map +1 -1
- package/dist/endpoint-service.js +34 -10
- package/dist/endpoint-service.js.map +1 -1
- package/dist/endpoint-session-rail.d.ts +127 -0
- package/dist/endpoint-session-rail.d.ts.map +1 -0
- package/dist/endpoint-session-rail.js +325 -0
- package/dist/endpoint-session-rail.js.map +1 -0
- package/dist/endpoint-session.d.ts +1 -115
- package/dist/endpoint-session.d.ts.map +1 -1
- package/dist/endpoint-session.js +4 -316
- package/dist/endpoint-session.js.map +1 -1
- package/dist/endpoint-subjects.d.ts +20 -1
- package/dist/endpoint-subjects.d.ts.map +1 -1
- package/dist/endpoint-subjects.js +20 -1
- package/dist/endpoint-subjects.js.map +1 -1
- package/dist/endpoint-verbs.d.ts +1 -2
- package/dist/endpoint-verbs.d.ts.map +1 -1
- package/dist/endpoint-verbs.js +6 -6
- package/dist/endpoint-verbs.js.map +1 -1
- package/dist/endpoint.d.ts +157 -11
- package/dist/endpoint.d.ts.map +1 -1
- package/dist/endpoint.js +471 -47
- package/dist/endpoint.js.map +1 -1
- package/dist/evict.d.ts +47 -0
- package/dist/evict.d.ts.map +1 -1
- package/dist/evict.js +191 -59
- package/dist/evict.js.map +1 -1
- package/dist/identity.d.ts +34 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +46 -0
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/kv-scan.d.ts +8 -0
- package/dist/kv-scan.d.ts.map +1 -1
- package/dist/kv-scan.js +8 -0
- package/dist/kv-scan.js.map +1 -1
- package/dist/lease.d.ts +12 -5
- package/dist/lease.d.ts.map +1 -1
- package/dist/lease.js.map +1 -1
- package/dist/lifecycle-saga.d.ts +174 -0
- package/dist/lifecycle-saga.d.ts.map +1 -0
- package/dist/lifecycle-saga.js +418 -0
- package/dist/lifecycle-saga.js.map +1 -0
- package/dist/lifecycle-state.d.ts +202 -0
- package/dist/lifecycle-state.d.ts.map +1 -0
- package/dist/lifecycle-state.js +347 -0
- package/dist/lifecycle-state.js.map +1 -0
- package/dist/parts.d.ts +13 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +17 -0
- package/dist/parts.js.map +1 -0
- package/dist/provision.d.ts +124 -15
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +578 -121
- package/dist/provision.js.map +1 -1
- package/dist/schema-profile.d.ts +3 -2
- package/dist/schema-profile.d.ts.map +1 -1
- package/dist/schema-profile.js +242 -18
- package/dist/schema-profile.js.map +1 -1
- package/dist/session-browser.d.ts +15 -0
- package/dist/session-browser.d.ts.map +1 -0
- package/dist/session-browser.js +14 -0
- package/dist/session-browser.js.map +1 -0
- package/dist/session-terminal-frames.d.ts +36 -0
- package/dist/session-terminal-frames.d.ts.map +1 -0
- package/dist/session-terminal-frames.js +149 -0
- package/dist/session-terminal-frames.js.map +1 -0
- package/dist/spaces.d.ts.map +1 -1
- package/dist/spaces.js +6 -1
- package/dist/spaces.js.map +1 -1
- package/dist/streams.d.ts +168 -1
- package/dist/streams.d.ts.map +1 -1
- package/dist/streams.js +299 -21
- package/dist/streams.js.map +1 -1
- package/dist/subjects.d.ts +34 -36
- package/dist/subjects.d.ts.map +1 -1
- package/dist/subjects.js +46 -35
- package/dist/subjects.js.map +1 -1
- package/dist/types.d.ts +27 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
|
@@ -37,6 +37,15 @@ export declare function retirementFrontierStreams(space: string): string[];
|
|
|
37
37
|
* grant — and `allow_direct=false`: every fence on it is a leader-served revision-pinned CAS,
|
|
38
38
|
* and Direct Get's follower/mirror reads would defeat read-your-writes (§13.1). */
|
|
39
39
|
export declare function epAuthBucket(space: string): string;
|
|
40
|
+
/** The per-space SESSION ledger store (P2 item 6, §13.6): the `session.<id>` rows the manager's
|
|
41
|
+
* session plane CASes over. DEDICATED — split out of the auth bucket deliberately. KV reads are
|
|
42
|
+
* subject-BLIND (a `STREAM.MSG.GET` on a bucket serves any key, the campaign's known vector class),
|
|
43
|
+
* so co-locating session rows with credentials + gates would let the standing session-ledger cred read
|
|
44
|
+
* the whole control plane. A dedicated bucket makes that blind read STRUCTURALLY confined to session
|
|
45
|
+
* rows and nothing else. `allow_direct=false` for the SAME reason the auth bucket carries it: every
|
|
46
|
+
* ledger fence is a leader-served revision-pinned CAS (the one-use `createIssuing`, the finalize +
|
|
47
|
+
* terminal updates), and Direct Get's follower/mirror reads would defeat read-your-writes (§13.1). */
|
|
48
|
+
export declare function sessionsBucket(space: string): string;
|
|
40
49
|
/** EPJ duplicate window: the server MINIMUM (100 ms), set explicitly. A `0` is not accepted
|
|
41
50
|
* (it normalizes to the 120 s default), and native dedupe is deliberately NOT relied upon —
|
|
42
51
|
* submitters never set `Nats-Msg-Id`, and a wide window is exactly the cross-caller
|
|
@@ -75,13 +84,62 @@ export interface EndpointStreamOptions {
|
|
|
75
84
|
}
|
|
76
85
|
/**
|
|
77
86
|
* Create (idempotently) the §13.12 per-space control-surface resources: the seven JetStream
|
|
78
|
-
* streams, the work-pool WorkQueue, and the
|
|
79
|
-
* `jsm.streams.add`/`kvm.create` are idempotent for an identical
|
|
80
|
-
* config delta, which is wanted: a drifted resource is an operator error,
|
|
87
|
+
* streams, the work-pool WorkQueue, and the KV buckets (records + auth + the §13.6 session ledger).
|
|
88
|
+
* Privileged — runs at space setup. `jsm.streams.add`/`kvm.create` are idempotent for an identical
|
|
89
|
+
* config and FAIL LOUD on a config delta, which is wanted: a drifted resource is an operator error,
|
|
90
|
+
* never silently adopted.
|
|
81
91
|
*
|
|
82
|
-
*
|
|
92
|
+
* The session byte SUBJECTS (`eps`) are deliberately absent: core-only, never captured (§13.12).
|
|
93
|
+
* Only the durable `session.<id>` ledger rows are captured — in their own dedicated bucket
|
|
94
|
+
* ({@link createSessionsStore}), never the auth bucket.
|
|
83
95
|
*/
|
|
84
96
|
export declare function createEndpointStreams(jsm: JetStreamManager, kvm: Kvm, space: string, opts?: EndpointStreamOptions): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Ensure the per-space CONTRACT store (EPC) exists with its normative shape (§13.7/§13.12) —
|
|
99
|
+
* content-addressed artifacts, one immutable message per digest subject, create-only mediated
|
|
100
|
+
* publication, NO age eviction (artifacts are permanent). allow_direct: the subject-scoped
|
|
101
|
+
* last-by-subject read IS the fetch path.
|
|
102
|
+
*
|
|
103
|
+
* PER-SUBJECT IMMUTABILITY is BROKER-ENFORCED, not left to publisher cooperation (the append-shadow
|
|
104
|
+
* blocker, live-confirmed by the panel). The create-only fence `Nats-Expected-Last-Subject-Sequence:
|
|
105
|
+
* 0` is a publisher-SET header the `epc.*` publish grant cannot compel, so a non-cooperative
|
|
106
|
+
* grant-holder could APPEND a second message to an already-published digest subject; `last_by_subj`
|
|
107
|
+
* would then return that shadow and a fail-closed read would make the honest artifact permanently
|
|
108
|
+
* unfetchable (deny_delete/deny_purge block recovery — an operator-reprovision-only DoS, and at
|
|
109
|
+
* §13.11's ep-only cut the SOLE contract path). The store closes this at the SOURCE:
|
|
110
|
+
* `max_msgs_per_subject: 1` + `discard: new` + `discard_new_per_subject: true` makes a second
|
|
111
|
+
* publish to an occupied digest subject BROKER-REJECTED (err 10077) regardless of headers, so a
|
|
112
|
+
* digest subject holds exactly one message forever. `deny_delete`/`deny_purge` then keep that one
|
|
113
|
+
* message from being removed. (The read path additionally defends itself version-agnostically —
|
|
114
|
+
* {@link fetchContractArtifact} prefers the create-only winner over any shadow — so a broker or a
|
|
115
|
+
* legacy stream that lacked the per-subject cap is still safe.)
|
|
116
|
+
*
|
|
117
|
+
* Create-or-verify-AND-harden, safe at every authority-daemon boot (the {@link
|
|
118
|
+
* ensureAuthorityStores} discipline): a fresh space gets the store created with the full shape; a
|
|
119
|
+
* CLEAN pre-existing store (incl. a pre-hardening one from an earlier release, OR the config-A
|
|
120
|
+
* footgun `max_msgs_per_subject:1` + `discard:old` that would DELETE the honest artifact) is UPDATED
|
|
121
|
+
* to the three config-B flags (idempotent, like the records store's rollup/deny update) and then
|
|
122
|
+
* VERIFIED - a shape that cannot be brought to config B FAILS LOUD, never silently adopted.
|
|
123
|
+
*
|
|
124
|
+
* The config-B upgrade ENFORCES per-subject immutability GOING FORWARD; it does NOT heal a shadow
|
|
125
|
+
* that predates it. Applying `max_msgs_per_subject:1` trims each subject to its newest message, so a
|
|
126
|
+
* legacy store that ALREADY holds a shadow (some digest subject with >1 message) would have the
|
|
127
|
+
* honest create-only winner trimmed away and the shadow cemented. Rather than silently cement it,
|
|
128
|
+
* the upgrade REFUSES LOUD on such a store (`messages > num_subjects`), so the operator reprovisions
|
|
129
|
+
* a clean store. This is a narrow guard: a fresh deploy is born at config B and never reaches it.
|
|
130
|
+
*
|
|
131
|
+
* FAIL-LOUD IS THE AGENT'S DEFENSE (critic completeness item): because this verify refuses to serve
|
|
132
|
+
* unless all three config-B flags are present, the manager NEVER serves an un-hardened EPC store —
|
|
133
|
+
* so a shadow-append cannot exist on any store an agent reads. That is why the ordinary agent
|
|
134
|
+
* baseline needs only the subject-scoped `last_by_subj` read (never the bare `next_by_subj` the
|
|
135
|
+
* shadow fallback uses): on every store the manager actually serves, `last_by_subj` always verifies
|
|
136
|
+
* and the fallback never triggers. The {@link fetchContractArtifact} create-only-winner fallback is
|
|
137
|
+
* defense-in-depth for the publisher path (the executor, which holds `next_by_subj`) and for a
|
|
138
|
+
* hypothetical un-hardened store the manager would refuse to serve anyway. On the pinned broker
|
|
139
|
+
* floor (nats-server >= 2.12, well past the 2.9 that added `discard_new_per_subject`) config B always
|
|
140
|
+
* lands; an older broker that ignores the flag is caught here and the daemon fails to start.
|
|
141
|
+
*/
|
|
142
|
+
export declare function ensureContractStore(jsm: JetStreamManager, space: string): Promise<void>;
|
|
85
143
|
/**
|
|
86
144
|
* Ensure the two per-space AUTHORITY stores exist with their normative shape (§13.12):
|
|
87
145
|
*
|
|
@@ -98,6 +156,18 @@ export declare function createEndpointStreams(jsm: JetStreamManager, kvm: Kvm, s
|
|
|
98
156
|
* silently adopted (§13.12: the flags are load-bearing for deny-new and the barrier CAS fences).
|
|
99
157
|
*/
|
|
100
158
|
export declare function ensureAuthorityStores(jsm: JetStreamManager, kvm: Kvm, space: string): Promise<void>;
|
|
159
|
+
/** Create (idempotently) the per-space SESSION ledger store (P2 item 6, §13.6): the DEDICATED
|
|
160
|
+
* {@link sessionsBucket} the manager's session plane CASes `session.<id>` rows over. Kept OUT of
|
|
161
|
+
* {@link ensureAuthorityStores} deliberately — the auth path never touches session rows, and the
|
|
162
|
+
* manager provisions this store from its own boot — but it wears the SAME authority-store shape as
|
|
163
|
+
* the auth bucket: `allow_direct=false` (every ledger fence is a leader-served revision-pinned CAS,
|
|
164
|
+
* and Direct Get's follower reads would defeat read-your-writes, §13.1) plus the per-key TTL
|
|
165
|
+
* machinery (a terminal/expired session row can carry a delete-marker TTL). The dedication is the
|
|
166
|
+
* security substance: the standing session-ledger cred's `kv.get` is a bucket-blind body-selected read,
|
|
167
|
+
* and a bucket holding ONLY `session.>` rows makes that read expose nothing but session state — the
|
|
168
|
+
* structural fix for the §13.9 subject-blindness a shared auth bucket would carry (creds + gates).
|
|
169
|
+
* Create-or-verify, safe at every manager boot; a drifted store FAILS LOUD (§13.12). */
|
|
170
|
+
export declare function createSessionsStore(jsm: JetStreamManager, kvm: Kvm, space: string): Promise<void>;
|
|
101
171
|
/** `poolD = pool_<e>_<pool>` — parses uniquely from its LAST `_` because a pool token contains
|
|
102
172
|
* no `_` (`[a-z0-9-]`) while `<e>` may. */
|
|
103
173
|
export declare function poolDurable(endpoint: string, pool: string): string;
|
|
@@ -321,6 +391,60 @@ export declare function commitPrincipalGrants(space: string, endpoint: string, c
|
|
|
321
391
|
publish: string[];
|
|
322
392
|
subscribe: string[];
|
|
323
393
|
};
|
|
394
|
+
/** The SELF-MEDIATED GOAL-WRITER profile (P2 item 2 "spawn becomes an action"): a standing
|
|
395
|
+
* connection that both BINDS a goal at accept AND COMMITS its terminal, for an endpoint that
|
|
396
|
+
* accepts action goals INLINE on its ephemeral serve handler (Model B) rather than through a
|
|
397
|
+
* separate canonicalizer + effects executor. It is exactly {@link commitPrincipalGrants} (the
|
|
398
|
+
* `goal.*.*.*.*.result` terminal + `$KV.<records>.goal.<e>.>` record write + the two leader-served
|
|
399
|
+
* `STREAM.MSG.GET` fencing reads the substrate uses) PLUS the ONE row commitPrincipalGrants
|
|
400
|
+
* deliberately leaves to the canonicalizer — the goal `.bind` leaf
|
|
401
|
+
* (`epf.<e>.goal.*.*.*.*.bind`) — so this single principal owns the whole `accepted → terminal`
|
|
402
|
+
* goal-fact chain of its OWN endpoint. The endpoint's SERVE credential
|
|
403
|
+
* ({@link import("./endpoint-grants.js").epServePublishRows}) holds NONE of these: a serve
|
|
404
|
+
* connection is broker-DENIED every goal write, which is the item-2 privilege separation (the
|
|
405
|
+
* dedicated writer is minted on a distinct connection, the serve rails stay serve-only). All of
|
|
406
|
+
* commitPrincipalGrants' D32 residuals carry unchanged (payload-blind create-only publish; raw
|
|
407
|
+
* `$KV` cannot enforce the per-key CAS the substrate layers on; the two body-selected
|
|
408
|
+
* `STREAM.MSG.GET` reads expose EPF + records space-wide). The reply inbox is connection-scoped
|
|
409
|
+
* (`_INBOX_<connId>.>`). The `eff`/`wrk`/`receipt`/`cp`/`lease` families in the commit-principal
|
|
410
|
+
* base are inert for a goal-only endpoint (the manager writes none) but are the commit-principal
|
|
411
|
+
* profile's standard ceiling; a tighter goal-only ceiling is a follow-up if the panel prefers it. */
|
|
412
|
+
export declare function goalWriterGrants(space: string, endpoint: string, connId: string): {
|
|
413
|
+
publish: string[];
|
|
414
|
+
subscribe: string[];
|
|
415
|
+
};
|
|
416
|
+
/** The manager's SESSION-LEDGER rows (P2 item 6): the standing connection that owns the §13.6
|
|
417
|
+
* session ledger and NOTHING else. It holds NO session rail — not the wildcard it used to hold,
|
|
418
|
+
* not an exact one. That is the whole point of the split.
|
|
419
|
+
*
|
|
420
|
+
* §13.6 gives the ledger a job the byte rails do not have: it is "a DURABLE named authority that
|
|
421
|
+
* survives the serving endpoint", the thing that still knows what to revoke after the endpoint
|
|
422
|
+
* serving a session is gone. So it is standing and renewable, while the rails it records are
|
|
423
|
+
* per-session, exact-subject, and die with their session ({@link import("./provision.js").Profile}
|
|
424
|
+
* `session-serving` / `session-caller`). An earlier revision fused the two into one standing
|
|
425
|
+
* credential carrying `eps.<endpoint>.*.<epoch>.{in,out}`, which contradicted §13.9:2526 ("no
|
|
426
|
+
* standing EPS grant exists on either side") and let one credential read and write every live
|
|
427
|
+
* session's bytes at that epoch. Splitting on the lifetime boundary is what removes the wildcard:
|
|
428
|
+
* the standing half no longer has rails to widen.
|
|
429
|
+
*
|
|
430
|
+
* Its store is the DEDICATED {@link sessionsBucket}, NOT the auth bucket. The write is
|
|
431
|
+
* `$KV.<sessions>.session.*` (create-only CAS + revision-pinned update; `sessionLedgerKey` is the
|
|
432
|
+
* single-token `session.<id>`). The read is a bucket-blind leader `STREAM.MSG.GET` (allow_direct=
|
|
433
|
+
* false, so `kv.get` is a body-selected read that cannot be key-pinned) — but the dedicated bucket
|
|
434
|
+
* holds ONLY `session.>` rows, so that blind read exposes nothing but session ledger state,
|
|
435
|
+
* structurally closing the §13.9 subject-blindness the auth bucket carries (creds + gates). The
|
|
436
|
+
* reply inbox is connection-scoped. NO auth-bucket, records-bucket, or messaging-plane grant.
|
|
437
|
+
*
|
|
438
|
+
* NAMED RESIDUAL: `session.*` is one token wide and carries no endpoint component, because a
|
|
439
|
+
* `sessionId` is an opaque unguessable token with no endpoint inside it. So this credential can
|
|
440
|
+
* read and CAS any session row in its space, including another endpoint's. That was equally true
|
|
441
|
+
* of the credential it replaces; it is not a regression, and it is confined to ledger STATE — row
|
|
442
|
+
* state and credential ids — never to session bytes, which now require a per-session credential
|
|
443
|
+
* this profile cannot mint. */
|
|
444
|
+
export declare function sessionLedgerGrants(space: string, connId: string): {
|
|
445
|
+
publish: string[];
|
|
446
|
+
subscribe: string[];
|
|
447
|
+
};
|
|
324
448
|
/** The CONTRACT PUBLISHER principal's rows (§13.9 matrix "Contract-artifact publication" +
|
|
325
449
|
* the trusted-infra half of "Contract-artifact read"): publish `epc.*` (the digest-hex is ONE
|
|
326
450
|
* subject token; create-only rides `Nats-Expected-Last-Subject-Sequence: 0` at the typed path,
|
|
@@ -332,10 +456,16 @@ export declare function commitPrincipalGrants(space: string, endpoint: string, c
|
|
|
332
456
|
* connection-scoped (`_INBOX_<connId>.>`, never the account-wide default).
|
|
333
457
|
*
|
|
334
458
|
* D32 residuals, EXPLICIT: (1) the `epc.*` publish is payload-blind — a compromised publisher
|
|
335
|
-
* can
|
|
336
|
-
* non-canonical or digest-mismatched bytes,
|
|
337
|
-
*
|
|
338
|
-
*
|
|
459
|
+
* can publish garbage artifacts at NEW (previously-unused) digest subjects (verify-on-read refuses
|
|
460
|
+
* to SERVE non-canonical or digest-mismatched bytes, so this is a bounded storage flood carrying
|
|
461
|
+
* no authority). It can NOT overwrite, shadow, or replace an EXISTING published artifact: the EPC
|
|
462
|
+
* store's shape ({@link ensureContractStore}: `max_msgs_per_subject:1` + discard-new-per-subject)
|
|
463
|
+
* makes a second publish to an occupied digest subject broker-REJECTED regardless of the
|
|
464
|
+
* create-only header, so per-subject immutability is broker-enforced, not publisher-cooperative.
|
|
465
|
+
* (Earlier revisions relied on the cooperative create-only header alone and a fail-closed read,
|
|
466
|
+
* which the panel's live repro showed a non-cooperative publisher could defeat by raw append — a
|
|
467
|
+
* permanent shadow-DoS; the stream shape + the create-only-winner read fallback close it.) (2) the
|
|
468
|
+
* raw JS API request carries a caller-selected reply subject (the same confused-deputy injection
|
|
339
469
|
* class as every API-holding profile). */
|
|
340
470
|
export declare function contractPublisherGrants(space: string, connId: string): {
|
|
341
471
|
publish: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint-binding.d.ts","sourceRoot":"","sources":["../src/endpoint-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"endpoint-binding.d.ts","sourceRoot":"","sources":["../src/endpoint-binding.ts"],"names":[],"mappings":"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"}
|
package/dist/endpoint-binding.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* created by exactly one trusted principal (provisioner or the owning infra principal) and the
|
|
10
10
|
* §13.9 grant rows are generated against these same names and filters.
|
|
11
11
|
*/
|
|
12
|
-
import { AckPolicy, DeliverPolicy, RetentionPolicy, StorageType, } from "@nats-io/jetstream";
|
|
12
|
+
import { AckPolicy, DeliverPolicy, DiscardPolicy, RetentionPolicy, StorageType, } from "@nats-io/jetstream";
|
|
13
13
|
import { nanos } from "@nats-io/transport-node";
|
|
14
14
|
import { spacePrefix, token, assertInboxConnId } from "./subjects.js";
|
|
15
15
|
import { endpointToken, assertIdToken, assertGrantId, assertPoolToken, assertLifecycleToken, callerTokens, } from "./endpoint-subjects.js";
|
|
@@ -44,6 +44,17 @@ export function retirementFrontierStreams(space) {
|
|
|
44
44
|
export function epAuthBucket(space) {
|
|
45
45
|
return `cotal_auth_${token(space)}`;
|
|
46
46
|
}
|
|
47
|
+
/** The per-space SESSION ledger store (P2 item 6, §13.6): the `session.<id>` rows the manager's
|
|
48
|
+
* session plane CASes over. DEDICATED — split out of the auth bucket deliberately. KV reads are
|
|
49
|
+
* subject-BLIND (a `STREAM.MSG.GET` on a bucket serves any key, the campaign's known vector class),
|
|
50
|
+
* so co-locating session rows with credentials + gates would let the standing session-ledger cred read
|
|
51
|
+
* the whole control plane. A dedicated bucket makes that blind read STRUCTURALLY confined to session
|
|
52
|
+
* rows and nothing else. `allow_direct=false` for the SAME reason the auth bucket carries it: every
|
|
53
|
+
* ledger fence is a leader-served revision-pinned CAS (the one-use `createIssuing`, the finalize +
|
|
54
|
+
* terminal updates), and Direct Get's follower/mirror reads would defeat read-your-writes (§13.1). */
|
|
55
|
+
export function sessionsBucket(space) {
|
|
56
|
+
return `cotal_sessions_${token(space)}`;
|
|
57
|
+
}
|
|
47
58
|
// ---- §13.12 retention knobs (documented defaults, overridable per space policy) ----
|
|
48
59
|
/** EPJ duplicate window: the server MINIMUM (100 ms), set explicitly. A `0` is not accepted
|
|
49
60
|
* (it normalizes to the 120 s default), and native dedupe is deliberately NOT relied upon —
|
|
@@ -70,11 +81,14 @@ export const EP_TIMER_MAX_AGE_MS = 31 * 24 * 60 * 60 * 1000;
|
|
|
70
81
|
export const EP_AUTH_MARKER_TTL_MS = 60 * 60 * 1000;
|
|
71
82
|
/**
|
|
72
83
|
* Create (idempotently) the §13.12 per-space control-surface resources: the seven JetStream
|
|
73
|
-
* streams, the work-pool WorkQueue, and the
|
|
74
|
-
* `jsm.streams.add`/`kvm.create` are idempotent for an identical
|
|
75
|
-
* config delta, which is wanted: a drifted resource is an operator error,
|
|
84
|
+
* streams, the work-pool WorkQueue, and the KV buckets (records + auth + the §13.6 session ledger).
|
|
85
|
+
* Privileged — runs at space setup. `jsm.streams.add`/`kvm.create` are idempotent for an identical
|
|
86
|
+
* config and FAIL LOUD on a config delta, which is wanted: a drifted resource is an operator error,
|
|
87
|
+
* never silently adopted.
|
|
76
88
|
*
|
|
77
|
-
*
|
|
89
|
+
* The session byte SUBJECTS (`eps`) are deliberately absent: core-only, never captured (§13.12).
|
|
90
|
+
* Only the durable `session.<id>` ledger rows are captured — in their own dedicated bucket
|
|
91
|
+
* ({@link createSessionsStore}), never the auth bucket.
|
|
78
92
|
*/
|
|
79
93
|
export async function createEndpointStreams(jsm, kvm, space, opts = {}) {
|
|
80
94
|
const p = spacePrefix(space);
|
|
@@ -148,24 +162,109 @@ export async function createEndpointStreams(jsm, kvm, space, opts = {}) {
|
|
|
148
162
|
storage: StorageType.File,
|
|
149
163
|
allow_direct: false,
|
|
150
164
|
});
|
|
151
|
-
|
|
152
|
-
// create-only mediated publication, NO age eviction (artifacts are permanent). allow_direct:
|
|
153
|
-
// the subject-scoped last-by-subject read IS the fetch path. Permanence is BROKER-ENFORCED,
|
|
154
|
-
// not just configured-by-omission: deny_delete/deny_purge reject the message-delete and purge
|
|
155
|
-
// APIs even from a stream-API-holding principal, so a digest subject cannot be emptied and
|
|
156
|
-
// re-created through them. Per §13.12 the flags alone are NOT the whole claim: permanence is
|
|
157
|
-
// their COMBINATION with the retention floor (no age eviction, no teardown), verify-on-read
|
|
158
|
-
// pinning WHAT a subject carries, and stream management held by no profile.
|
|
159
|
-
await jsm.streams.add({
|
|
160
|
-
name: epcStreamName(space),
|
|
161
|
-
subjects: [`${p}.epc.>`],
|
|
162
|
-
retention: RetentionPolicy.Limits,
|
|
163
|
-
storage: StorageType.File,
|
|
164
|
-
allow_direct: true,
|
|
165
|
-
deny_delete: true,
|
|
166
|
-
deny_purge: true,
|
|
167
|
-
});
|
|
165
|
+
await ensureContractStore(jsm, space);
|
|
168
166
|
await ensureAuthorityStores(jsm, kvm, space);
|
|
167
|
+
// P2 item 6: the DEDICATED §13.6 session ledger bucket. The eps byte SUBJECTS stay core-only and
|
|
168
|
+
// uncaptured (above), but the `session.<id>` ledger rows are a captured authority KV — kept in
|
|
169
|
+
// their own bucket so the manager's standing session-ledger cred's bucket-blind STREAM.MSG.GET reads
|
|
170
|
+
// ONLY session rows (the §13.9 subject-blindness structural fix). Provisioned here so every mesh
|
|
171
|
+
// that ensures the endpoint streams (auth + open both run this at the manager's boot) has it.
|
|
172
|
+
await createSessionsStore(jsm, kvm, space);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Ensure the per-space CONTRACT store (EPC) exists with its normative shape (§13.7/§13.12) —
|
|
176
|
+
* content-addressed artifacts, one immutable message per digest subject, create-only mediated
|
|
177
|
+
* publication, NO age eviction (artifacts are permanent). allow_direct: the subject-scoped
|
|
178
|
+
* last-by-subject read IS the fetch path.
|
|
179
|
+
*
|
|
180
|
+
* PER-SUBJECT IMMUTABILITY is BROKER-ENFORCED, not left to publisher cooperation (the append-shadow
|
|
181
|
+
* blocker, live-confirmed by the panel). The create-only fence `Nats-Expected-Last-Subject-Sequence:
|
|
182
|
+
* 0` is a publisher-SET header the `epc.*` publish grant cannot compel, so a non-cooperative
|
|
183
|
+
* grant-holder could APPEND a second message to an already-published digest subject; `last_by_subj`
|
|
184
|
+
* would then return that shadow and a fail-closed read would make the honest artifact permanently
|
|
185
|
+
* unfetchable (deny_delete/deny_purge block recovery — an operator-reprovision-only DoS, and at
|
|
186
|
+
* §13.11's ep-only cut the SOLE contract path). The store closes this at the SOURCE:
|
|
187
|
+
* `max_msgs_per_subject: 1` + `discard: new` + `discard_new_per_subject: true` makes a second
|
|
188
|
+
* publish to an occupied digest subject BROKER-REJECTED (err 10077) regardless of headers, so a
|
|
189
|
+
* digest subject holds exactly one message forever. `deny_delete`/`deny_purge` then keep that one
|
|
190
|
+
* message from being removed. (The read path additionally defends itself version-agnostically —
|
|
191
|
+
* {@link fetchContractArtifact} prefers the create-only winner over any shadow — so a broker or a
|
|
192
|
+
* legacy stream that lacked the per-subject cap is still safe.)
|
|
193
|
+
*
|
|
194
|
+
* Create-or-verify-AND-harden, safe at every authority-daemon boot (the {@link
|
|
195
|
+
* ensureAuthorityStores} discipline): a fresh space gets the store created with the full shape; a
|
|
196
|
+
* CLEAN pre-existing store (incl. a pre-hardening one from an earlier release, OR the config-A
|
|
197
|
+
* footgun `max_msgs_per_subject:1` + `discard:old` that would DELETE the honest artifact) is UPDATED
|
|
198
|
+
* to the three config-B flags (idempotent, like the records store's rollup/deny update) and then
|
|
199
|
+
* VERIFIED - a shape that cannot be brought to config B FAILS LOUD, never silently adopted.
|
|
200
|
+
*
|
|
201
|
+
* The config-B upgrade ENFORCES per-subject immutability GOING FORWARD; it does NOT heal a shadow
|
|
202
|
+
* that predates it. Applying `max_msgs_per_subject:1` trims each subject to its newest message, so a
|
|
203
|
+
* legacy store that ALREADY holds a shadow (some digest subject with >1 message) would have the
|
|
204
|
+
* honest create-only winner trimmed away and the shadow cemented. Rather than silently cement it,
|
|
205
|
+
* the upgrade REFUSES LOUD on such a store (`messages > num_subjects`), so the operator reprovisions
|
|
206
|
+
* a clean store. This is a narrow guard: a fresh deploy is born at config B and never reaches it.
|
|
207
|
+
*
|
|
208
|
+
* FAIL-LOUD IS THE AGENT'S DEFENSE (critic completeness item): because this verify refuses to serve
|
|
209
|
+
* unless all three config-B flags are present, the manager NEVER serves an un-hardened EPC store —
|
|
210
|
+
* so a shadow-append cannot exist on any store an agent reads. That is why the ordinary agent
|
|
211
|
+
* baseline needs only the subject-scoped `last_by_subj` read (never the bare `next_by_subj` the
|
|
212
|
+
* shadow fallback uses): on every store the manager actually serves, `last_by_subj` always verifies
|
|
213
|
+
* and the fallback never triggers. The {@link fetchContractArtifact} create-only-winner fallback is
|
|
214
|
+
* defense-in-depth for the publisher path (the executor, which holds `next_by_subj`) and for a
|
|
215
|
+
* hypothetical un-hardened store the manager would refuse to serve anyway. On the pinned broker
|
|
216
|
+
* floor (nats-server >= 2.12, well past the 2.9 that added `discard_new_per_subject`) config B always
|
|
217
|
+
* lands; an older broker that ignores the flag is caught here and the daemon fails to start.
|
|
218
|
+
*/
|
|
219
|
+
export async function ensureContractStore(jsm, space) {
|
|
220
|
+
const name = epcStreamName(space);
|
|
221
|
+
const subject = `${spacePrefix(space)}.epc.>`;
|
|
222
|
+
const hardening = { max_msgs_per_subject: 1, discard: DiscardPolicy.New, discard_new_per_subject: true };
|
|
223
|
+
if (await jsm.streams.info(name).catch(() => undefined) === undefined) {
|
|
224
|
+
await jsm.streams.add({
|
|
225
|
+
name,
|
|
226
|
+
subjects: [subject],
|
|
227
|
+
retention: RetentionPolicy.Limits,
|
|
228
|
+
storage: StorageType.File,
|
|
229
|
+
allow_direct: true,
|
|
230
|
+
deny_delete: true,
|
|
231
|
+
deny_purge: true,
|
|
232
|
+
...hardening,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Upgrade path: an EPC stream from a pre-hardening release lacks the per-subject cap. Add it
|
|
237
|
+
// idempotently (a live update of max_msgs_per_subject + discard settings is allowed and, tested,
|
|
238
|
+
// leaves a CLEAN store's artifacts intact). A stream that WON'T take the update surfaces at the
|
|
239
|
+
// verify below.
|
|
240
|
+
const info = await jsm.streams.info(name);
|
|
241
|
+
const cur = info.config;
|
|
242
|
+
if (cur.max_msgs_per_subject !== 1 || cur.discard !== DiscardPolicy.New || cur.discard_new_per_subject !== true) {
|
|
243
|
+
// HEAL-OR-FAIL, never cement (critic upgrade-trim residual): applying max_msgs_per_subject:1
|
|
244
|
+
// TRIMS each subject to its NEWEST message. On a CLEAN legacy store (create-only, ≤1 message
|
|
245
|
+
// per subject) that trims nothing. But if a legacy store ALREADY holds a shadow (a raw append
|
|
246
|
+
// left a subject with >1 message, from before this hardening existed), the trim would keep the
|
|
247
|
+
// NEWEST (the shadow) and DELETE the honest create-only winner - cementing the shadow
|
|
248
|
+
// permanently. `messages > num_subjects` is exactly "some subject has more than one message",
|
|
249
|
+
// so refuse the upgrade LOUD there: the operator reprovisions a clean store rather than the
|
|
250
|
+
// daemon silently trim-cementing a pre-existing shadow. (Fresh deploys are born at config B and
|
|
251
|
+
// never reach this; the check only guards a legacy store that was shadowed before its first
|
|
252
|
+
// config-B boot.)
|
|
253
|
+
const { messages, num_subjects } = info.state;
|
|
254
|
+
if (messages > 0 && (num_subjects === undefined || messages > num_subjects))
|
|
255
|
+
throw new Error(`the contract store ${name} is a pre-hardening stream that cannot be proven clean (${messages} messages across ${String(num_subjects)} subjects) - a subject holding more than one message means a shadow-append predates this immutability upgrade, and applying the per-subject cap would TRIM to the newest (shadow) message and delete the honest artifact; reprovision a clean store rather than cement the shadow (SPEC 13.7/13.12)`);
|
|
256
|
+
await jsm.streams.update(name, { ...cur, ...hardening });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const cfg = (await jsm.streams.info(name)).config;
|
|
260
|
+
if (cfg.allow_direct !== true || cfg.deny_delete !== true || cfg.deny_purge !== true)
|
|
261
|
+
throw new Error(`the contract store ${name} has a drifted shape (allow_direct=${String(cfg.allow_direct)}, deny_delete=${String(cfg.deny_delete)}, deny_purge=${String(cfg.deny_purge)}); an authority store is never silently adopted - reprovision it (SPEC 13.12)`);
|
|
262
|
+
if (cfg.max_msgs_per_subject !== 1 || cfg.discard !== DiscardPolicy.New || cfg.discard_new_per_subject !== true)
|
|
263
|
+
throw new Error(`the contract store ${name} does not enforce per-subject immutability (max_msgs_per_subject=${String(cfg.max_msgs_per_subject)}, discard=${String(cfg.discard)}, discard_new_per_subject=${String(cfg.discard_new_per_subject)}); a digest subject must hold exactly one broker-immutable message or an append-shadow can DoS the store - reprovision it (SPEC 13.7/13.12)`);
|
|
264
|
+
if (!Array.isArray(cfg.subjects) || cfg.subjects.length !== 1 || cfg.subjects[0] !== subject)
|
|
265
|
+
throw new Error(`the contract store ${name} does not carry exactly the subject ${subject} (got ${JSON.stringify(cfg.subjects)}); a stream that captures anything else is not the contract store - reprovision it (SPEC 13.12)`);
|
|
266
|
+
if (cfg.storage !== "file")
|
|
267
|
+
throw new Error(`the contract store ${name} has storage ${JSON.stringify(cfg.storage)}, not file; a non-durable contract store forgets permanent artifacts on restart - reprovision it (SPEC 13.12)`);
|
|
169
268
|
}
|
|
170
269
|
/**
|
|
171
270
|
* Ensure the two per-space AUTHORITY stores exist with their normative shape (§13.12):
|
|
@@ -209,6 +308,27 @@ export async function ensureAuthorityStores(jsm, kvm, space) {
|
|
|
209
308
|
throw new Error(`the auth store ${authBucket} has a drifted shape (allow_direct=${String(authCfg.allow_direct)}, allow_msg_ttl=${String(authCfg.allow_msg_ttl)}); an authority store is never silently adopted - reprovision it (SPEC 13.12)`);
|
|
210
309
|
assertAuthorityStoreBinding(authCfg, authBucket);
|
|
211
310
|
}
|
|
311
|
+
/** Create (idempotently) the per-space SESSION ledger store (P2 item 6, §13.6): the DEDICATED
|
|
312
|
+
* {@link sessionsBucket} the manager's session plane CASes `session.<id>` rows over. Kept OUT of
|
|
313
|
+
* {@link ensureAuthorityStores} deliberately — the auth path never touches session rows, and the
|
|
314
|
+
* manager provisions this store from its own boot — but it wears the SAME authority-store shape as
|
|
315
|
+
* the auth bucket: `allow_direct=false` (every ledger fence is a leader-served revision-pinned CAS,
|
|
316
|
+
* and Direct Get's follower reads would defeat read-your-writes, §13.1) plus the per-key TTL
|
|
317
|
+
* machinery (a terminal/expired session row can carry a delete-marker TTL). The dedication is the
|
|
318
|
+
* security substance: the standing session-ledger cred's `kv.get` is a bucket-blind body-selected read,
|
|
319
|
+
* and a bucket holding ONLY `session.>` rows makes that read expose nothing but session state — the
|
|
320
|
+
* structural fix for the §13.9 subject-blindness a shared auth bucket would carry (creds + gates).
|
|
321
|
+
* Create-or-verify, safe at every manager boot; a drifted store FAILS LOUD (§13.12). */
|
|
322
|
+
export async function createSessionsStore(jsm, kvm, space) {
|
|
323
|
+
const bucket = sessionsBucket(space);
|
|
324
|
+
const stream = `KV_${bucket}`;
|
|
325
|
+
if (await jsm.streams.info(stream).catch(() => undefined) === undefined)
|
|
326
|
+
await kvm.create(bucket, { allow_direct: false, markerTTL: EP_AUTH_MARKER_TTL_MS });
|
|
327
|
+
const cfg = (await jsm.streams.info(stream)).config;
|
|
328
|
+
if (cfg.allow_direct !== false || cfg.allow_msg_ttl !== true)
|
|
329
|
+
throw new Error(`the sessions store ${bucket} has a drifted shape (allow_direct=${String(cfg.allow_direct)}, allow_msg_ttl=${String(cfg.allow_msg_ttl)}); an authority store is never silently adopted - reprovision it (SPEC 13.12)`);
|
|
330
|
+
assertAuthorityStoreBinding(cfg, bucket);
|
|
331
|
+
}
|
|
212
332
|
/** The store-BINDING half of the verify (SPEC 13.12): a stream wearing an authority bucket's name
|
|
213
333
|
* must BE that KV bucket — exactly the one `$KV.<bucket>.>` subject (an extra captured subject
|
|
214
334
|
* would put foreign bodies inside every body-selected MSG.GET grant on the stream) and durable
|
|
@@ -712,6 +832,9 @@ export function commitPrincipalGrants(space, endpoint, connId) {
|
|
|
712
832
|
const p = spacePrefix(space);
|
|
713
833
|
const records = recordsBucket(space);
|
|
714
834
|
const publish = [
|
|
835
|
+
// ONE terminal subject per goal (SPEC:1394, the §13.9 "Result/receipt/terminal/resume facts"
|
|
836
|
+
// row): the exact-arity `…result` leaf, never an `…result.*` epoch-scoped variant — a per-epoch
|
|
837
|
+
// subject hid a legitimate pre-restart winner from every reader.
|
|
715
838
|
`${p}.epf.${e}.goal.*.*.*.*.result`,
|
|
716
839
|
`${p}.epf.${e}.eff.>`,
|
|
717
840
|
`${p}.epf.${e}.receipt.>`,
|
|
@@ -726,6 +849,84 @@ export function commitPrincipalGrants(space, endpoint, connId) {
|
|
|
726
849
|
];
|
|
727
850
|
return { publish, subscribe: [`_INBOX_${assertInboxConnId(connId)}.>`] };
|
|
728
851
|
}
|
|
852
|
+
/** The SELF-MEDIATED GOAL-WRITER profile (P2 item 2 "spawn becomes an action"): a standing
|
|
853
|
+
* connection that both BINDS a goal at accept AND COMMITS its terminal, for an endpoint that
|
|
854
|
+
* accepts action goals INLINE on its ephemeral serve handler (Model B) rather than through a
|
|
855
|
+
* separate canonicalizer + effects executor. It is exactly {@link commitPrincipalGrants} (the
|
|
856
|
+
* `goal.*.*.*.*.result` terminal + `$KV.<records>.goal.<e>.>` record write + the two leader-served
|
|
857
|
+
* `STREAM.MSG.GET` fencing reads the substrate uses) PLUS the ONE row commitPrincipalGrants
|
|
858
|
+
* deliberately leaves to the canonicalizer — the goal `.bind` leaf
|
|
859
|
+
* (`epf.<e>.goal.*.*.*.*.bind`) — so this single principal owns the whole `accepted → terminal`
|
|
860
|
+
* goal-fact chain of its OWN endpoint. The endpoint's SERVE credential
|
|
861
|
+
* ({@link import("./endpoint-grants.js").epServePublishRows}) holds NONE of these: a serve
|
|
862
|
+
* connection is broker-DENIED every goal write, which is the item-2 privilege separation (the
|
|
863
|
+
* dedicated writer is minted on a distinct connection, the serve rails stay serve-only). All of
|
|
864
|
+
* commitPrincipalGrants' D32 residuals carry unchanged (payload-blind create-only publish; raw
|
|
865
|
+
* `$KV` cannot enforce the per-key CAS the substrate layers on; the two body-selected
|
|
866
|
+
* `STREAM.MSG.GET` reads expose EPF + records space-wide). The reply inbox is connection-scoped
|
|
867
|
+
* (`_INBOX_<connId>.>`). The `eff`/`wrk`/`receipt`/`cp`/`lease` families in the commit-principal
|
|
868
|
+
* base are inert for a goal-only endpoint (the manager writes none) but are the commit-principal
|
|
869
|
+
* profile's standard ceiling; a tighter goal-only ceiling is a follow-up if the panel prefers it. */
|
|
870
|
+
export function goalWriterGrants(space, endpoint, connId) {
|
|
871
|
+
const base = commitPrincipalGrants(space, endpoint, connId);
|
|
872
|
+
const e = endpointToken(endpoint);
|
|
873
|
+
const bindLeaf = `${spacePrefix(space)}.epf.${e}.goal.*.*.*.*.bind`;
|
|
874
|
+
// must-5 Q-B — the reconcile index: the goal-writer records each accepted goal under
|
|
875
|
+
// `goalidx.<e>.<caller triple>.<goalId>` (create-only) before the bind and deletes it at the
|
|
876
|
+
// terminal, so a successor incarnation can settle orphaned goals. Key-pinned to THIS endpoint's
|
|
877
|
+
// index subtree; the goal-writer holds NO records CONSUMER.CREATE (the boot sweep enumerates the
|
|
878
|
+
// index over the PROVISIONER, never this standing connection).
|
|
879
|
+
const indexRow = `$KV.${recordsBucket(space)}.goalidx.${e}.>`;
|
|
880
|
+
// must-5 (a) — the own-gate currency belt: the manager reads its OWN issuance gate
|
|
881
|
+
// (`epgate.<e>.<iid>`) over this connection before the first-terminal-fact CAS and skips a
|
|
882
|
+
// superseded commit. The auth store is `allow_direct=false`, so the read is a body-selected
|
|
883
|
+
// leader `STREAM.MSG.GET` that cannot be key-pinned to the single gate key — the SAME residual
|
|
884
|
+
// class the `endpoint-serve-executor` carries (reads any auth-bucket row = gate + ledger
|
|
885
|
+
// metadata, never bearer bytes), here on a standing rather than one-shot connection. The manager
|
|
886
|
+
// reads ONLY `epgate.<e>.<iid>`; (a) is the fast-fail belt, (b) barrier-revoke is the durable fence.
|
|
887
|
+
const gateRead = `${JSAPI}.STREAM.MSG.GET.KV_${epAuthBucket(space)}`;
|
|
888
|
+
return { publish: [bindLeaf, indexRow, gateRead, ...base.publish], subscribe: base.subscribe };
|
|
889
|
+
}
|
|
890
|
+
/** The manager's SESSION-LEDGER rows (P2 item 6): the standing connection that owns the §13.6
|
|
891
|
+
* session ledger and NOTHING else. It holds NO session rail — not the wildcard it used to hold,
|
|
892
|
+
* not an exact one. That is the whole point of the split.
|
|
893
|
+
*
|
|
894
|
+
* §13.6 gives the ledger a job the byte rails do not have: it is "a DURABLE named authority that
|
|
895
|
+
* survives the serving endpoint", the thing that still knows what to revoke after the endpoint
|
|
896
|
+
* serving a session is gone. So it is standing and renewable, while the rails it records are
|
|
897
|
+
* per-session, exact-subject, and die with their session ({@link import("./provision.js").Profile}
|
|
898
|
+
* `session-serving` / `session-caller`). An earlier revision fused the two into one standing
|
|
899
|
+
* credential carrying `eps.<endpoint>.*.<epoch>.{in,out}`, which contradicted §13.9:2526 ("no
|
|
900
|
+
* standing EPS grant exists on either side") and let one credential read and write every live
|
|
901
|
+
* session's bytes at that epoch. Splitting on the lifetime boundary is what removes the wildcard:
|
|
902
|
+
* the standing half no longer has rails to widen.
|
|
903
|
+
*
|
|
904
|
+
* Its store is the DEDICATED {@link sessionsBucket}, NOT the auth bucket. The write is
|
|
905
|
+
* `$KV.<sessions>.session.*` (create-only CAS + revision-pinned update; `sessionLedgerKey` is the
|
|
906
|
+
* single-token `session.<id>`). The read is a bucket-blind leader `STREAM.MSG.GET` (allow_direct=
|
|
907
|
+
* false, so `kv.get` is a body-selected read that cannot be key-pinned) — but the dedicated bucket
|
|
908
|
+
* holds ONLY `session.>` rows, so that blind read exposes nothing but session ledger state,
|
|
909
|
+
* structurally closing the §13.9 subject-blindness the auth bucket carries (creds + gates). The
|
|
910
|
+
* reply inbox is connection-scoped. NO auth-bucket, records-bucket, or messaging-plane grant.
|
|
911
|
+
*
|
|
912
|
+
* NAMED RESIDUAL: `session.*` is one token wide and carries no endpoint component, because a
|
|
913
|
+
* `sessionId` is an opaque unguessable token with no endpoint inside it. So this credential can
|
|
914
|
+
* read and CAS any session row in its space, including another endpoint's. That was equally true
|
|
915
|
+
* of the credential it replaces; it is not a regression, and it is confined to ledger STATE — row
|
|
916
|
+
* state and credential ids — never to session bytes, which now require a per-session credential
|
|
917
|
+
* this profile cannot mint. */
|
|
918
|
+
export function sessionLedgerGrants(space, connId) {
|
|
919
|
+
const SESS = sessionsBucket(space);
|
|
920
|
+
return {
|
|
921
|
+
publish: [
|
|
922
|
+
`$KV.${SESS}.session.*`, // ledger create/update — single-token key (`session.<id>`)
|
|
923
|
+
`${JSAPI}.STREAM.MSG.GET.KV_${SESS}`, // kv.get: leader-served body-selected read, confined to the sessions bucket
|
|
924
|
+
`${JSAPI}.STREAM.INFO.KV_${SESS}`, // Kvm bind probe (§13.12)
|
|
925
|
+
`${JSAPI}.INFO`, // JS-API context info (KV client)
|
|
926
|
+
],
|
|
927
|
+
subscribe: [`_INBOX_${assertInboxConnId(connId)}.>`], // connection-scoped reply inbox
|
|
928
|
+
};
|
|
929
|
+
}
|
|
729
930
|
/** The CONTRACT PUBLISHER principal's rows (§13.9 matrix "Contract-artifact publication" +
|
|
730
931
|
* the trusted-infra half of "Contract-artifact read"): publish `epc.*` (the digest-hex is ONE
|
|
731
932
|
* subject token; create-only rides `Nats-Expected-Last-Subject-Sequence: 0` at the typed path,
|
|
@@ -737,10 +938,16 @@ export function commitPrincipalGrants(space, endpoint, connId) {
|
|
|
737
938
|
* connection-scoped (`_INBOX_<connId>.>`, never the account-wide default).
|
|
738
939
|
*
|
|
739
940
|
* D32 residuals, EXPLICIT: (1) the `epc.*` publish is payload-blind — a compromised publisher
|
|
740
|
-
* can
|
|
741
|
-
* non-canonical or digest-mismatched bytes,
|
|
742
|
-
*
|
|
743
|
-
*
|
|
941
|
+
* can publish garbage artifacts at NEW (previously-unused) digest subjects (verify-on-read refuses
|
|
942
|
+
* to SERVE non-canonical or digest-mismatched bytes, so this is a bounded storage flood carrying
|
|
943
|
+
* no authority). It can NOT overwrite, shadow, or replace an EXISTING published artifact: the EPC
|
|
944
|
+
* store's shape ({@link ensureContractStore}: `max_msgs_per_subject:1` + discard-new-per-subject)
|
|
945
|
+
* makes a second publish to an occupied digest subject broker-REJECTED regardless of the
|
|
946
|
+
* create-only header, so per-subject immutability is broker-enforced, not publisher-cooperative.
|
|
947
|
+
* (Earlier revisions relied on the cooperative create-only header alone and a fail-closed read,
|
|
948
|
+
* which the panel's live repro showed a non-cooperative publisher could defeat by raw append — a
|
|
949
|
+
* permanent shadow-DoS; the stream shape + the create-only-winner read fallback close it.) (2) the
|
|
950
|
+
* raw JS API request carries a caller-selected reply subject (the same confused-deputy injection
|
|
744
951
|
* class as every API-holding profile). */
|
|
745
952
|
export function contractPublisherGrants(space, connId) {
|
|
746
953
|
const publish = [
|