@cotal-ai/auth 0.12.0 → 0.13.1

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.
Files changed (95) hide show
  1. package/dist/admission-mediator.d.ts +319 -0
  2. package/dist/admission-mediator.d.ts.map +1 -0
  3. package/dist/admission-mediator.js +1033 -0
  4. package/dist/admission-mediator.js.map +1 -0
  5. package/dist/auth-admin.d.ts +29 -0
  6. package/dist/auth-admin.d.ts.map +1 -0
  7. package/dist/auth-admin.js +224 -0
  8. package/dist/auth-admin.js.map +1 -0
  9. package/dist/authority-client.d.ts +215 -0
  10. package/dist/authority-client.d.ts.map +1 -0
  11. package/dist/authority-client.js +346 -0
  12. package/dist/authority-client.js.map +1 -0
  13. package/dist/barrier-evict.d.ts +16 -0
  14. package/dist/barrier-evict.d.ts.map +1 -0
  15. package/dist/barrier-evict.js +100 -0
  16. package/dist/barrier-evict.js.map +1 -0
  17. package/dist/connect-reader.d.ts +72 -0
  18. package/dist/connect-reader.d.ts.map +1 -0
  19. package/dist/connect-reader.js +154 -0
  20. package/dist/connect-reader.js.map +1 -0
  21. package/dist/continuity.d.ts.map +1 -1
  22. package/dist/continuity.js +3 -0
  23. package/dist/continuity.js.map +1 -1
  24. package/dist/credential-ledger.d.ts +343 -0
  25. package/dist/credential-ledger.d.ts.map +1 -0
  26. package/dist/credential-ledger.js +732 -0
  27. package/dist/credential-ledger.js.map +1 -0
  28. package/dist/drain-repair.d.ts +95 -0
  29. package/dist/drain-repair.d.ts.map +1 -0
  30. package/dist/drain-repair.js +305 -0
  31. package/dist/drain-repair.js.map +1 -0
  32. package/dist/idp.d.ts +19 -0
  33. package/dist/idp.d.ts.map +1 -1
  34. package/dist/idp.js +17 -0
  35. package/dist/idp.js.map +1 -1
  36. package/dist/index.d.ts +5 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +29 -2
  39. package/dist/index.js.map +1 -1
  40. package/dist/issuer.d.ts +8 -0
  41. package/dist/issuer.d.ts.map +1 -1
  42. package/dist/issuer.js +4 -2
  43. package/dist/issuer.js.map +1 -1
  44. package/dist/ledger-scanner.d.ts +98 -0
  45. package/dist/ledger-scanner.d.ts.map +1 -0
  46. package/dist/ledger-scanner.js +352 -0
  47. package/dist/ledger-scanner.js.map +1 -0
  48. package/dist/ledger.d.ts +7 -0
  49. package/dist/ledger.d.ts.map +1 -1
  50. package/dist/ledger.js +53 -5
  51. package/dist/ledger.js.map +1 -1
  52. package/dist/lifecycle-registry.d.ts +397 -0
  53. package/dist/lifecycle-registry.d.ts.map +1 -0
  54. package/dist/lifecycle-registry.js +819 -0
  55. package/dist/lifecycle-registry.js.map +1 -0
  56. package/dist/permissions.d.ts +5 -1
  57. package/dist/permissions.d.ts.map +1 -1
  58. package/dist/permissions.js +29 -6
  59. package/dist/permissions.js.map +1 -1
  60. package/dist/plane-claim.d.ts +83 -0
  61. package/dist/plane-claim.d.ts.map +1 -0
  62. package/dist/plane-claim.js +290 -0
  63. package/dist/plane-claim.js.map +1 -0
  64. package/dist/provider.d.ts.map +1 -1
  65. package/dist/provider.js +2 -1
  66. package/dist/provider.js.map +1 -1
  67. package/dist/records-scanner.d.ts +78 -0
  68. package/dist/records-scanner.d.ts.map +1 -0
  69. package/dist/records-scanner.js +320 -0
  70. package/dist/records-scanner.js.map +1 -0
  71. package/dist/retirement-barrier.d.ts +217 -0
  72. package/dist/retirement-barrier.d.ts.map +1 -0
  73. package/dist/retirement-barrier.js +624 -0
  74. package/dist/retirement-barrier.js.map +1 -0
  75. package/dist/retirement-cleaner.d.ts +54 -0
  76. package/dist/retirement-cleaner.d.ts.map +1 -0
  77. package/dist/retirement-cleaner.js +158 -0
  78. package/dist/retirement-cleaner.js.map +1 -0
  79. package/dist/root-credential.d.ts +26 -0
  80. package/dist/root-credential.d.ts.map +1 -0
  81. package/dist/root-credential.js +116 -0
  82. package/dist/root-credential.js.map +1 -0
  83. package/dist/service.d.ts +60 -0
  84. package/dist/service.d.ts.map +1 -1
  85. package/dist/service.js +317 -10
  86. package/dist/service.js.map +1 -1
  87. package/dist/session-ledger.d.ts +224 -0
  88. package/dist/session-ledger.d.ts.map +1 -0
  89. package/dist/session-ledger.js +713 -0
  90. package/dist/session-ledger.js.map +1 -0
  91. package/dist/token.d.ts +17 -0
  92. package/dist/token.d.ts.map +1 -1
  93. package/dist/token.js +30 -2
  94. package/dist/token.js.map +1 -1
  95. package/package.json +5 -3
@@ -0,0 +1,1033 @@
1
+ /**
2
+ * The D13 (4) ADMISSION MEDIATOR + admission-policy coordinate (SPEC §13.6/§13.8/§13.9, as
3
+ * amended): the per-endpoint mediated writer that owns the `oblig.` prefix — the ONE durable
4
+ * serialization coordinate on which a durable acceptance/start contends with its authority
5
+ * head's movement — plus the govern-head policy selector's stage/drain/promote protocol and
6
+ * the immutable `policy.<endpoint>.<digest-hex>` version publication.
7
+ *
8
+ * The §13.8 protocol, implemented here end to end:
9
+ * 1. CREATE-FENCE: immediately before the create, the mediator performs the FENCING currency
10
+ * reads it will pin — a leader-served read of the target's lifecycle head (REFUSING unless
11
+ * `active` at the presented uid) for a target-bound admission, and a leader-served read of
12
+ * the enforced policy under the govern head for a policy-admitted decision (REFUSING while
13
+ * a `pendingPolicy…` is staged: the endpoint is inside its drain window). It then creates
14
+ * the obligation row create-only at the deterministic acceptance-identity key; a create
15
+ * loser leader-reads the winner and JOINS only on the FULL pinned identity (an `epf` row on
16
+ * fingerprint + route; a `self` row on the ENTIRE commit intent), else `conflict`.
17
+ * 2. PROOF-GATED ADMISSION: after winning or joining, the mediator leader-reads the SAME
18
+ * coordinates AGAIN and only then returns the opaque admission proof (BRANDED, bounded-
19
+ * lived, bound to `{ space, endpoint, obligation key, opId }`); on any movement it settles
20
+ * its own provisional through the row's per-class decision coordinate and refuses. An
21
+ * obligation created in the movement window exists durably but can never admit.
22
+ * 3. PER-CLASS DECISION COORDINATES: an `epf` row settles through the EPF decision subject's
23
+ * create-only CAS (the mediator holds the terminal-REJECTION publish, §13.9, with the
24
+ * explicit D32 residual); a `self` row settles on ITSELF (`provisional → accepted` by the
25
+ * writer under an unexpired proof vs `provisional → rejected` by the drain, one CAS wins),
26
+ * and an `accepted` `self` row is deterministically finishable from its pinned commit
27
+ * intent alone ({@link recoverSelfObligation}: landed / re-apply / superseded).
28
+ * 4. DRAIN TO QUIESCENCE: enumerate the prefix (per-run throwaway LastPerSubject PULL
29
+ * consumer, fail-loud on markers and parse failures), settle every provisional through its
30
+ * decision coordinate, drive every accepted `self` row to terminal, RE-ENUMERATE until an
31
+ * enumeration finds no unsettled row. Quiescence = no `provisional` and no un-driven
32
+ * accepted `self` rows (§13.8; accepted `epf` work is tracked by its route facts).
33
+ *
34
+ * AUTHORITY BOUNDARIES (§13.9): the mediator's own grant is the `oblig.` create + CAS, the
35
+ * FENCING leader reads, and the EPF terminal-rejection publish. The guarded commit's WRITE
36
+ * stays with the writer principal — recovery takes an injected `applyCommit` and the mediator
37
+ * never holds the record-write grant itself. The policy-version publish and the govern-head
38
+ * selector CAS belong to the provisioner-registration principal, so those functions take the
39
+ * sealed {@link LifecycleRegistry} (the reference implementation's provisioner authority),
40
+ * not the mediator. The endpoint identity bind (§13.8's confined policy reader) is structural
41
+ * here: the mediator is SEALED per endpoint at open, every obligation key derives its endpoint
42
+ * token from the sealed identity, and a proof never validates for another endpoint's mediator.
43
+ *
44
+ * NOT HERE (later slices): the exact-pool terminal cleaner and the retirement barrier
45
+ * orchestration (D13 (5)) consume {@link drainTargetForEndpoint}; the production canonicalizer
46
+ * wiring rides the EPF slice.
47
+ */
48
+ import { jetstream, jetstreamManager } from "@nats-io/jetstream";
49
+ import { Kvm } from "@nats-io/kv";
50
+ import { EpEnvelopeError, OBLIGATION, OBLIGATION_EP_SENTINEL, POLICY_VERSION, GOVERN_HEAD, recordAtomicKey, createRecordEntry, updateRecordEntry, readRecordLeader, recordsBucket, epfEffectSubject, epfSubject, epfStreamName, epwSubject, epwStreamName, goalResultSubject, parseGoalResultFact, publishFactCreateOnly, readLastFact, parseDecisionFact, parseEffectFact, readGoalStatusByRefLeader, workTerminalSubject, parseWorkTerminalFact, workItemBytesOf, contractDigest, parseEpSubject, mintLifecycleUid, assertLifecycleToken, assertIdToken, endpointToken, assertPoolToken, } from "@cotal-ai/core";
51
+ import { assertAuthorityStreamShape, registryStores, openLifecycleMappingReader, readLifecycleMappingLeader, } from "./lifecycle-registry.js";
52
+ import { assertRecordsScannerSpace } from "./records-scanner.js";
53
+ const enc = new TextEncoder();
54
+ const dec = new TextDecoder();
55
+ const isRec = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
56
+ const uint = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
57
+ /** Every `*Digest` scalar on the wire is `sha256:<hex>` (§13.7/§13.8), the shape core's own fact
58
+ * validators and canonicalizer emit; the mediator stores and compares that shape end to end and
59
+ * strips the prefix only at a raw-hash boundary. */
60
+ const DIGEST_SCALAR_RE = /^sha256:[0-9a-f]{64}$/;
61
+ /** The §13.4 error-detail bound: `parseDecisionFact` rejects a longer detail, so a rejection the
62
+ * mediator publishes must fit it or core would refuse the mediator's own fact. */
63
+ const MAX_ERROR_DETAIL = 256;
64
+ /** The proof TTL ceiling (§13.8: a proof is bounded-lived; 60 s is 4x the reference call
65
+ * deadline, headroom for a slow admission without letting a proof outlive its state). */
66
+ const MAX_PROOF_TTL_MS = 60_000;
67
+ /** Truncate a string to at most `maxBytes` UTF-8 bytes without splitting a multibyte character
68
+ * (the §13.4 error-detail bound is a BYTE limit; a UTF-16 `.slice` can overshoot it). */
69
+ function truncateUtf8(s, maxBytes) {
70
+ if (Buffer.byteLength(s, "utf8") <= maxBytes)
71
+ return s;
72
+ let out = s;
73
+ while (Buffer.byteLength(out, "utf8") > maxBytes)
74
+ out = out.slice(0, -1);
75
+ return out;
76
+ }
77
+ const isCasLoss = (e) => e instanceof EpEnvelopeError && e.code === "conflict";
78
+ /** The CANONICAL content digest of a JSON value, `<hex>` (no prefix) for a `policy` key segment.
79
+ * Uses core's RFC-8785 canonicalizer (`contractDigest` = `sha256:<hex over I-JSON>`), so
80
+ * publication is insensitive to property order and a second implementation that content-
81
+ * addresses the same policy lands on the SAME key; also validates the value is I-JSON (throws
82
+ * otherwise), so a non-canonicalizable policy is refused before any create. */
83
+ const canonicalDigestHex = (value) => contractDigest(value).slice("sha256:".length);
84
+ const MEDIATORS = new WeakMap();
85
+ function internals(med) {
86
+ const i = MEDIATORS.get(med);
87
+ if (!i)
88
+ throw new EpEnvelopeError("failed-precondition", "the admission mediator was not constructed by openAdmissionMediator(); a hand-assembled context never authorizes (SPEC 13.12)");
89
+ return i;
90
+ }
91
+ /** Open the sealed per-endpoint admission mediator over the space's PRIMARY records store —
92
+ * shape-proved at bind exactly like every trusted consumer of that store (SPEC 13.12).
93
+ * `now`/`proofTtlMs` are probe seams; the proof TTL defaults to the §13.8 reference call
94
+ * deadline (15 s). `recordsScanner` is the SEALED records-obligation scanner the mediator's
95
+ * drain-to-quiescence enumeration runs on (SPEC 13.9, site 3): the mediator's own credential holds
96
+ * NO `CONSUMER.CREATE` on the records stream (a create-request body is not subject-ACL confinable —
97
+ * nats-server#8274), so it can never build a durable+PUSH exporter of its `oblig.` subtree; the
98
+ * CREATE lives only inside {@link ./records-scanner.ts}. Required: a mediator that cannot enumerate
99
+ * cannot drain (SPEC 13.8), which is a composition bug, not a silent degrade. */
100
+ export async function openAdmissionMediator(nc, space, endpoint, opts) {
101
+ assertRecordsScannerSpace(opts.recordsScanner, space);
102
+ const ep = endpointToken(endpoint);
103
+ const bucket = recordsBucket(space);
104
+ const jsm = await jetstreamManager(nc);
105
+ let recordsKv;
106
+ let cfg;
107
+ try {
108
+ recordsKv = await new Kvm(nc).open(bucket);
109
+ cfg = (await jsm.streams.info(`KV_${bucket}`)).config;
110
+ }
111
+ catch (e) {
112
+ throw new EpEnvelopeError("failed-precondition", `the records store ${bucket} is not provisioned (run space setup; SPEC 13.12): ${e?.message ?? String(e)}`);
113
+ }
114
+ assertAuthorityStreamShape(cfg, bucket);
115
+ const reader = await openLifecycleMappingReader(nc, space);
116
+ // The proof TTL is CAPPED to the reference call-deadline ceiling (§13.8: a proof is
117
+ // bounded-lived, and an effectively-unbounded TTL would let a stale proof outlive the state
118
+ // it was issued against). A larger value fails loud rather than being silently clamped.
119
+ if (opts.proofTtlMs !== undefined && (!Number.isSafeInteger(opts.proofTtlMs) || opts.proofTtlMs <= 0 || opts.proofTtlMs > MAX_PROOF_TTL_MS))
120
+ throw new EpEnvelopeError("failed-precondition", `proofTtlMs ${JSON.stringify(opts.proofTtlMs)} is not a positive safe integer within the ${MAX_PROOF_TTL_MS} ms bound (a proof is bounded-lived, SPEC 13.8)`);
121
+ const med = Object.freeze({ space, endpoint: ep });
122
+ MEDIATORS.set(med, {
123
+ space, endpoint: ep, recordsKv, jsm, js: jetstream(nc), reader,
124
+ now: opts.now ?? Date.now, proofTtlMs: opts.proofTtlMs ?? 15_000,
125
+ recordsScanner: opts.recordsScanner,
126
+ });
127
+ return med;
128
+ }
129
+ const MEDIATED_REQUESTS = new WeakSet();
130
+ /** Build the branded mediated request from the RAW authenticated request subject the broker
131
+ * delivered (an `ep`-plane request or an `epj`-plane journal submission). Core's
132
+ * {@link parseEpSubject} extracts the endpoint + caller triple STRUCTURALLY from the subject;
133
+ * a malformed subject, or one that is not a request/journal, refuses. The `id` (the
134
+ * caller-chosen request id, §13.4) is NOT in the subject and stays an explicit operation
135
+ * argument; everything that identifies WHO the caller is comes from here. */
136
+ export function mediatedRequestFromSubject(subject) {
137
+ const parsed = parseEpSubject(subject);
138
+ if (parsed === null || (parsed.plane !== "request" && parsed.plane !== "journal"))
139
+ throw new EpEnvelopeError("failed-precondition", `the admission request subject ${JSON.stringify(subject)} is not an authenticated request/journal subject; the obligation identity derives from the subject, never a body field (SPEC 13.8)`);
140
+ const c = parsed.caller;
141
+ const req = Object.freeze({ endpoint: parsed.endpoint, caller: Object.freeze({ owner: c.owner, actor: c.actor, uid: c.uid }) });
142
+ MEDIATED_REQUESTS.add(req);
143
+ return req;
144
+ }
145
+ function assertMediatedRequest(med, request) {
146
+ if (!MEDIATED_REQUESTS.has(request))
147
+ throw new EpEnvelopeError("permission-denied", "the admission request was not derived from an authenticated subject via mediatedRequestFromSubject(); a hand-assembled caller identity never authorizes (SPEC 13.8/13.12)");
148
+ if (request.endpoint !== med.endpoint)
149
+ throw new EpEnvelopeError("permission-denied", `the admission request is for endpoint "${request.endpoint}", not this mediator's "${med.endpoint}"; a mediator admits only its own endpoint's requests (SPEC 13.8/13.9)`);
150
+ }
151
+ function obligationKey(med, request, op) {
152
+ const target = op.target === undefined ? OBLIGATION_EP_SENTINEL : assertLifecycleToken(op.target.lifecycleUid, "target lifecycleUid");
153
+ return recordAtomicKey(OBLIGATION, [target, med.endpoint, request.caller.owner, request.caller.actor, request.caller.uid, op.id]);
154
+ }
155
+ const OBLIGATION_STATES = new Set(["provisional", "accepted", "rejected", "terminal"]);
156
+ const ROUTE_RE = /^(effects|pool\.[a-z0-9_-]{1,64})$/;
157
+ const DIGEST_HEX_RE = /^[0-9a-f]{64}$/; // the `policy.<endpoint>.<digest-hex>` KEY segment is bare hex (§13.7)
158
+ /** The submission fingerprint core's `parseDecisionFact` accepts — a `sha256:<hex>` scalar (the
159
+ * same shape `submissionFingerprint` emits). A row whose fingerprint is not this cannot settle
160
+ * through a core-conformant rejection fact, so it is refused at obtain, never persisted. */
161
+ const FINGERPRINT_RE = DIGEST_SCALAR_RE;
162
+ function parseCommitValue(v, key) {
163
+ if (!isRec(v))
164
+ throw new EpEnvelopeError("internal", `the obligation ${key} carries a non-object commitValue (SPEC 13.8)`);
165
+ const keys = Object.keys(v).sort();
166
+ if (v.enc === "b64u" && keys.join(",") === "bytes,enc" && typeof v.bytes === "string" && /^[A-Za-z0-9_-]*$/.test(v.bytes))
167
+ return v;
168
+ if (v.enc === "ref" && keys.join(",") === "enc,key" && typeof v.key === "string" && v.key.length > 0)
169
+ return v;
170
+ throw new EpEnvelopeError("internal", `the obligation ${key} carries a malformed commitValue (the CLOSED b64u|ref union, SPEC 13.8)`);
171
+ }
172
+ /** Validate an obligation row at the consuming boundary: CLOSED per-class schema, at least one
173
+ * currency pin, class-required fields present and class-foreign fields absent (SPEC 13.8). */
174
+ export function parseObligationRow(raw, key) {
175
+ let o;
176
+ try {
177
+ o = JSON.parse(dec.decode(raw));
178
+ }
179
+ catch {
180
+ throw new EpEnvelopeError("internal", `the obligation ${key} is not JSON; garbled trusted-path state never authorizes (SPEC 13.8)`);
181
+ }
182
+ if (!isRec(o))
183
+ throw new EpEnvelopeError("internal", `the obligation ${key} is not an object`);
184
+ const allowed = new Set(["state", "decision", "opId", "mappingRevision", "policyRevision", "fingerprint", "sourceSeq", "route", "commit"]);
185
+ for (const k of Object.keys(o))
186
+ if (!allowed.has(k))
187
+ throw new EpEnvelopeError("internal", `the obligation ${key} carries the unknown field "${k}" (closed schema, SPEC 13.8)`);
188
+ if (typeof o.state !== "string" || !OBLIGATION_STATES.has(o.state) || (o.decision !== "epf" && o.decision !== "self") || typeof o.opId !== "string")
189
+ throw new EpEnvelopeError("internal", `the obligation ${key} does not validate (state/decision/opId, SPEC 13.8)`);
190
+ try {
191
+ assertLifecycleToken(o.opId);
192
+ }
193
+ catch {
194
+ throw new EpEnvelopeError("internal", `the obligation ${key} carries a malformed opId (SPEC 13.8)`);
195
+ }
196
+ if (o.mappingRevision !== undefined && !uint(o.mappingRevision))
197
+ throw new EpEnvelopeError("internal", `the obligation ${key} carries a non-integer mappingRevision (SPEC 13.8)`);
198
+ if (o.policyRevision !== undefined && !uint(o.policyRevision))
199
+ throw new EpEnvelopeError("internal", `the obligation ${key} carries a non-integer policyRevision (SPEC 13.8)`);
200
+ if (o.mappingRevision === undefined && o.policyRevision === undefined)
201
+ throw new EpEnvelopeError("internal", `the obligation ${key} pins NO currency coordinate (at least one of mappingRevision/policyRevision, SPEC 13.8)`);
202
+ if (o.decision === "epf") {
203
+ if (o.commit !== undefined)
204
+ throw new EpEnvelopeError("internal", `the obligation ${key} is epf-class but carries a self-class commit intent (SPEC 13.8)`);
205
+ if (typeof o.fingerprint !== "string" || !FINGERPRINT_RE.test(o.fingerprint) || !uint(o.sourceSeq) || o.sourceSeq < 1 || typeof o.route !== "string" || !ROUTE_RE.test(o.route))
206
+ throw new EpEnvelopeError("internal", `the obligation ${key} is epf-class without a valid { fingerprint (sha256:<hex>), sourceSeq (>=1), route } (SPEC 13.4/13.8)`);
207
+ }
208
+ else {
209
+ if (o.fingerprint !== undefined || o.sourceSeq !== undefined || o.route !== undefined)
210
+ throw new EpEnvelopeError("internal", `the obligation ${key} is self-class but carries epf-class fields (SPEC 13.8)`);
211
+ if (!isRec(o.commit))
212
+ throw new EpEnvelopeError("internal", `the obligation ${key} is self-class without its commit intent (SPEC 13.8)`);
213
+ const cm = o.commit;
214
+ for (const k of Object.keys(cm))
215
+ if (!["commitKey", "commitBaseRevision", "commitValue", "commitDigest"].includes(k))
216
+ throw new EpEnvelopeError("internal", `the obligation ${key} commit intent carries the unknown field "${k}" (closed schema, SPEC 13.8)`);
217
+ if (typeof cm.commitKey !== "string" || cm.commitKey.length === 0 || !uint(cm.commitBaseRevision) || typeof cm.commitDigest !== "string" || !DIGEST_SCALAR_RE.test(cm.commitDigest))
218
+ throw new EpEnvelopeError("internal", `the obligation ${key} commit intent does not validate (commitDigest is sha256:<hex>, SPEC 13.8)`);
219
+ parseCommitValue(cm.commitValue, key);
220
+ }
221
+ return o;
222
+ }
223
+ async function readObligationLeader(med, key) {
224
+ const read = await readRecordLeader(med.jsm, med.space, key);
225
+ if (read === undefined)
226
+ return undefined;
227
+ return { row: parseObligationRow(enc.encode(JSON.stringify(read.value)), key), revision: read.revision };
228
+ }
229
+ function parseSelector(head, key) {
230
+ const sel = {};
231
+ const pair = (kk, rk) => {
232
+ const k = head[kk], r = head[rk];
233
+ if ((k === undefined) !== (r === undefined))
234
+ throw new EpEnvelopeError("internal", `the govern head ${key} carries ${kk}/${rk} unpaired; the selector is atomic (SPEC 13.6)`);
235
+ if (k === undefined)
236
+ return;
237
+ if (typeof k !== "string" || k.length === 0 || !uint(r))
238
+ throw new EpEnvelopeError("internal", `the govern head ${key} carries a malformed ${kk}/${rk} (SPEC 13.6)`);
239
+ sel[kk] = k;
240
+ sel[rk] = r;
241
+ };
242
+ pair("enforcedPolicyKey", "enforcedPolicyRevision");
243
+ pair("pendingPolicyKey", "pendingPolicyRevision");
244
+ return sel;
245
+ }
246
+ /** Publish ONE immutable policy version (§13.7 `policy.<endpoint>.<digest-hex>`): the key is the
247
+ * RFC-8785 CANONICAL content digest of the value (property-order-insensitive, so a conforming
248
+ * second implementation content-addresses the same policy to the same key), so publication is
249
+ * content-addressed and idempotent; an existing key whose value does not re-digest to it is
250
+ * corruption. Provisioner-registration authority (§13.9): takes the sealed registry, not the
251
+ * mediator. */
252
+ export async function publishPolicyVersion(reg, endpoint, value) {
253
+ const { recordsKv } = registryStores(reg);
254
+ const digestHex = canonicalDigestHex(value); // throws on non-I-JSON before any create
255
+ const key = recordAtomicKey(POLICY_VERSION, [endpoint, digestHex]);
256
+ try {
257
+ const revision = await createRecordEntry(recordsKv, key, value);
258
+ return { key, revision, digestHex };
259
+ }
260
+ catch (e) {
261
+ if (!isCasLoss(e))
262
+ throw e;
263
+ const existing = await recordsKv.get(key);
264
+ if (!existing || existing.operation !== "PUT")
265
+ throw new EpEnvelopeError("failed-precondition", `the policy version ${key} carries a ${existing?.operation ?? "missing"} marker; policy versions are never deleted (corruption, SPEC 13.12)`);
266
+ if (canonicalDigestHex(JSON.parse(dec.decode(existing.value))) !== digestHex)
267
+ throw new EpEnvelopeError("internal", `the policy version ${key} exists with a value that does not canonically digest to its own key; a self-certifying key never lies (corruption, SPEC 13.7)`);
268
+ return { key, revision: existing.revision, digestHex };
269
+ }
270
+ }
271
+ /** Leader-read + SELF-CERTIFY one immutable policy version: the stored bytes must digest to
272
+ * the key's own `<digest-hex>`, and the key must belong to THIS endpoint (the §13.8 identity
273
+ * bind: endpoint A can never be governed by a key smuggled from endpoint B's family). */
274
+ async function readPolicyVersionCertified(jsm, space, endpoint, key) {
275
+ const expectedPrefix = `policy.${endpoint}.`;
276
+ if (!key.startsWith(expectedPrefix))
277
+ throw new EpEnvelopeError("failed-precondition", `the policy key ${key} does not belong to endpoint "${endpoint}" (the selector names only this endpoint's own policy family, SPEC 13.6)`);
278
+ const digestHex = key.slice(expectedPrefix.length);
279
+ if (!DIGEST_HEX_RE.test(digestHex))
280
+ throw new EpEnvelopeError("failed-precondition", `the policy key ${key} is not digest-addressed (SPEC 13.7)`);
281
+ const read = await readRecordLeader(jsm, space, key);
282
+ if (read === undefined)
283
+ throw new EpEnvelopeError("failed-precondition", `the policy version ${key} does not exist; the selector names a published immutable version only (SPEC 13.6)`);
284
+ if (canonicalDigestHex(read.value) !== digestHex)
285
+ throw new EpEnvelopeError("internal", `the policy version ${key} does not canonically digest to its own key; a self-certifying key never lies (corruption, SPEC 13.7)`);
286
+ return read;
287
+ }
288
+ /** LEADER-SERVED govern-head read (§13.9: every govern read that fences a stage/promote/drain
289
+ * decision is leader-served `STREAM.MSG.GET`, never a follower Direct Get, or a multi-server
290
+ * stage/promote could act on a stale selector). `readRecordLeader` refuses a DEL/PURGE marker
291
+ * as corruption. Returns the head object + its store revision (the stage/promote CAS pins it). */
292
+ async function readGovernHeadRaw(jsm, space, endpoint) {
293
+ const key = recordAtomicKey(GOVERN_HEAD, [endpoint]);
294
+ const read = await readRecordLeader(jsm, space, key);
295
+ if (read === undefined)
296
+ return undefined;
297
+ if (!isRec(read.value))
298
+ throw new EpEnvelopeError("internal", `the govern head ${key} is not an object`);
299
+ return { head: read.value, revision: read.revision };
300
+ }
301
+ /** STAGE a policy mutation (§13.6 step 1, provisioner authority): verify the NEW immutable
302
+ * version exists and self-certifies, then CAS `pendingPolicy{Key,Revision}` onto the govern
303
+ * head, preserving every other head field verbatim (the binding map and registration slot are
304
+ * the registration path's). A virgin head is created with an empty binding map. A pending
305
+ * already staged refuses (`conflict`: one mutation at a time; promote or abandon it first). */
306
+ export async function stagePolicySelector(reg, endpoint, policyKey) {
307
+ const { recordsKv, jsm, space } = registryStores(reg);
308
+ const ep = endpointToken(endpoint);
309
+ const version = await readPolicyVersionCertified(jsm, space, ep, policyKey);
310
+ const govKey = recordAtomicKey(GOVERN_HEAD, [ep]);
311
+ const cur = await readGovernHeadRaw(jsm, space, ep);
312
+ const head = cur === undefined ? { commands: {} } : { ...cur.head };
313
+ parseSelector(head, govKey);
314
+ if (head.pendingPolicyKey !== undefined)
315
+ throw new EpEnvelopeError("conflict", `the govern head ${govKey} already stages pending policy ${String(head.pendingPolicyKey)}; one mutation at a time; promote or abandon it first (SPEC 13.6)`);
316
+ // A UNIQUE mutation opId stamped on the head binds this exact stage: a drain witness carries it,
317
+ // and a later re-stage (a NEW opId) invalidates any witness from an earlier stage, even for the
318
+ // same content-addressed policy key (the freelance's stage→drain→promote→re-stage→reuse attack).
319
+ const mutationOpId = mintLifecycleUid();
320
+ head.pendingPolicyKey = policyKey;
321
+ head.pendingPolicyRevision = version.revision;
322
+ head.pendingMutationOpId = mutationOpId;
323
+ const governRevision = cur === undefined
324
+ ? await createRecordEntry(recordsKv, govKey, head)
325
+ : await updateRecordEntry(recordsKv, govKey, head, cur.revision);
326
+ return { pendingPolicyKey: policyKey, pendingPolicyRevision: version.revision, mutationOpId, governRevision };
327
+ }
328
+ const QUIESCENCE = new WeakSet();
329
+ /** PROMOTE a staged policy mutation (§13.6 step 2, provisioner authority): only under the
330
+ * branded drain witness for the SAME endpoint and pending key. Moves `pendingPolicy…` into
331
+ * `enforcedPolicy…` and clears the pending slot, preserving every other head field. */
332
+ export async function promotePolicySelector(reg, endpoint, quiescence) {
333
+ const { recordsKv, jsm, space } = registryStores(reg);
334
+ const ep = endpointToken(endpoint);
335
+ if (!QUIESCENCE.has(quiescence))
336
+ throw new EpEnvelopeError("failed-precondition", "the promote was not given a drain-minted quiescence witness; a hand-assembled witness never authorizes (SPEC 13.6/13.12)");
337
+ if (quiescence.space !== space)
338
+ throw new EpEnvelopeError("permission-denied", `the quiescence witness belongs to space "${quiescence.space}", not "${space}"; a witness never crosses spaces (SPEC 13.6)`);
339
+ if (quiescence.endpoint !== ep)
340
+ throw new EpEnvelopeError("permission-denied", `the quiescence witness belongs to endpoint "${quiescence.endpoint}", not "${ep}" (SPEC 13.6)`);
341
+ const govKey = recordAtomicKey(GOVERN_HEAD, [ep]);
342
+ const cur = await readGovernHeadRaw(jsm, space, ep); // leader-served fence
343
+ if (cur === undefined)
344
+ throw new EpEnvelopeError("failed-precondition", `the govern head ${govKey} does not exist; nothing is staged (SPEC 13.6)`);
345
+ // Bind to the EXACT staged mutation: the head must still carry the witness's pending key AND
346
+ // the witness's mutation opId AND be at the witness's captured store revision. A re-stage
347
+ // (new opId, moved revision), a clear/restage, or a foreign promote in between all fail here,
348
+ // and the CAS from that exact revision is the final serialization.
349
+ const sel = parseSelector(cur.head, govKey);
350
+ if (sel.pendingPolicyKey === undefined)
351
+ throw new EpEnvelopeError("failed-precondition", `the govern head ${govKey} stages no pending policy; nothing to promote (SPEC 13.6)`);
352
+ if (sel.pendingPolicyKey !== quiescence.pendingPolicyKey || sel.pendingPolicyRevision !== quiescence.pendingPolicyRevision || cur.head.pendingMutationOpId !== quiescence.mutationOpId || cur.revision !== quiescence.governStageRevision)
353
+ throw new EpEnvelopeError("conflict", `the govern head ${govKey} moved since the witness was minted (staged ${String(sel.pendingPolicyKey)}@${String(sel.pendingPolicyRevision)} op ${String(cur.head.pendingMutationOpId)} rev ${cur.revision}, witness ${quiescence.pendingPolicyKey}@${quiescence.pendingPolicyRevision} op ${quiescence.mutationOpId} rev ${quiescence.governStageRevision}); drain again under the current stage (SPEC 13.6)`);
354
+ const head = { ...cur.head };
355
+ head.enforcedPolicyKey = sel.pendingPolicyKey;
356
+ head.enforcedPolicyRevision = sel.pendingPolicyRevision;
357
+ delete head.pendingPolicyKey;
358
+ delete head.pendingPolicyRevision;
359
+ delete head.pendingMutationOpId;
360
+ await updateRecordEntry(recordsKv, govKey, head, cur.revision);
361
+ QUIESCENCE.delete(quiescence); // consume: a witness authorizes exactly ONE promote
362
+ return { enforcedPolicyKey: sel.pendingPolicyKey, enforcedPolicyRevision: sel.pendingPolicyRevision };
363
+ }
364
+ /** The policy currency read (§13.6): leader-read the govern head, REFUSE while a pending
365
+ * policy is staged (the drain-window pause), follow `enforcedPolicyKey`, self-certify it, and
366
+ * RE-PROVE it is still at `enforcedPolicyRevision`. Public as {@link readEnforcedPolicy}. */
367
+ async function readPolicyCurrency(med) {
368
+ const govKey = recordAtomicKey(GOVERN_HEAD, [med.endpoint]);
369
+ const read = await readRecordLeader(med.jsm, med.space, govKey);
370
+ if (read === undefined || !isRec(read.value))
371
+ throw new EpEnvelopeError("failed-precondition", `endpoint "${med.endpoint}" has no govern head; no admission policy was ever enforced (SPEC 13.6)`);
372
+ const sel = parseSelector(read.value, govKey);
373
+ if (sel.pendingPolicyKey !== undefined)
374
+ throw new EpEnvelopeError("failed-precondition", `endpoint "${med.endpoint}" stages a pending policy (${sel.pendingPolicyKey}); proof issuance for policy-admitted decisions PAUSES inside the drain window (SPEC 13.6)`);
375
+ if (sel.enforcedPolicyKey === undefined)
376
+ throw new EpEnvelopeError("failed-precondition", `endpoint "${med.endpoint}" enforces no admission policy; a policy-admitted decision has nothing to pin (SPEC 13.6)`);
377
+ const version = await readPolicyVersionCertified(med.jsm, med.space, med.endpoint, sel.enforcedPolicyKey);
378
+ if (version.revision !== sel.enforcedPolicyRevision)
379
+ throw new EpEnvelopeError("failed-precondition", `the enforced policy ${sel.enforcedPolicyKey} reads at revision ${version.revision}, not the selector's ${sel.enforcedPolicyRevision}; an immutable version never moves (corruption); admission pauses rather than guessing (SPEC 13.6)`);
380
+ return { policy: version.value, revision: version.revision, key: sel.enforcedPolicyKey };
381
+ }
382
+ /** The confined policy read for a canonicalizer's capacity decision (§13.6/§13.8): the sealed
383
+ * mediator IS the identity bind (it reads only its own endpoint's policy). Returns
384
+ * `{ policy, revision }`; revision is what an admission pins. */
385
+ export async function readEnforcedPolicy(med) {
386
+ return readPolicyCurrency(internals(med));
387
+ }
388
+ /** The target currency read (§13.1/§13.8): leader-served head, `active` at the PRESENTED uid
389
+ * only (a retiring or retired target admits nothing). */
390
+ async function readTargetCurrency(med, target) {
391
+ const head = await readLifecycleMappingLeader(med.reader, target.owner, target.actor);
392
+ if (head === undefined || head.mapping.state !== "active" || head.mapping.lifecycleUid !== target.lifecycleUid)
393
+ throw new EpEnvelopeError("failed-precondition", `admission target "${target.owner}/${target.actor}" is ${head === undefined ? "unknown" : `${head.mapping.state} at uid ${head.mapping.lifecycleUid}`}, not ACTIVE at uid ${target.lifecycleUid}; a non-current target admits nothing (SPEC 13.8)`);
394
+ return { mappingRevision: head.revision };
395
+ }
396
+ async function readPins(med, op) {
397
+ if (op.target === undefined && op.policy !== true)
398
+ throw new EpEnvelopeError("failed-precondition", "an admission pins at least one currency coordinate: a target lifecycle or the enforced policy (SPEC 13.8)");
399
+ const pins = {};
400
+ if (op.target !== undefined)
401
+ pins.mappingRevision = (await readTargetCurrency(med, op.target)).mappingRevision;
402
+ if (op.policy === true)
403
+ pins.policyRevision = (await readPolicyCurrency(med)).revision;
404
+ return pins;
405
+ }
406
+ /** The post-create RECHECK (§13.8 step 2): the SAME coordinates, and the row's own pins must
407
+ * still be current. Movement between the create and here leaves the row as inert debt. */
408
+ async function recheckPins(med, op, row) {
409
+ if (row.mappingRevision !== undefined) {
410
+ if (op.target === undefined)
411
+ throw new EpEnvelopeError("failed-precondition", "the winning obligation is target-bound but this join presents no target; the full pinned identity must match (SPEC 13.8)");
412
+ const cur = await readTargetCurrency(med, op.target);
413
+ if (cur.mappingRevision !== row.mappingRevision)
414
+ throw new EpEnvelopeError("failed-precondition", `the target head moved (revision ${cur.mappingRevision} vs pinned ${row.mappingRevision}); the proof can never issue and the obligation is inert debt for the drain (SPEC 13.8)`);
415
+ }
416
+ if (row.policyRevision !== undefined) {
417
+ const cur = await readPolicyCurrency(med);
418
+ if (cur.revision !== row.policyRevision)
419
+ throw new EpEnvelopeError("failed-precondition", `the enforced policy moved (revision ${cur.revision} vs pinned ${row.policyRevision}); the proof can never issue and the obligation is inert debt for the drain (SPEC 13.8)`);
420
+ }
421
+ }
422
+ const PROOFS = new WeakSet();
423
+ function mintProof(med, key, opId) {
424
+ const proof = Object.freeze({ space: med.space, endpoint: med.endpoint, obligationKey: key, opId, exp: med.now() + med.proofTtlMs });
425
+ PROOFS.add(proof);
426
+ return proof;
427
+ }
428
+ /** The STRUCTURAL proof check: branded, unexpired, same space/endpoint/key (§13.8: endpoint A
429
+ * can never replay endpoint B's proof). This is the fast pre-check; the AUTHORITATIVE gate that
430
+ * admission requires is {@link verifyAdmissionProof}, which ALSO re-reads the CURRENT obligation
431
+ * state (§13.8: a drained or settled row leaves a locally-valid-looking proof inert). */
432
+ export function assertAdmissionProof(med, proof, obligationKey) {
433
+ const i = internals(med);
434
+ if (!PROOFS.has(proof))
435
+ throw new EpEnvelopeError("permission-denied", "the presented admission proof was not issued by a mediator; a hand-assembled proof never authorizes (SPEC 13.8/13.12)");
436
+ if (proof.space !== i.space || proof.endpoint !== i.endpoint)
437
+ throw new EpEnvelopeError("permission-denied", `the admission proof binds ${proof.space}/${proof.endpoint}, not ${i.space}/${i.endpoint}; a proof never crosses endpoints (SPEC 13.8)`);
438
+ if (proof.obligationKey !== obligationKey)
439
+ throw new EpEnvelopeError("permission-denied", `the admission proof binds ${proof.obligationKey}, not ${obligationKey} (SPEC 13.8)`);
440
+ if (i.now() >= proof.exp)
441
+ throw new EpEnvelopeError("deadline-exceeded", `the admission proof for ${obligationKey} expired; re-obtain through the mediator (the CURRENT obligation state is re-checked, SPEC 13.8)`);
442
+ }
443
+ /** The COMPLETE proof gate (§13.8: "checked against the CURRENT obligation state"): the
444
+ * structural check PLUS a leader-read of the row confirming it still exists, carries the
445
+ * proof's opId, and is NOT settled (rejected/terminal). A proof whose row a drain settled after
446
+ * issuance is refused here even though its brand/bind/expiry still look valid, so admission is
447
+ * proof-gated on live state, never on stale possession. Every effect the proof authorizes (the
448
+ * EPF acceptance publish, the self-class accept) gates on THIS, not the structural check alone. */
449
+ export async function verifyAdmissionProof(med, proof, obligationKey) {
450
+ const i = internals(med);
451
+ assertAdmissionProof(med, proof, obligationKey);
452
+ const cur = await readObligationLeader(i, obligationKey);
453
+ if (cur === undefined)
454
+ throw new EpEnvelopeError("failed-precondition", `the obligation ${obligationKey} no longer exists; a proof never authorizes over a vanished row (SPEC 13.8)`);
455
+ if (cur.row.opId !== proof.opId)
456
+ throw new EpEnvelopeError("permission-denied", `the obligation ${obligationKey} is held by operation ${cur.row.opId}, not the proof's ${proof.opId}; a superseded operation's proof never admits (SPEC 13.8)`);
457
+ if (cur.row.state === "rejected" || cur.row.state === "terminal")
458
+ throw new EpEnvelopeError("failed-precondition", `the obligation ${obligationKey} is ${cur.row.state}; its proof is stale (a drain or a foreign settle resolved it) and never admits (SPEC 13.8)`);
459
+ return cur.row;
460
+ }
461
+ async function obtainObligation(med, request, op, build, join) {
462
+ assertMediatedRequest(med, request); // the caller identity is broker-authenticated, not a body field
463
+ assertIdToken(op.id, "id");
464
+ const key = obligationKey(med, request, op);
465
+ // Step 1: the create-fence currency reads, IMMEDIATELY before the create.
466
+ const pins = await readPins(med, op);
467
+ const opId = mintLifecycleUid();
468
+ const fresh = build(pins, opId);
469
+ let row;
470
+ let revision;
471
+ let joined = false;
472
+ try {
473
+ revision = await createRecordEntry(med.recordsKv, key, fresh);
474
+ row = fresh;
475
+ }
476
+ catch (e) {
477
+ if (!isCasLoss(e))
478
+ throw e;
479
+ // A create loser leader-reads the winner and joins ONLY on the full pinned identity.
480
+ const winner = await readObligationLeader(med, key);
481
+ if (winner === undefined)
482
+ throw new EpEnvelopeError("failed-precondition", `the obligation ${key} lost its create but has no readable winner (a deletion marker or torn state); rows are never deleted (corruption, SPEC 13.12)`);
483
+ if (winner.row.decision !== fresh.decision)
484
+ throw new EpEnvelopeError("conflict", `the obligation ${key} is held by a ${winner.row.decision}-class winner; the full pinned identity must match to join (SPEC 13.8)`);
485
+ join(winner.row);
486
+ if (winner.row.state === "rejected" || winner.row.state === "terminal")
487
+ throw new EpEnvelopeError("failed-precondition", `the obligation ${key} is already ${winner.row.state}; a settled acceptance identity never re-admits (SPEC 13.8)`);
488
+ row = winner.row;
489
+ revision = winner.revision;
490
+ joined = true;
491
+ }
492
+ // Step 2: proof issuance is a post-create currency recheck. Movement settles our OWN
493
+ // provisional through its decision coordinate and refuses.
494
+ try {
495
+ await recheckPins(med, op, row);
496
+ }
497
+ catch (e) {
498
+ if (row.state === "provisional") {
499
+ try {
500
+ await settleObligation(med, key, row, revision, "the post-create recheck found the pinned coordinate moved");
501
+ }
502
+ catch {
503
+ /* the drain settles it; the refusal below is the authoritative outcome */
504
+ }
505
+ }
506
+ throw e;
507
+ }
508
+ return { key, row, revision, proof: mintProof(med, key, row.opId), joined };
509
+ }
510
+ /** Obtain an EPF-class obligation (a canonical acceptance's reservation, §13.8). The class is
511
+ * fixed HERE, by the trusted operation kind. */
512
+ export async function obtainEpfObligation(med, request, args) {
513
+ const i = internals(med);
514
+ // The fingerprint and sourceSeq must satisfy CORE's own fact validators up front (§13.4): the
515
+ // mediator settles an unresolved row by publishing a terminal RejectionFact carrying exactly
516
+ // these, and `parseDecisionFact` requires a `sha256:<hex>` fingerprint and a positive sourceSeq
517
+ // — a row that could not settle through a core-conformant fact is refused before it exists.
518
+ if (typeof args.fingerprint !== "string" || !FINGERPRINT_RE.test(args.fingerprint) || !uint(args.sourceSeq) || args.sourceSeq < 1 || typeof args.route !== "string" || !ROUTE_RE.test(args.route))
519
+ throw new EpEnvelopeError("failed-precondition", "an epf-class obtain requires { fingerprint (sha256:<hex>), sourceSeq (>=1), route (effects | pool.<pool>) } (SPEC 13.4/13.8)");
520
+ if (args.route.startsWith("pool."))
521
+ assertPoolToken(args.route.slice("pool.".length));
522
+ return obtainObligation(i, request, args, (pins, opId) => ({ state: "provisional", decision: "epf", opId, ...pins, fingerprint: args.fingerprint, sourceSeq: args.sourceSeq, route: args.route }), (winner) => {
523
+ // The join identity: coordinate (the key) + fingerprint + route. sourceSeq is the
524
+ // winner's own (whichever delivery is processing publishes with the WINNER's, §13.8).
525
+ if (winner.fingerprint !== args.fingerprint || winner.route !== args.route)
526
+ throw new EpEnvelopeError("conflict", `the obligation winner pins fingerprint ${winner.fingerprint}/route ${winner.route}, not ${args.fingerprint}/${args.route}; a mismatched join is a conflict, never a second obligation (SPEC 13.8)`);
527
+ });
528
+ }
529
+ /** Obtain a SELF-class obligation (a guarded record commit's reservation, e.g. the
530
+ * restart-status CAS, §13.6/§13.8). The COMPLETE commit intent is pinned at obtain. */
531
+ export async function obtainSelfObligation(med, request, args) {
532
+ const i = internals(med);
533
+ const cm = args.commit;
534
+ if (!isRec(cm) || typeof cm.commitKey !== "string" || cm.commitKey.length === 0 || !uint(cm.commitBaseRevision) || typeof cm.commitDigest !== "string" || !DIGEST_SCALAR_RE.test(cm.commitDigest))
535
+ throw new EpEnvelopeError("failed-precondition", "a self-class obtain requires the complete commit intent { commitKey, commitBaseRevision, commitValue, commitDigest (sha256:<hex>) } (SPEC 13.8)");
536
+ parseCommitValue(cm.commitValue, "<obtain>");
537
+ // RESOLVE + verify the commit value canonically digests to commitDigest BEFORE the row can be
538
+ // created/accepted (§13.8: deterministic finishability). A mismatched value/digest, a
539
+ // non-canonical b64u, or a mutable/absent ref is refused here, never reaching `accepted` where
540
+ // recovery would wedge every drain forever.
541
+ await resolveCommitValue(i, { commitKey: cm.commitKey, commitBaseRevision: cm.commitBaseRevision, commitValue: cm.commitValue, commitDigest: cm.commitDigest });
542
+ return obtainObligation(i, request, args, (pins, opId) => ({ state: "provisional", decision: "self", opId, ...pins, commit: { commitKey: cm.commitKey, commitBaseRevision: cm.commitBaseRevision, commitValue: cm.commitValue, commitDigest: cm.commitDigest } }), (winner) => {
543
+ // The join identity: the ENTIRE commit intent (key + base revision + digest), so two
544
+ // different desired values or base revisions never join under one commitKey (§13.8).
545
+ const w = winner.commit;
546
+ if (w.commitKey !== cm.commitKey || w.commitBaseRevision !== cm.commitBaseRevision || w.commitDigest !== cm.commitDigest)
547
+ throw new EpEnvelopeError("conflict", `the obligation winner pins commit ${w.commitKey}@${w.commitBaseRevision} digest ${w.commitDigest.slice(0, 12)}…, not ${cm.commitKey}@${cm.commitBaseRevision} digest ${cm.commitDigest.slice(0, 12)}…; a mismatched join is a conflict (SPEC 13.8)`);
548
+ });
549
+ }
550
+ // ---- the per-class decision coordinates (§13.8 step 3) ----------------------------------------
551
+ function epfDecisionSubjectFor(med, key) {
552
+ // The obligation key IS the acceptance identity: oblig.<target>.<endpoint>.<cO>.<cA>.<cUid>.<id>
553
+ // (toks: [oblig, target, endpoint, cOwner, cActor, cUid, id]) — the caller triple + id start
554
+ // at index 3, AFTER the endpoint at index 2.
555
+ const toks = key.split(".");
556
+ const [cOwner, cActor, cUid, id] = toks.slice(3);
557
+ return epfSubject(med.space, med.endpoint, ["dec", cOwner, cActor, cUid, id]);
558
+ }
559
+ /** Settle ONE unresolved (provisional) obligation through its class's decision coordinate
560
+ * (§13.8): an `epf` row reads the decision subject — an existing acceptance advances the row
561
+ * (the writer crashed between the decision CAS and the row advance), otherwise the mediator
562
+ * publishes the create-only TERMINAL REJECTION (its §13.9 grant; a delayed acceptance CAS
563
+ * loses) and rejects the row; a `self` row settles on ITSELF (`provisional → rejected`,
564
+ * contending with the writer's own `provisional → accepted` on the one row). */
565
+ async function settleObligation(med, key, row, revision, why) {
566
+ if (row.state !== "provisional")
567
+ throw new EpEnvelopeError("failed-precondition", `the obligation ${key} is ${row.state}, not provisional; only an unresolved row settles here (SPEC 13.8)`);
568
+ if (row.decision === "self") {
569
+ try {
570
+ await updateRecordEntry(med.recordsKv, key, { ...row, state: "rejected" }, revision);
571
+ return "rejected";
572
+ }
573
+ catch (e) {
574
+ if (!isCasLoss(e))
575
+ throw e;
576
+ // Exactly one of { writer's accept, our reject } wins the one-row CAS. Re-read and report.
577
+ const after = await readObligationLeader(med, key);
578
+ if (after === undefined)
579
+ throw new EpEnvelopeError("internal", `the obligation ${key} vanished mid-settle; rows are never deleted (corruption, SPEC 13.12)`);
580
+ if (after.row.state === "accepted")
581
+ return "accepted";
582
+ if (after.row.state === "rejected" || after.row.state === "terminal")
583
+ return "rejected";
584
+ throw new EpEnvelopeError("conflict", `the obligation ${key} moved during its settle (still ${after.row.state}); re-read and re-decide (SPEC 13.8)`);
585
+ }
586
+ }
587
+ // epf: the EPF decision subject is the coordinate.
588
+ const subject = epfDecisionSubjectFor(med, key);
589
+ const stream = epfStreamName(med.space);
590
+ const existing = await readLastFact(med.jsm, stream, subject);
591
+ if (existing !== undefined) {
592
+ const fact = parseDecisionFact(existing, subject);
593
+ assertDecisionMatchesRow(fact, row, key); // the fact on this coordinate must be THIS obligation's identity
594
+ const to = fact.decision === "accepted" ? "accepted" : "rejected";
595
+ await advanceRowSettled(med, key, row, revision, to);
596
+ return to;
597
+ }
598
+ const toks = key.split(".");
599
+ const [cOwner, cActor, cUid, id] = toks.slice(3); // [oblig, target, endpoint, cOwner, cActor, cUid, id]
600
+ // The detail is BOUNDED to the §13.4 error-detail limit, which counts UTF-8 BYTES (what
601
+ // `parseDecisionFact` enforces), not UTF-16 code units — a multibyte `why` truncated by
602
+ // `.slice` could still exceed the byte limit and make core refuse the mediator's own fact.
603
+ const detail = truncateUtf8(`settled by the admission mediator: ${why}`, MAX_ERROR_DETAIL);
604
+ const rejection = {
605
+ v: 1, id, decision: "rejected", fingerprint: row.fingerprint,
606
+ error: { code: "failed-precondition", detail },
607
+ caller: { id: `${cOwner}.${cActor}`, lifecycleUid: cUid },
608
+ sourceSeq: row.sourceSeq, ts: med.now(),
609
+ };
610
+ const published = await publishFactCreateOnly(med.js, subject, enc.encode(JSON.stringify(rejection)));
611
+ if (!published.won) {
612
+ // A decision landed between our read and our publish: the loser reads the winner (§13.4).
613
+ const winner = await readLastFact(med.jsm, stream, subject);
614
+ if (winner === undefined)
615
+ throw new EpEnvelopeError("internal", `the decision subject ${subject} rejected our create but reads empty; fail closed (SPEC 13.4)`);
616
+ const fact = parseDecisionFact(winner, subject);
617
+ assertDecisionMatchesRow(fact, row, key);
618
+ const to = fact.decision === "accepted" ? "accepted" : "rejected";
619
+ await advanceRowSettled(med, key, row, revision, to);
620
+ return to;
621
+ }
622
+ await advanceRowSettled(med, key, row, revision, "rejected");
623
+ return "rejected";
624
+ }
625
+ /** A decision fact found on an obligation's coordinate MUST be THIS obligation's acceptance
626
+ * identity (§13.4/§13.8): the caller-scoped subject can only bear one first-wins decision, so a
627
+ * fact whose fingerprint (or, for an acceptance, route) differs from the row is a foreign
628
+ * identity on the same coordinate — never allowed to silently advance this row. Fail loud. */
629
+ function assertDecisionMatchesRow(fact, row, key) {
630
+ if (fact.fingerprint !== row.fingerprint)
631
+ throw new EpEnvelopeError("internal", `the decision fact on ${key}'s coordinate pins fingerprint ${fact.fingerprint}, not this obligation's ${row.fingerprint}; a foreign acceptance identity never settles this obligation (SPEC 13.4/13.8)`);
632
+ if (fact.sourceSeq !== row.sourceSeq)
633
+ throw new EpEnvelopeError("internal", `the decision fact on ${key}'s coordinate pins sourceSeq ${fact.sourceSeq}, not this obligation's ${row.sourceSeq}; the winner's full acceptance identity (fingerprint + sourceSeq + route) must match (SPEC 13.4/13.8)`);
634
+ if (fact.decision === "accepted" && fact.route !== row.route)
635
+ throw new EpEnvelopeError("internal", `the acceptance fact on ${key}'s coordinate pins route ${fact.route}, not this obligation's ${row.route} (SPEC 13.4/13.8)`);
636
+ }
637
+ async function advanceRowSettled(med, key, row, revision, to) {
638
+ try {
639
+ await updateRecordEntry(med.recordsKv, key, { ...row, state: to }, revision);
640
+ }
641
+ catch (e) {
642
+ if (!isCasLoss(e))
643
+ throw e;
644
+ const after = await readObligationLeader(med, key);
645
+ if (after === undefined)
646
+ throw new EpEnvelopeError("internal", `the obligation ${key} vanished mid-advance (corruption, SPEC 13.12)`);
647
+ if (after.row.state !== to && !(to === "rejected" && after.row.state === "terminal"))
648
+ throw new EpEnvelopeError("conflict", `the obligation ${key} advanced to ${after.row.state} while settling to ${to}; re-read and re-decide (SPEC 13.8)`);
649
+ }
650
+ }
651
+ /** Public settle of one unresolved obligation (the drains call it; the recheck refusals call
652
+ * it internally). Leader-reads the current row first. */
653
+ export async function settleEpfOrSelfObligation(med, obligationKey, why) {
654
+ const i = internals(med);
655
+ assertObligationOfEndpoint(i, obligationKey);
656
+ const cur = await readObligationLeader(i, obligationKey);
657
+ if (cur === undefined)
658
+ throw new EpEnvelopeError("not-found", `no obligation exists at ${obligationKey} (SPEC 13.8)`);
659
+ if (cur.row.state !== "provisional")
660
+ return "already-settled";
661
+ return settleObligation(i, obligationKey, cur.row, cur.revision, why);
662
+ }
663
+ function assertObligationOfEndpoint(med, key) {
664
+ const toks = key.split(".");
665
+ if (toks[0] !== "oblig" || toks.length !== 7 || toks[2] !== med.endpoint)
666
+ throw new EpEnvelopeError("permission-denied", `the obligation ${key} does not belong to endpoint "${med.endpoint}"; a mediator settles only its own endpoint's rows (SPEC 13.9)`);
667
+ }
668
+ // ---- the self-class writer protocol (accept → guarded commit → terminal) ----------------------
669
+ /** Advance a `self`-class obligation `provisional → accepted` under an unexpired proof
670
+ * (§13.8 step 3: the guarded commit is authorized only while the row is `accepted`). */
671
+ export async function acceptSelfObligation(med, proof) {
672
+ const i = internals(med);
673
+ // The state-aware proof gate (§13.8): re-reads the CURRENT row (opId + not-settled), so a
674
+ // drain that settled the row after the proof issued refuses the accept here.
675
+ const row = await verifyAdmissionProof(med, proof, proof.obligationKey);
676
+ if (row.decision !== "self")
677
+ throw new EpEnvelopeError("failed-precondition", `the obligation ${proof.obligationKey} is epf-class; only a self-class row advances on itself (SPEC 13.8)`);
678
+ const cur = await readObligationLeader(i, proof.obligationKey);
679
+ if (cur === undefined)
680
+ throw new EpEnvelopeError("not-found", `no obligation exists at ${proof.obligationKey} (SPEC 13.8)`);
681
+ if (cur.row.state === "accepted")
682
+ return { revision: cur.revision }; // the writer's own resume
683
+ if (cur.row.state !== "provisional")
684
+ throw new EpEnvelopeError("failed-precondition", `the obligation ${proof.obligationKey} is ${cur.row.state}; a settled row never re-accepts (the drain won the one-row CAS, SPEC 13.8)`);
685
+ try {
686
+ const revision = await updateRecordEntry(i.recordsKv, proof.obligationKey, { ...cur.row, state: "accepted" }, cur.revision);
687
+ return { revision };
688
+ }
689
+ catch (e) {
690
+ if (!isCasLoss(e))
691
+ throw e;
692
+ throw new EpEnvelopeError("permission-denied", `the obligation ${proof.obligationKey} moved during the accept (a drain's rejection contends on the one row and exactly one wins); the delayed commit's authority is gone (SPEC 13.8)`);
693
+ }
694
+ }
695
+ /** Strictly decode a `b64u` commit value: CANONICAL base64url (a round-trip re-encode must
696
+ * reproduce the input exactly, rejecting non-zero pad bits that Node's decoder tolerates) and
697
+ * FATAL UTF-8 (no replacement chars), then parse JSON. A strict second implementation refuses
698
+ * exactly what a lax one would silently accept, so the closed union stays interoperable (§13.8). */
699
+ function strictB64uToJson(b64u, commitKey) {
700
+ if (!/^[A-Za-z0-9_-]*$/.test(b64u))
701
+ throw new EpEnvelopeError("failed-precondition", `the b64u commit value for ${commitKey} is not base64url (RFC 4648 §5, no pad); a non-canonical encoding never resolves (SPEC 13.8)`);
702
+ const bytes = new Uint8Array(Buffer.from(b64u, "base64url"));
703
+ if (Buffer.from(bytes).toString("base64url") !== b64u)
704
+ throw new EpEnvelopeError("failed-precondition", `the b64u commit value for ${commitKey} is not canonical base64url (its bytes re-encode differently); a lax decoder would tolerate stray pad bits (SPEC 13.8)`);
705
+ let text;
706
+ try {
707
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
708
+ }
709
+ catch {
710
+ throw new EpEnvelopeError("failed-precondition", `the b64u commit value for ${commitKey} is not valid UTF-8; a recovery never writes replacement-decoded bytes (SPEC 13.8)`);
711
+ }
712
+ let value;
713
+ try {
714
+ value = JSON.parse(text);
715
+ }
716
+ catch {
717
+ throw new EpEnvelopeError("failed-precondition", `the b64u commit value for ${commitKey} is not JSON; a recovery never writes non-JSON (SPEC 13.8)`);
718
+ }
719
+ return { value, bytes };
720
+ }
721
+ /** Resolve a commit intent's value + bytes (§13.8) and VERIFY its canonical digest equals
722
+ * `commitDigest` — the SINGLE integrity gate used at BOTH obtain (before the row can accept)
723
+ * and recovery, so a mismatched value/digest never reaches `accepted` to wedge a drain. A
724
+ * `b64u` value is strictly decoded (canonical base64url + fatal UTF-8); a `ref` MUST name an
725
+ * IMMUTABLE `policy`-kind key (a mutable ref could move after acceptance and permanently wedge
726
+ * recovery), leader-read and self-certified. The digest is over the canonical VALUE, so a
727
+ * non-canonical storage stringify never breaks the comparison (RFC-8785). */
728
+ async function resolveCommitValue(med, intent) {
729
+ let value;
730
+ let bytes;
731
+ if (intent.commitValue.enc === "b64u") {
732
+ ({ value, bytes } = strictB64uToJson(intent.commitValue.bytes, intent.commitKey));
733
+ }
734
+ else {
735
+ const refKey = intent.commitValue.key;
736
+ if (!/^policy\.[^.]+\.[0-9a-f]{64}$/.test(refKey))
737
+ throw new EpEnvelopeError("failed-precondition", `the commit-value ref ${refKey} is not an immutable policy.<endpoint>.<digest> key; only an immutable create-only kind may be referenced (a mutable ref could move after acceptance and wedge recovery, SPEC 13.8)`);
738
+ const read = await readRecordLeader(med.jsm, med.space, refKey);
739
+ if (read === undefined)
740
+ throw new EpEnvelopeError("failed-precondition", `the commit-value ref ${refKey} does not exist; a recovery cannot resolve the promised value (SPEC 13.8)`);
741
+ // SELF-CERTIFY the referenced version against ITS OWN key digest (§13.7: every policy
742
+ // reader refuses a key/content mismatch), not only against the caller-pinned commitDigest
743
+ // below — a corrupted row whose content matches the caller's pin but not its key would
744
+ // otherwise launder a non-canonical version through the ref form.
745
+ const keyHex = refKey.split(".").pop();
746
+ const valueHex = contractDigest(read.value).slice("sha256:".length);
747
+ if (valueHex !== keyHex)
748
+ throw new EpEnvelopeError("failed-precondition", `the commit-value ref ${refKey} resolves to content whose canonical digest is ${valueHex}, not the key's own digest; a policy version is self-certifying and a mismatch is corruption, refused (SPEC 13.7)`);
749
+ value = read.value;
750
+ bytes = enc.encode(JSON.stringify(read.value));
751
+ }
752
+ if (contractDigest(value) !== intent.commitDigest)
753
+ throw new EpEnvelopeError("failed-precondition", `the resolved commit value does not canonically digest to the pinned commitDigest; the intent is refused before it can accept (SPEC 13.8)`);
754
+ return { value, bytes };
755
+ }
756
+ /** Drive an `accepted` `self`-class obligation to `terminal` deterministically from its pinned
757
+ * intent alone (§13.8): landed (digest matches) / re-apply (still at base; via the injected
758
+ * {@link ApplyCommit}) / superseded (moved past to a foreign value). The writer's own resume
759
+ * AND the drain reconciler both use this — an accepted commit is never an unrecoverable
760
+ * orphan. Returns what it found. */
761
+ export async function recoverSelfObligation(med, obligationKey, deps) {
762
+ return recoverSelfCore(internals(med), obligationKey, deps);
763
+ }
764
+ async function recoverSelfCore(i, obligationKey, deps) {
765
+ assertObligationOfEndpoint(i, obligationKey);
766
+ const cur = await readObligationLeader(i, obligationKey);
767
+ if (cur === undefined)
768
+ throw new EpEnvelopeError("not-found", `no obligation exists at ${obligationKey} (SPEC 13.8)`);
769
+ if (cur.row.decision !== "self" || cur.row.state !== "accepted")
770
+ throw new EpEnvelopeError("failed-precondition", `the obligation ${obligationKey} is ${cur.row.decision}/${cur.row.state}; recovery drives ACCEPTED self-class rows only (a provisional settles through the drain, SPEC 13.8)`);
771
+ const intent = cur.row.commit;
772
+ const record = await readRecordLeader(i.jsm, i.space, intent.commitKey);
773
+ // The terminal CAS is idempotent under concurrency (distsys H3): two recoveries, or a drain
774
+ // reconciler racing the writer's own resume, both drive `accepted → terminal`. On a lost CAS,
775
+ // re-read: already terminal is SUCCESS, still accepted (a foreign advance bumped the revision)
776
+ // retries once at the fresh revision, anything else is a real conflict.
777
+ const terminal = async () => {
778
+ try {
779
+ await updateRecordEntry(i.recordsKv, obligationKey, { ...cur.row, state: "terminal" }, cur.revision);
780
+ }
781
+ catch (e) {
782
+ if (!isCasLoss(e))
783
+ throw e;
784
+ const after = await readObligationLeader(i, obligationKey);
785
+ if (after === undefined)
786
+ throw new EpEnvelopeError("internal", `the obligation ${obligationKey} vanished mid-terminalize (corruption, SPEC 13.12)`);
787
+ if (after.row.state === "terminal")
788
+ return;
789
+ if (after.row.state === "accepted") {
790
+ await updateRecordEntry(i.recordsKv, obligationKey, { ...after.row, state: "terminal" }, after.revision);
791
+ return;
792
+ }
793
+ throw new EpEnvelopeError("conflict", `the obligation ${obligationKey} is ${after.row.state} while terminalizing; re-read and re-decide (SPEC 13.8)`);
794
+ }
795
+ };
796
+ if (record !== undefined && contractDigest(record.value) === intent.commitDigest) {
797
+ await terminal();
798
+ return "landed";
799
+ }
800
+ const atBase = (record === undefined && intent.commitBaseRevision === 0) || (record !== undefined && record.revision === intent.commitBaseRevision);
801
+ if (atBase) {
802
+ const { bytes } = await resolveCommitValue(i, intent);
803
+ await deps.applyCommit(intent.commitKey, bytes, intent.commitBaseRevision);
804
+ await terminal();
805
+ return "re-applied";
806
+ }
807
+ // Moved past the base to a foreign value: the guarded CAS could never land. Terminal as
808
+ // superseded (§13.8) — the intended commit is dead, not pending.
809
+ await terminal();
810
+ return "superseded";
811
+ }
812
+ /** Enumerate an obligation filter through the SEALED records scanner (§13.9, site 3), turning each
813
+ * raw entry into a parsed, closed-schema obligation row. Markers and parse failures abort LOUD: a
814
+ * drain that skipped either would declare quiescence over rows it never read. The scanner holds the
815
+ * ONLY `CONSUMER.CREATE` on the records stream (fence-free LastPerSubject; the caller — mediator or
816
+ * §13.1 retirement barrier — holds none, so a compromise can never durable-export the `oblig.`
817
+ * subtree, nats-server#8274). The barrier enumerates the target-wide `oblig.<targetUid>.>` (its
818
+ * endpoint discovery + quiescence re-check) through the registry's records scanner; each mediator
819
+ * enumerates its own endpoint's subtree through the scanner injected at open. INVARIANT: the
820
+ * `scanner` argument MUST come from one of those brand-asserted seams (`med.recordsScanner`,
821
+ * `registryRecordsScanner(reg)`), never a hand-passed instance; the brand assert lives at
822
+ * INJECTION (openAdmissionMediator / openLifecycleRegistry), not here, so a new caller that
823
+ * hand-passes a scanner bypasses the space bond. */
824
+ export async function enumerateObligationRows(scanner, filter) {
825
+ const out = [];
826
+ for (const e of await scanner.scanObligations(filter)) {
827
+ if (e.op === "DEL" || e.op === "PURGE")
828
+ throw new EpEnvelopeError("failed-precondition", `the obligation ${e.key} carries a ${e.op} marker; obligation rows are never deleted (corruption, SPEC 13.12)`);
829
+ out.push({ key: e.key, row: parseObligationRow(e.data, e.key), revision: e.seq });
830
+ }
831
+ return out;
832
+ }
833
+ function enumerateObligations(med, filter) {
834
+ return enumerateObligationRows(med.recordsScanner, filter);
835
+ }
836
+ /** Verify an ACCEPTED epf row's route reached its durable postcondition before the drain counts
837
+ * it quiescent (§13.8 accept-side reconciliation). The drain OWNS the check so a presence-only
838
+ * reconciler cannot fake it: an `effects` route is established by its own acceptance decision
839
+ * fact (the row is accepted, so that fact exists); a `pool.<pool>` route is established by the
840
+ * EXACT §13.6 reconciliation predicate: a VALIDATED terminal `wrk` fact (the item is SETTLED —
841
+ * EPW is a WorkQueue, so a terminally-acked item is normally ABSENT, and settled work is never
842
+ * re-enqueued) OR a live EPW entry at the acceptance identity (the enqueue landed and is in
843
+ * flight). Only BOTH absent is the repairable crash-before-enqueue state: the injected
844
+ * reconciler repairs it and the drain RE-READS both coordinates; still unestablished fails
845
+ * closed. */
846
+ async function verifyAcceptedEpfRoute(med, key, row, deps) {
847
+ const [cOwner, cActor, cUid, id] = key.split(".").slice(3); // [oblig, target, endpoint, cO, cA, cUid, id]
848
+ if (row.route === "effects") {
849
+ const decSubject = epfSubject(med.space, med.endpoint, ["dec", cOwner, cActor, cUid, id]);
850
+ const decRaw = await readLastFact(med.jsm, epfStreamName(med.space), decSubject);
851
+ if (decRaw === undefined)
852
+ throw new EpEnvelopeError("internal", `accepted effects obligation ${key} has no decision fact; accepted rows derive from a durable decision (corruption, SPEC 13.8)`);
853
+ const fact = parseDecisionFact(decRaw, decSubject);
854
+ if (fact.decision !== "accepted")
855
+ throw new EpEnvelopeError("internal", `accepted effects obligation ${key} points at a rejected decision fact; obligation/decision state diverged (corruption, SPEC 13.8)`);
856
+ assertDecisionMatchesRow(fact, row, key);
857
+ const goalId = typeof fact.request.goalId === "string" ? fact.request.goalId : undefined;
858
+ const goalRef = goalId !== undefined
859
+ ? { endpoint: med.endpoint, caller: { owner: cOwner, actor: cActor, uid: cUid }, goalId }
860
+ : undefined;
861
+ const doneSubject = goalRef !== undefined
862
+ ? goalResultSubject(med.space, goalRef)
863
+ : epfEffectSubject(med.space, med.endpoint, { owner: cOwner, actor: cActor, uid: cUid }, id);
864
+ const established = async () => {
865
+ const doneRaw = await readLastFact(med.jsm, epfStreamName(med.space), doneSubject);
866
+ if (doneRaw === undefined)
867
+ return false;
868
+ if (goalRef !== undefined) {
869
+ const done = parseGoalResultFact(doneRaw, doneSubject, goalRef);
870
+ if (done.fingerprint !== fact.fingerprint)
871
+ throw new EpEnvelopeError("internal", `goal completion ${doneSubject} does not match accepted decision ${decSubject}; a mismatched terminal never proves quiescence (SPEC 13.6/13.9)`);
872
+ return true;
873
+ }
874
+ const done = parseEffectFact(doneRaw, doneSubject);
875
+ if (done.fingerprint !== fact.fingerprint || done.sourceSeq !== fact.sourceSeq)
876
+ throw new EpEnvelopeError("internal", `effect completion ${doneSubject} does not match accepted decision ${decSubject}; a mismatched marker never proves quiescence (SPEC 13.9)`);
877
+ return true;
878
+ };
879
+ if (await established())
880
+ return;
881
+ if (deps.cancelEffectsRoute === undefined)
882
+ throw new EpEnvelopeError("failed-precondition", `accepted effects obligation ${key} has no durable completion marker ${doneSubject} and no cancelEffectsRoute was given; a drain never declares quiescence over executable accepted effects work (SPEC 13.8/13.9)`);
883
+ // CANCEL-FIRST for ACTIONS (fact#2): a retirement's create-only `cancelled` terminal asserts
884
+ // the effect did NOT run (SPEC 13.6: "a reader that sees cancelled KNOWS the effect did not
885
+ // run"). For an ACTION goal that is proof only while the goal is still `accepted` — the
886
+ // `running` edge is where it effects, and it is guard/currency-fenced. Once the goal is
887
+ // running/waiting/cancelling the external action MAY have effected, so a create-only cancelled
888
+ // could beat the executor's real completion and lie. The action's own `accepted → cancelling →
889
+ // terminal` machine (executor epoch/currency-fenced) is the ONLY correct cancel there; the
890
+ // retirement drives no create-only over it. So: leader-read the goal status and, if it is past
891
+ // `accepted`, REFUSE the drain loud + resumable — the goal terminalizes through its own machine
892
+ // (a racing completion, fenced out once the target is retired, or its readiness → uncertain
893
+ // settle) and the drain re-reads on resume. An `accepted` (or never-created) goal never
894
+ // effected, so its create-only cancelled below is truthful (SPEC 13.8 option (i), the SPEC's
895
+ // first-terminal-wins model). The NON-action effect variant is untouched (goalRef undefined):
896
+ // it has no goal machine and its create-only cancel is a held SPEC decision.
897
+ if (goalRef !== undefined) {
898
+ const goalStatus = await readGoalStatusByRefLeader(med.jsm, med.space, goalRef);
899
+ if (goalStatus !== undefined && goalStatus.state !== "accepted")
900
+ throw new EpEnvelopeError("unavailable", `accepted action goal "${goalRef.goalId}" for ${key} is "${goalStatus.state}", not "accepted"; a retirement never writes a create-only cancelled over an action that may have effected - it terminalizes through its own accepted->cancelling machine (executor epoch-fenced) or its readiness settle, and the drain re-reads on resume (SPEC 13.6/13.8)`);
901
+ }
902
+ // The MEDIATOR derives the repair coordinates (the closed-command boundary): the parsed,
903
+ // row-bound acceptance + the exact completion subject. The executor stamps its own retirement
904
+ // identity through the core validated builders and publishes create-only (first-terminal-wins).
905
+ await deps.cancelEffectsRoute({ kind: "effects-cancel", subject: doneSubject, key, goal: goalRef !== undefined, acceptance: fact });
906
+ if (!(await established()))
907
+ throw new EpEnvelopeError("unavailable", `the effects hook for accepted obligation ${key} did not establish a bound completion terminal at ${doneSubject}; effects work is still executable and quiescence fails closed (SPEC 13.8/13.9)`);
908
+ return;
909
+ }
910
+ const pool = row.route.slice("pool.".length);
911
+ const ref = { endpoint: med.endpoint, pool, acceptance: { owner: cOwner, actor: cActor, uid: cUid, id } };
912
+ const wrkSubject = workTerminalSubject(med.space, ref);
913
+ const itemSubject = epwSubject(med.space, med.endpoint, pool, ref.acceptance);
914
+ const established = async () => {
915
+ const wrk = await readLastFact(med.jsm, epfStreamName(med.space), wrkSubject);
916
+ if (wrk !== undefined) {
917
+ parseWorkTerminalFact(wrk, wrkSubject, ref); // a garbled terminal never counts as settled
918
+ return true;
919
+ }
920
+ return (await readLastFact(med.jsm, epwStreamName(med.space), itemSubject)) !== undefined;
921
+ };
922
+ if (await established())
923
+ return;
924
+ if (deps.reconcilePoolRoute === undefined)
925
+ throw new EpEnvelopeError("failed-precondition", `accepted pool obligation ${key} has no terminal wrk fact and no live EPW item (its enqueue did not land) and no reconcilePoolRoute was given; a drain never declares quiescence over unmaterialized accepted work (SPEC 13.8)`);
926
+ // The MEDIATOR derives the repair (fact pin 3): read + validate the durable acceptance decision
927
+ // itself, then hand the executor a closed command — never row-supplied coordinates or bytes.
928
+ const decSubject = epfSubject(med.space, med.endpoint, ["dec", cOwner, cActor, cUid, id]);
929
+ const decRaw = await readLastFact(med.jsm, epfStreamName(med.space), decSubject);
930
+ if (decRaw === undefined)
931
+ throw new EpEnvelopeError("internal", `accepted pool obligation ${key} has no decision fact; accepted rows derive from a durable decision (corruption, SPEC 13.8)`);
932
+ const fact = parseDecisionFact(decRaw, decSubject);
933
+ if (fact.decision !== "accepted")
934
+ throw new EpEnvelopeError("internal", `accepted pool obligation ${key} points at a rejected decision fact; obligation/decision state diverged (corruption, SPEC 13.8)`);
935
+ assertDecisionMatchesRow(fact, row, key);
936
+ if (fact.route !== row.route)
937
+ throw new EpEnvelopeError("internal", `the decision fact for ${key} routes "${fact.route}" but the obligation row routes "${row.route}"; state diverged (corruption, SPEC 13.8)`);
938
+ if (typeof fact.workExpiry !== "number")
939
+ throw new EpEnvelopeError("internal", `the accepted pool decision for ${key} pins no workExpiry; a pool item carries its absolute horizon (corruption, SPEC 13.8)`);
940
+ await deps.reconcilePoolRoute({ kind: "pool", subject: itemSubject, bytes: workItemBytesOf(fact), workExpiry: fact.workExpiry });
941
+ if (!(await established()))
942
+ throw new EpEnvelopeError("unavailable", `the reconciler for accepted pool obligation ${key} established neither a terminal wrk fact nor a live EPW item; the route is still unmaterialized and quiescence fails closed (SPEC 13.8)`);
943
+ }
944
+ /** Drain the rows a `counts` predicate SELECTS under `filter` to quiescence (§13.8): settle
945
+ * every counted provisional through its decision coordinate, drive every counted accepted
946
+ * self-class row to terminal, and run the injected route reconciler for every counted accepted
947
+ * EPF row; re-enumerate until a pass finds no counted provisional or accepted-self left. A row
948
+ * the predicate does NOT count is skipped (a policy drain must not settle a target-only row it
949
+ * does not govern). */
950
+ async function drainFilter(med, filter, why, counts, deps = {}) {
951
+ let settledProvisional = 0, recoveredSelf = 0, reconciledAcceptedEpf = 0;
952
+ for (let pass = 1; pass <= 8; pass++) {
953
+ const rows = await enumerateObligations(med, filter);
954
+ let unsettled = 0;
955
+ for (const item of rows) {
956
+ if (!counts(item.row))
957
+ continue; // not this drain's concern (e.g. a target-only row in a policy drain)
958
+ if (item.row.state === "provisional") {
959
+ unsettled++;
960
+ assertObligationOfEndpoint(med, item.key);
961
+ await settleObligation(med, item.key, item.row, item.revision, why);
962
+ settledProvisional++;
963
+ }
964
+ else if (item.row.state === "accepted" && item.row.decision === "self") {
965
+ unsettled++;
966
+ if (deps.applyCommit === undefined)
967
+ throw new EpEnvelopeError("failed-precondition", `the drain found an ACCEPTED self-class obligation ${item.key} but was given no applyCommit; an accepted commit is drivable to terminal and a drain must drive it, never skip it (SPEC 13.8)`);
968
+ await recoverSelfCore(med, item.key, { applyCommit: deps.applyCommit });
969
+ recoveredSelf++;
970
+ }
971
+ else if (item.row.state === "accepted") {
972
+ // Accepted EPF: the drain VERIFIES the route's durable postcondition BEFORE declaring
973
+ // quiescence (an accepted decision whose enqueue never landed must not be silently
974
+ // declared quiescent). The drain OWNS the check (it reads the route marker itself), so a
975
+ // no-op reconciler cannot fake it — for a pool route a missing EPW item calls the
976
+ // injected reconciler and then RE-READS, failing closed if still unmaterialized.
977
+ await verifyAcceptedEpfRoute(med, item.key, item.row, deps);
978
+ reconciledAcceptedEpf++;
979
+ }
980
+ }
981
+ // RE-ENUMERATE (§13.8): quiescence is declared only by an enumeration that finds no counted
982
+ // provisional or un-driven accepted-self row — never by having settled a PREVIOUS enumeration.
983
+ if (unsettled === 0)
984
+ return { passes: pass, settledProvisional, recoveredSelf, reconciledAcceptedEpf };
985
+ }
986
+ throw new EpEnvelopeError("unavailable", `the drain under ${filter} did not reach quiescence in 8 passes; admission traffic is outrunning it; investigate before promoting or retiring (SPEC 13.8)`);
987
+ }
988
+ /** The POLICY drain (§13.6): settles ONLY the obligations pinned to the OLD enforced policy
989
+ * revision (the one being replaced), across the endpoint's whole prefix. Target-only rows and
990
+ * rows pinned to a different revision are NOT this drain's concern (settling them would burn a
991
+ * target-bound acceptance the policy movement does not govern, and target-only traffic would
992
+ * block convergence). Run AFTER {@link stagePolicySelector} (new policy-admitted proofs are
993
+ * paused, so no new old-revision row appears and the drain converges); RE-READS the govern head
994
+ * after draining to confirm the stage did not move, then mints the exact-revision-bound
995
+ * quiescence witness {@link promotePolicySelector} consumes. */
996
+ export async function drainEndpointPolicy(med, deps = {}) {
997
+ const i = internals(med);
998
+ const govKey = recordAtomicKey(GOVERN_HEAD, [i.endpoint]);
999
+ const gov = await readGovernHeadRaw(i.jsm, i.space, i.endpoint);
1000
+ if (gov === undefined)
1001
+ throw new EpEnvelopeError("failed-precondition", `endpoint "${i.endpoint}" has no govern head; the policy drain runs inside a stage → drain → promote mutation (SPEC 13.6)`);
1002
+ const sel = parseSelector(gov.head, govKey);
1003
+ if (sel.pendingPolicyKey === undefined || typeof gov.head.pendingMutationOpId !== "string")
1004
+ throw new EpEnvelopeError("failed-precondition", `endpoint "${i.endpoint}" stages no pending policy; the policy drain runs inside a stage → drain → promote mutation (SPEC 13.6)`);
1005
+ if (sel.enforcedPolicyRevision === undefined)
1006
+ throw new EpEnvelopeError("failed-precondition", `endpoint "${i.endpoint}" enforces no policy to drain from; a first policy has nothing to migrate (SPEC 13.6)`);
1007
+ const retiringRevision = sel.enforcedPolicyRevision;
1008
+ const mutationOpId = gov.head.pendingMutationOpId;
1009
+ const result = await drainFilter(i, `oblig.*.${i.endpoint}.>`, `the enforced policy for "${i.endpoint}" is moving (stage → drain → promote)`, (row) => row.policyRevision === retiringRevision, // ONLY rows pinned to the OLD enforced revision
1010
+ deps);
1011
+ // Confirm the stage did not move under us (a concurrent clear/re-stage): the witness binds the
1012
+ // EXACT current govern revision + mutation opId; the promote CASes from it.
1013
+ const after = await readGovernHeadRaw(i.jsm, i.space, i.endpoint);
1014
+ const afterSel = after === undefined ? {} : parseSelector(after.head, govKey);
1015
+ if (after === undefined || afterSel.pendingPolicyKey !== sel.pendingPolicyKey || afterSel.pendingPolicyRevision !== sel.pendingPolicyRevision || after.head.pendingMutationOpId !== mutationOpId)
1016
+ throw new EpEnvelopeError("conflict", `the pending policy for "${i.endpoint}" moved during its drain (op ${mutationOpId}); re-stage and drain again (SPEC 13.6)`);
1017
+ const quiescence = Object.freeze({
1018
+ space: i.space, endpoint: i.endpoint, pendingPolicyKey: sel.pendingPolicyKey,
1019
+ pendingPolicyRevision: sel.pendingPolicyRevision, mutationOpId, governStageRevision: after.revision,
1020
+ });
1021
+ QUIESCENCE.add(quiescence);
1022
+ return { ...result, quiescence };
1023
+ }
1024
+ /** The RETIREMENT-side drain for THIS endpoint's rows under one target
1025
+ * (`oblig.<targetUid>.<endpoint>.>`): the §13.1 terminal barrier (D13 (5)) runs one per
1026
+ * endpoint found under the target. Both coordinates stay inside this mediator's own grant. */
1027
+ export async function drainTargetForEndpoint(med, targetUid, deps = {}) {
1028
+ const i = internals(med);
1029
+ // A retirement drains EVERY row bound to the target (the prefix is target-scoped and excludes
1030
+ // the `ep` sentinel), regardless of policy pin — a retiring target admits nothing new either way.
1031
+ return drainFilter(i, `oblig.${assertLifecycleToken(targetUid, "targetUid")}.${i.endpoint}.>`, `the target lifecycle ${targetUid} is retiring`, () => true, deps);
1032
+ }
1033
+ //# sourceMappingURL=admission-mediator.js.map