@cotal-ai/core 0.12.0 → 0.13.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.
Files changed (152) hide show
  1. package/dist/acls.d.ts +31 -7
  2. package/dist/acls.d.ts.map +1 -1
  3. package/dist/acls.js +105 -17
  4. package/dist/acls.js.map +1 -1
  5. package/dist/agent-file.js +1 -1
  6. package/dist/agent-file.js.map +1 -1
  7. package/dist/auth-provider.d.ts +8 -0
  8. package/dist/auth-provider.d.ts.map +1 -1
  9. package/dist/auth-provider.js.map +1 -1
  10. package/dist/backup.d.ts.map +1 -1
  11. package/dist/backup.js +6 -4
  12. package/dist/backup.js.map +1 -1
  13. package/dist/broker-floor.d.ts +34 -0
  14. package/dist/broker-floor.d.ts.map +1 -0
  15. package/dist/broker-floor.js +39 -0
  16. package/dist/broker-floor.js.map +1 -0
  17. package/dist/canonical.d.ts +24 -0
  18. package/dist/canonical.d.ts.map +1 -0
  19. package/dist/canonical.js +159 -0
  20. package/dist/canonical.js.map +1 -0
  21. package/dist/connector.d.ts +4 -0
  22. package/dist/connector.d.ts.map +1 -1
  23. package/dist/endpoint-action.d.ts +424 -0
  24. package/dist/endpoint-action.d.ts.map +1 -0
  25. package/dist/endpoint-action.js +910 -0
  26. package/dist/endpoint-action.js.map +1 -0
  27. package/dist/endpoint-binding.d.ts +344 -0
  28. package/dist/endpoint-binding.d.ts.map +1 -0
  29. package/dist/endpoint-binding.js +753 -0
  30. package/dist/endpoint-binding.js.map +1 -0
  31. package/dist/endpoint-checkpoint.d.ts +292 -0
  32. package/dist/endpoint-checkpoint.d.ts.map +1 -0
  33. package/dist/endpoint-checkpoint.js +818 -0
  34. package/dist/endpoint-checkpoint.js.map +1 -0
  35. package/dist/endpoint-cluster.d.ts +95 -0
  36. package/dist/endpoint-cluster.d.ts.map +1 -0
  37. package/dist/endpoint-cluster.js +194 -0
  38. package/dist/endpoint-cluster.js.map +1 -0
  39. package/dist/endpoint-contract-store.d.ts +90 -0
  40. package/dist/endpoint-contract-store.d.ts.map +1 -0
  41. package/dist/endpoint-contract-store.js +348 -0
  42. package/dist/endpoint-contract-store.js.map +1 -0
  43. package/dist/endpoint-envelope.d.ts +165 -0
  44. package/dist/endpoint-envelope.d.ts.map +1 -0
  45. package/dist/endpoint-envelope.js +346 -0
  46. package/dist/endpoint-envelope.js.map +1 -0
  47. package/dist/endpoint-grants.d.ts +123 -0
  48. package/dist/endpoint-grants.d.ts.map +1 -0
  49. package/dist/endpoint-grants.js +215 -0
  50. package/dist/endpoint-grants.js.map +1 -0
  51. package/dist/endpoint-guard.d.ts +222 -0
  52. package/dist/endpoint-guard.d.ts.map +1 -0
  53. package/dist/endpoint-guard.js +468 -0
  54. package/dist/endpoint-guard.js.map +1 -0
  55. package/dist/endpoint-handle.d.ts +176 -0
  56. package/dist/endpoint-handle.d.ts.map +1 -0
  57. package/dist/endpoint-handle.js +608 -0
  58. package/dist/endpoint-handle.js.map +1 -0
  59. package/dist/endpoint-journal.d.ts +225 -0
  60. package/dist/endpoint-journal.d.ts.map +1 -0
  61. package/dist/endpoint-journal.js +431 -0
  62. package/dist/endpoint-journal.js.map +1 -0
  63. package/dist/endpoint-receipt.d.ts +225 -0
  64. package/dist/endpoint-receipt.d.ts.map +1 -0
  65. package/dist/endpoint-receipt.js +417 -0
  66. package/dist/endpoint-receipt.js.map +1 -0
  67. package/dist/endpoint-records.d.ts +243 -0
  68. package/dist/endpoint-records.d.ts.map +1 -0
  69. package/dist/endpoint-records.js +591 -0
  70. package/dist/endpoint-records.js.map +1 -0
  71. package/dist/endpoint-serve.d.ts +176 -0
  72. package/dist/endpoint-serve.d.ts.map +1 -0
  73. package/dist/endpoint-serve.js +446 -0
  74. package/dist/endpoint-serve.js.map +1 -0
  75. package/dist/endpoint-service.d.ts +518 -0
  76. package/dist/endpoint-service.d.ts.map +1 -0
  77. package/dist/endpoint-service.js +973 -0
  78. package/dist/endpoint-service.js.map +1 -0
  79. package/dist/endpoint-session.d.ts +398 -0
  80. package/dist/endpoint-session.d.ts.map +1 -0
  81. package/dist/endpoint-session.js +834 -0
  82. package/dist/endpoint-session.js.map +1 -0
  83. package/dist/endpoint-signing.d.ts +105 -0
  84. package/dist/endpoint-signing.d.ts.map +1 -0
  85. package/dist/endpoint-signing.js +190 -0
  86. package/dist/endpoint-signing.js.map +1 -0
  87. package/dist/endpoint-subjects.d.ts +222 -0
  88. package/dist/endpoint-subjects.d.ts.map +1 -0
  89. package/dist/endpoint-subjects.js +381 -0
  90. package/dist/endpoint-subjects.js.map +1 -0
  91. package/dist/endpoint-supervisor.d.ts +19 -0
  92. package/dist/endpoint-supervisor.d.ts.map +1 -0
  93. package/dist/endpoint-supervisor.js +12 -0
  94. package/dist/endpoint-supervisor.js.map +1 -0
  95. package/dist/endpoint-traits.d.ts +225 -0
  96. package/dist/endpoint-traits.d.ts.map +1 -0
  97. package/dist/endpoint-traits.js +469 -0
  98. package/dist/endpoint-traits.js.map +1 -0
  99. package/dist/endpoint-verbs.d.ts +277 -0
  100. package/dist/endpoint-verbs.d.ts.map +1 -0
  101. package/dist/endpoint-verbs.js +576 -0
  102. package/dist/endpoint-verbs.js.map +1 -0
  103. package/dist/endpoint-virtual.d.ts +308 -0
  104. package/dist/endpoint-virtual.d.ts.map +1 -0
  105. package/dist/endpoint-virtual.js +440 -0
  106. package/dist/endpoint-virtual.js.map +1 -0
  107. package/dist/endpoint-work.d.ts +304 -0
  108. package/dist/endpoint-work.d.ts.map +1 -0
  109. package/dist/endpoint-work.js +728 -0
  110. package/dist/endpoint-work.js.map +1 -0
  111. package/dist/endpoint.d.ts +66 -22
  112. package/dist/endpoint.d.ts.map +1 -1
  113. package/dist/endpoint.js +289 -89
  114. package/dist/endpoint.js.map +1 -1
  115. package/dist/evict.d.ts +95 -32
  116. package/dist/evict.d.ts.map +1 -1
  117. package/dist/evict.js +193 -3
  118. package/dist/evict.js.map +1 -1
  119. package/dist/index.d.ts +25 -0
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +25 -0
  122. package/dist/index.js.map +1 -1
  123. package/dist/lease.d.ts +4 -0
  124. package/dist/lease.d.ts.map +1 -1
  125. package/dist/lease.js.map +1 -1
  126. package/dist/members.d.ts +4 -4
  127. package/dist/members.d.ts.map +1 -1
  128. package/dist/members.js +12 -12
  129. package/dist/members.js.map +1 -1
  130. package/dist/provision.d.ts +70 -18
  131. package/dist/provision.d.ts.map +1 -1
  132. package/dist/provision.js +194 -33
  133. package/dist/provision.js.map +1 -1
  134. package/dist/safe-pattern.d.ts +33 -0
  135. package/dist/safe-pattern.d.ts.map +1 -0
  136. package/dist/safe-pattern.js +486 -0
  137. package/dist/safe-pattern.js.map +1 -0
  138. package/dist/schema-profile.d.ts +93 -0
  139. package/dist/schema-profile.d.ts.map +1 -0
  140. package/dist/schema-profile.js +284 -0
  141. package/dist/schema-profile.js.map +1 -0
  142. package/dist/streams.d.ts +13 -8
  143. package/dist/streams.d.ts.map +1 -1
  144. package/dist/streams.js +35 -18
  145. package/dist/streams.js.map +1 -1
  146. package/dist/subjects.d.ts +100 -40
  147. package/dist/subjects.d.ts.map +1 -1
  148. package/dist/subjects.js +136 -60
  149. package/dist/subjects.js.map +1 -1
  150. package/dist/types.d.ts +9 -0
  151. package/dist/types.d.ts.map +1 -1
  152. package/package.json +3 -1
@@ -0,0 +1,973 @@
1
+ import { spacePrefix, principalKey } from "./subjects.js";
2
+ import { endpointToken, assertBoundedOwner, assertLifecycleToken, assertCommandToken, assertPoolToken, } from "./endpoint-subjects.js";
3
+ import { EpEnvelopeError } from "./endpoint-envelope.js";
4
+ import { RECORD_KINDS, GOVERN_HEAD, recordSpecKey, recordStatusKey, recordAtomicKey, readRecord, recordsBucket, createRecordEntry, updateRecordEntry, assertStatusValue, } from "./endpoint-records.js";
5
+ import { verifyClusterManifest, verifyClusterRoot, deriveDescriptor, GOVERNED_TRAIT_URNS } from "./endpoint-cluster.js";
6
+ import { isSupervisorWrite } from "./endpoint-supervisor.js";
7
+ /** The convergence states the SPEC keys on (§13.6 item 6). */
8
+ export const SERVICE_READY = "ready";
9
+ export const SERVICE_EXITED = "exited";
10
+ /** Restart-intensity escalation (§13.6 virtual endpoints): the instance stops restarting and
11
+ * the lifecycle retires terminally; readers treat it as permanently not-startable. The state
12
+ * is IRREVERSIBLE at {@link writeServiceStatus}: no later status write (any epoch) replaces
13
+ * it — the only touch a stored escalated row admits is the supervisor's own revision-pinned
14
+ * retirement mark, written directly by the reconciler, never through this writer. */
15
+ export const SERVICE_ESCALATED = "escalated";
16
+ /** The SUPERVISOR-OWNED status fields (§13.6 restart intensity): the durable restart history
17
+ * and the retirement-complete mark. {@link writeServiceStatus} carries them forward on every
18
+ * INSTANCE-side write and strips whatever the caller supplied; ONLY a holder of the branded
19
+ * {@link SupervisorWriteGrant} may originate them or the `escalated` state. */
20
+ export const SERVICE_RESTART_HISTORY_FIELD = "restarts";
21
+ export const SERVICE_RETIRED_MARK_FIELD = "retiredAt";
22
+ export function parseActivationPolicy(raw) {
23
+ const o = isRec(raw) ? raw : svcFail("activation policy is not an object");
24
+ for (const k of Object.keys(o))
25
+ if (!["mode", "capacity", "maxRestarts", "restartWindowMs"].includes(k))
26
+ svcFail(`activation policy carries unknown field "${k}" (closed schema)`);
27
+ if (o.mode !== "on-demand")
28
+ svcFail(`activation.mode "${String(o.mode)}" is not "on-demand"`);
29
+ if (typeof o.capacity !== "number" || !Number.isSafeInteger(o.capacity) || o.capacity <= 0)
30
+ svcFail("activation.capacity must be a positive integer (a virtual pool is bounded by policy, never open-ended)");
31
+ if (o.maxRestarts !== undefined && (typeof o.maxRestarts !== "number" || !Number.isSafeInteger(o.maxRestarts) || o.maxRestarts <= 0))
32
+ svcFail("activation.maxRestarts must be a positive integer");
33
+ if (o.restartWindowMs !== undefined && (typeof o.restartWindowMs !== "number" || !Number.isSafeInteger(o.restartWindowMs) || o.restartWindowMs <= 0))
34
+ svcFail("activation.restartWindowMs must be a positive integer");
35
+ return o;
36
+ }
37
+ const STATE_TOKEN = /^[a-z][a-z0-9-]{0,31}$/;
38
+ const isRec = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
39
+ const wireInt = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
40
+ const isDigest = (v) => typeof v === "string" && /^sha256:[0-9a-f]{64}$/.test(v);
41
+ function svcFail(what) {
42
+ throw new EpEnvelopeError("internal", `service record does not validate: ${what}`);
43
+ }
44
+ /** Validate a `svc….spec` value at its consuming boundary (§13.3: every plane is
45
+ * runtime-validated; mediated-writer state that does not validate is a writer bug, never a
46
+ * data error). The body's endpoint must AGREE with the key's endpoint qualifier. */
47
+ export function parseServiceSpec(raw, key) {
48
+ const o = isRec(raw) ? raw : svcFail("not an object");
49
+ if (typeof o.endpoint !== "string")
50
+ svcFail("endpoint");
51
+ if (endpointToken(o.endpoint) !== endpointToken(key.endpoint))
52
+ svcFail("endpoint disagrees with the record key");
53
+ if (typeof o.owner !== "string")
54
+ svcFail("owner");
55
+ try {
56
+ assertBoundedOwner(o.owner, "service owner");
57
+ }
58
+ catch (e) {
59
+ svcFail(`owner: ${e.message}`);
60
+ }
61
+ if (o.endpointType !== undefined && typeof o.endpointType !== "string")
62
+ svcFail("endpointType");
63
+ if (!Array.isArray(o.clusterDigests) || o.clusterDigests.length === 0 || !o.clusterDigests.every(isDigest))
64
+ svcFail("clusterDigests must be a non-empty array of sha256 digests");
65
+ if (!isRec(o.protocol) || o.protocol.v !== 1)
66
+ svcFail("protocol.v");
67
+ if (o.activation !== undefined)
68
+ parseActivationPolicy(o.activation); // closed schema, capacity REQUIRED (§13.6)
69
+ return o;
70
+ }
71
+ /** Validate a `svc….status` value at its consuming boundary. */
72
+ export function parseServiceStatus(raw) {
73
+ const o = isRec(raw) ? raw : svcFail("status not an object");
74
+ if (!wireInt(o.epoch))
75
+ svcFail("status.epoch");
76
+ if (typeof o.state !== "string" || !STATE_TOKEN.test(o.state))
77
+ svcFail("status.state");
78
+ if (!wireInt(o.observedSpecRevision))
79
+ svcFail("status.observedSpecRevision");
80
+ // The SUPERVISOR-OWNED fields are validated at the consuming boundary too, so corrupt/legacy
81
+ // state never rides through the escalation barrier (§13.6): the restart history is an array
82
+ // of {t, epoch} with UNIQUE epochs (a real restart advances the epoch; a duplicate is a
83
+ // fabricated count), and the retirement mark is a non-negative integer that appears ONLY on an
84
+ // escalated row (a mark on a live row would let a forged `retiredAt` fake completion).
85
+ if (o[SERVICE_RESTART_HISTORY_FIELD] !== undefined) {
86
+ const h = o[SERVICE_RESTART_HISTORY_FIELD];
87
+ if (!Array.isArray(h) || !h.every((e) => isRec(e) && wireInt(e.t) && wireInt(e.epoch)))
88
+ svcFail(`status.${SERVICE_RESTART_HISTORY_FIELD} is not an array of {t, epoch}`);
89
+ const epochs = new Set(h.map((e) => e.epoch));
90
+ if (epochs.size !== h.length)
91
+ svcFail(`status.${SERVICE_RESTART_HISTORY_FIELD} has duplicate epochs (a fabricated count)`);
92
+ }
93
+ if (o[SERVICE_RETIRED_MARK_FIELD] !== undefined) {
94
+ if (!wireInt(o[SERVICE_RETIRED_MARK_FIELD]))
95
+ svcFail(`status.${SERVICE_RETIRED_MARK_FIELD} is not a non-negative integer`);
96
+ if (o.state !== SERVICE_ESCALATED)
97
+ svcFail(`status.${SERVICE_RETIRED_MARK_FIELD} is present on a "${String(o.state)}" row; a retirement mark appears only on an escalated row (§13.6)`);
98
+ }
99
+ return o;
100
+ }
101
+ /** Enforce §13.9 name authority before a registration/serve grant is minted, from ONE atomic
102
+ * snapshot: an endpoint name binds to exactly ONE owner (§13.2), so a registration claiming a
103
+ * name its owner does not hold fails `permission-denied`, and an UNREGISTERED reverse-DNS name
104
+ * fails closed. Returns the name-authority binding REVISION read atomically WITH the decision —
105
+ * the caller binds it into the issuance gate so a transfer between decision and mint is fenced,
106
+ * never a torn owner-vs-revision read. */
107
+ export async function assertServiceNameAuthority(endpoint, owner, authority) {
108
+ endpointToken(endpoint); // grammar first: a malformed name is refused before any authority answer
109
+ assertBoundedOwner(owner, "service owner");
110
+ const snapshot = await authority.authorize(endpoint, owner);
111
+ if (!snapshot.authorized)
112
+ throw new EpEnvelopeError("permission-denied", `service name "${endpoint}" does not authorize owner "${owner}" (SPEC 13.9: a core name needs operator authority; a reverse-DNS name binds to its registered owner and an unregistered one is never adopted first-come)`);
113
+ if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0)
114
+ throw new EpEnvelopeError("internal", `the name-authority revision for "${endpoint}" is ${JSON.stringify(snapshot.revision)}, not an unsigned integer`);
115
+ return snapshot.revision;
116
+ }
117
+ // ---- registration (spec writes, the `provisioner-registration` principal) ---------------------
118
+ /** Register (or re-register) a service instance: authenticated-registrant binding, name
119
+ * authority, then the spec-key CAS. The returned `registrationRevision` is the spec key's
120
+ * store revision (§13.7) — a re-registration advances it, which is exactly what invalidates a
121
+ * frozen scatter slot (§13.5 `churn`). A concurrent registration race is a loud `conflict`
122
+ * (§13.8: re-read and re-decide).
123
+ *
124
+ * `registrant` is the BROKER-AUTHENTICATED caller of the registration request (its subject
125
+ * principal, §13.9 — never a payload claim): the descriptor owner must BE that caller, so a
126
+ * privileged owner's descriptor cannot be registered by anyone else, and a re-registration can
127
+ * never change an instance's ownership. `instanceId` MUST be provisioner-minted and never
128
+ * reused (§13.1); the allocator that enforces non-reuse is the lifecycle registry (D13) — this
129
+ * seam enforces what is checkable at the record: grammar, ownership stability, and CAS.
130
+ *
131
+ * ISSUANCE-GATE BARRIER (§13.1). A registration is a WRITER on the instance's issuance gate: to
132
+ * be linearizable against an in-flight serve mint it MUST run the barrier protocol on the SAME
133
+ * `gate.<lifecycleUid>` key, in order: freeze the gate (so a fresh mint observes `frozen` and
134
+ * refuses, and a staged-but-uncommitted mint loses its revision-pinned CAS), authorize the owner
135
+ * under the frozen gate, revoke + VERIFIED-evict the superseded credential family, THEN advance
136
+ * the spec, then reopen at the successor `registrationRevision`. Old authority dies before new
137
+ * authority is published. This is REQUIRED, not documented: core exports no bare spec-key advance
138
+ * that could leave a mint's observed `registrationRevision` permanently equal to its snapshot,
139
+ * win a never-frozen CAS, and silently release a superseded-surface credential. The gate is
140
+ * created by the provisioner at instance mint (D13); a missing gate is `failed-precondition`. The
141
+ * production `barrier` wires to the durable KV CAS (D13/D14); the D4 seam is the typed protocol
142
+ * and its faithful in-memory model, so the barrier's writes serialize with the mint's on one key. */
143
+ /** Reconstruct a registered spec's command surface from trusted registry + content-addressed
144
+ * store state (§13.7): EVERY command name -> the set of governed URNs its verified cluster
145
+ * document declares (an empty set for an un-governed command; the full command set is needed so
146
+ * continuity can tell a STRIPPED-but-surviving command from a REMOVED one). The registrar drives
147
+ * this, so the OWNER never supplies the prior state that continuity compares against - it is read
148
+ * from the mediated spec + the digest-verified cluster bytes. The governed set is the canonical
149
+ * {@link GOVERNED_TRAIT_URNS}, pinned STRUCTURALLY - a caller-supplied set was the
150
+ * subset-narrowing escape (pass guarded-only and a priced imposition is never recorded), the
151
+ * same class as the required-but-empty policy the panel already rejected. */
152
+ async function readGovernedDeclarations(readArtifact, clusterDigests) {
153
+ const out = new Map();
154
+ const classes = new Map();
155
+ const read = async (digest) => {
156
+ const raw = await readArtifact(digest);
157
+ if (raw === undefined)
158
+ throw new EpEnvelopeError("failed-precondition", `governed-continuity: cluster artifact ${digest} is not readable; an unverifiable prior/next surface cannot authorize a governance change (SPEC 13.7)`);
159
+ return raw;
160
+ };
161
+ for (const closureDigest of clusterDigests) {
162
+ const { root } = verifyClusterManifest(closureDigest, await read(closureDigest));
163
+ const document = verifyClusterRoot(root, await read(root));
164
+ for (const cmd of document.commands) {
165
+ const set = out.get(cmd.name) ?? new Set();
166
+ for (const t of (cmd.traits ?? []))
167
+ if (GOVERNED_TRAIT_URNS.includes(t))
168
+ set.add(t);
169
+ // A command name is DECLARED ONCE across the whole closure: a cross-cluster duplicate is
170
+ // an ambiguous surface (serve authorization rejects it later as internal-ambiguous), so
171
+ // registration refuses it up front rather than publishing a surface that cannot serve.
172
+ if (out.has(cmd.name))
173
+ throw new EpEnvelopeError("failed-precondition", `command "${cmd.name}" is declared in more than one cluster of the closure; a duplicate command name is an ambiguous surface (SPEC 13.7)`);
174
+ out.set(cmd.name, set); // present for EVERY command, governed or not
175
+ classes.set(cmd.name, cmd.class);
176
+ }
177
+ }
178
+ return { governed: out, classes };
179
+ }
180
+ /** Governed-continuity at the mediated registration write (§13.7: removal/downgrade is an
181
+ * AUTHORIZED contract revision). For every command the NEW spec DECLARES, its governed-trait
182
+ * set must be a SUPERSET of the endpoint's recorded governance for that command: a
183
+ * self-published descriptor cannot strip an authority-imposed annotation. A command the new
184
+ * spec does NOT declare (removed) keeps its recorded governance as a TOMBSTONE (so a later
185
+ * re-add ungoverned still refuses) but does not itself refuse here; a NEW command and an ADDED
186
+ * trait are fine.
187
+ *
188
+ * `prior` is the ENDPOINT-WIDE governance head ({@link readEndpointGovernance}), NOT a single
189
+ * instance's prior spec: a per-instance head compare is defeated by three launder paths a
190
+ * history-bearing endpoint record closes — a FRESH instanceId (no prior head of its own), a
191
+ * REMOVE→RE-ADD across two revisions (the intermediate head carries no governance), and the
192
+ * optional-policy omission. (The "authority AUTHORIZES stopping governance" path needs the
193
+ * authority's own consent artifact, the D18 governance-consent record; until then a governed
194
+ * trait can be lifted by no owner-driven path at all, fail-closed.) */
195
+ function assertGovernedDeclarationContinuity(prior, next) {
196
+ for (const [command, priorTraits] of prior) {
197
+ if (priorTraits.size === 0)
198
+ continue; // was un-governed - nothing to carry forward
199
+ const nextTraits = next.get(command);
200
+ if (nextTraits === undefined)
201
+ continue; // the command is not declared by the new spec (removed) - tombstone persists, not a strip
202
+ for (const urn of priorTraits)
203
+ if (!nextTraits.has(urn))
204
+ throw new EpEnvelopeError("permission-denied", `registration drops governed trait "${urn}" from command "${command}", which the endpoint's governance record still imposes; a self-published descriptor cannot strip an authority-imposed annotation via re-registration, a fresh instance, or a remove-then-re-add - land an authority-authorized revision (SPEC 13.7)`);
205
+ }
206
+ }
207
+ function parseGovernanceCommands(commands, key, what) {
208
+ if (commands === null || typeof commands !== "object" || Array.isArray(commands))
209
+ throw new EpEnvelopeError("internal", `the endpoint governance head ${key} has a non-object ${what} map; garbled mediated governance state never authorizes (SPEC 13.7)`);
210
+ const out = new Map();
211
+ for (const [command, urns] of Object.entries(commands)) {
212
+ if (!Array.isArray(urns) || !urns.every((u) => typeof u === "string" && u.length > 0))
213
+ throw new EpEnvelopeError("internal", `the endpoint governance head ${key} maps ${what} command "${command}" to a non-string-array; garbled state never authorizes (SPEC 13.7)`);
214
+ out.set(command, new Set(urns));
215
+ }
216
+ return out;
217
+ }
218
+ function parseEndpointGovernance(raw, key) {
219
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw) || !("commands" in raw))
220
+ throw new EpEnvelopeError("internal", `the endpoint governance head ${key} is not a { commands } record; garbled mediated governance state never authorizes (SPEC 13.7)`);
221
+ const commands = parseGovernanceCommands(raw.commands, key, "binding");
222
+ const p = raw.provisional;
223
+ if (p === undefined)
224
+ return { commands, provisional: null };
225
+ if (p === null || typeof p !== "object" || Array.isArray(p))
226
+ throw new EpEnvelopeError("internal", `the endpoint governance head ${key} has a non-object provisional slot; garbled state never authorizes (SPEC 13.7)`);
227
+ const slot = p;
228
+ if (typeof slot.instanceId !== "string" || slot.instanceId.length === 0)
229
+ throw new EpEnvelopeError("internal", `the endpoint governance head ${key} provisional slot has no holder instanceId; garbled state never authorizes (SPEC 13.7)`);
230
+ if (typeof slot.generation !== "number" || !Number.isSafeInteger(slot.generation) || slot.generation < 0)
231
+ throw new EpEnvelopeError("internal", `the endpoint governance head ${key} provisional slot has a non-integer generation; garbled state never authorizes (SPEC 13.7)`);
232
+ return {
233
+ commands,
234
+ provisional: { instanceId: slot.instanceId, generation: slot.generation, commands: parseGovernanceCommands(slot.commands, key, "provisional") },
235
+ };
236
+ }
237
+ /** Read the endpoint-wide governance head fresh under the frozen gate (a KV get, not a fence -
238
+ * the fence is the slot-take CAS that follows): binding impositions + the in-flight provisional
239
+ * slot, plus the store `revision` the slot-take must CAS against (`null` = the head does not
240
+ * exist yet). Fail-closed on anything but a clean read: a garbled head is `internal`, and a
241
+ * DEL/PURGE marker is REFUSED, never treated as a virgin head - the KV client's get() returns
242
+ * deletion markers and its create() recreates over them, so mapping non-PUT to "no history"
243
+ * would let whoever can delete the key erase every tombstone and register a stripped surface
244
+ * against a reset record. Only TRUE ABSENCE is virgin; a deletion marker on a monotonic
245
+ * history-bearing authority record is tampering or a storage fault to reconcile, never
246
+ * authorization to forget (SPEC 13.7). */
247
+ async function readEndpointGovernance(kv, endpoint) {
248
+ const key = recordAtomicKey(GOVERN_HEAD, [endpoint]);
249
+ const entry = await kv.get(key);
250
+ if (!entry)
251
+ return { commands: new Map(), provisional: null, revision: null };
252
+ if (entry.operation !== "PUT")
253
+ throw new EpEnvelopeError("failed-precondition", `the endpoint governance head ${key} carries a ${entry.operation} marker; a monotonic history-bearing record is never deleted, so this is tampering or a storage fault - reconcile the head before registering, a deletion never resets governance history (SPEC 13.7)`);
254
+ return { ...parseEndpointGovernance(decodeJson(entry.value, key), key), revision: entry.revision };
255
+ }
256
+ /** Union the new spec's governed declarations into the binding governance (monotonic: a governed
257
+ * trait is only ever ADDED, never dropped; un-governed commands are not recorded). This is the
258
+ * PROMOTE content - written only after the spec publish commits. */
259
+ function mergeEndpointGovernance(prior, next) {
260
+ const merged = new Map();
261
+ for (const [command, urns] of prior)
262
+ merged.set(command, new Set(urns));
263
+ for (const [command, urns] of next) {
264
+ if (urns.size === 0)
265
+ continue; // un-governed commands are not recorded (only impositions)
266
+ const into = merged.get(command) ?? new Set();
267
+ for (const urn of urns)
268
+ into.add(urn);
269
+ merged.set(command, into);
270
+ }
271
+ return merged;
272
+ }
273
+ function serializeGovernanceCommands(commands) {
274
+ const out = {};
275
+ for (const [command, urns] of commands)
276
+ if (urns.size > 0)
277
+ out[command] = [...urns].sort();
278
+ return out;
279
+ }
280
+ export async function registerServiceInstance(kv, args) {
281
+ if (typeof args.readClusterArtifact !== "function")
282
+ throw new EpEnvelopeError("failed-precondition", "registerServiceInstance requires a content-store reader (readClusterArtifact); governed-continuity is not an optional seam (SPEC 13.7)");
283
+ spacePrefix(args.space); // up-front boundary guard on the space arg (mirrors authorizeServeGrant): usable as a subject token, throws on an absent/non-string space at an untyped caller. This is NOT the cross-space authority fence - that is the observed-gate `(space, endpoint, instanceId)` identity check below (trusted-context equality against the per-space KV bucket).
284
+ const spec = parseServiceSpec(args.spec, { endpoint: args.spec.endpoint });
285
+ assertBoundedOwner(args.registrant.owner, "registrant owner");
286
+ if (args.registrant.owner !== spec.owner)
287
+ throw new EpEnvelopeError("permission-denied", `the registration's authenticated caller "${args.registrant.owner}" is not the descriptor owner "${spec.owner}" (SPEC 13.9: authenticated caller binding, never a payload claim)`);
288
+ // The NAME-AUTHORITY decision is deferred until UNDER the frozen gate (phase 1): a transfer must
289
+ // freeze this same gate, so authorizing while we hold the freeze serializes the decision with the
290
+ // transfer — checking here (pre-freeze) would repeat the torn owner-vs-revision read the atomic
291
+ // authorize() closed for authorizeServeGrant.
292
+ const key = recordSpecKey(RECORD_KINDS.svc, [spec.endpoint, assertLifecycleToken(args.instanceId, "instanceId")]);
293
+ // §13.1 barrier: freeze the instance's gate FIRST so no serve mint can win against the surface
294
+ // this registration is about to supersede. A non-open gate or a lost freeze is another barrier
295
+ // holding the key — a loud `conflict` (§13.8: re-read and re-decide), never a bare write.
296
+ const obs = await args.barrier.observe();
297
+ if (obs === null)
298
+ throw new EpEnvelopeError("failed-precondition", `no issuance gate for instance "${args.instanceId}"; a registration writes only behind the provisioner-created gate (SPEC 13.1)`);
299
+ if (obs.space !== args.space || obs.endpoint !== spec.endpoint || obs.lifecycleUid !== args.instanceId)
300
+ throw new EpEnvelopeError("internal", `the issuance gate is for "${obs.space}/${obs.endpoint}/${obs.lifecycleUid}", not "${args.space}/${spec.endpoint}/${args.instanceId}"; a registration drives only its OWN instance's gate, and the instance token is unique only within (space, endpoint) (SPEC 13.1)`);
301
+ if (obs.state === "retired")
302
+ throw new EpEnvelopeError("failed-precondition", `the issuance gate for "${args.instanceId}" is retired; the lifecycle is permanently closed and its id is never reused, so a re-read cannot help (SPEC 13.1)`);
303
+ if (obs.state !== "open")
304
+ throw new EpEnvelopeError("conflict", `the issuance gate for "${args.instanceId}" is ${obs.state}; another barrier holds it; re-read and re-decide (SPEC 13.8)`);
305
+ const token = await args.barrier.freeze(obs.revision);
306
+ if (token === null)
307
+ throw new EpEnvelopeError("conflict", `a concurrent barrier froze the issuance gate for "${args.instanceId}" first; re-read and re-decide (SPEC 13.1/13.8)`);
308
+ // The gate is frozen; every exit below reopens it (token-pinned, at the original coordinate) or
309
+ // deliberately leaves it FROZEN for reconciliation. The successor the completing reopen targets.
310
+ const successorAt = (registrationRevision) => ({
311
+ generation: obs.generation + 1, processEpoch: obs.processEpoch, registrationRevision, nameAuthorityRevision: obs.nameAuthorityRevision,
312
+ });
313
+ // PHASE 1 — authorize UNDER the frozen gate, then ownership stability. Both are authority /
314
+ // local reads with NO write side-effect, so any failure (owner not authorized, name-authority
315
+ // drift, a garbled stored spec, an ownership change) is a DEFINITE no-write and no revoke has
316
+ // run → reopen the ORIGINAL coordinate and rethrow.
317
+ // - the name-authority decision is made HERE (holding the freeze), and the authorized revision
318
+ // MUST equal the frozen gate's `nameAuthorityRevision`: a transfer that raced would have to
319
+ // freeze this same gate (it can't) or would leave the gate at a different coordinate, so a
320
+ // mismatch is a raced transfer — a loud `conflict`, never a stale-owner registration.
321
+ let current;
322
+ const govKey = recordAtomicKey(GOVERN_HEAD, [spec.endpoint]);
323
+ // Assigned in PHASE 1 on every non-throwing path (the slot-take is PHASE 1's last act).
324
+ let governSlotRevision;
325
+ let governPromote;
326
+ try {
327
+ const authorizedNameRevision = await assertServiceNameAuthority(spec.endpoint, spec.owner, args.authority);
328
+ if (authorizedNameRevision !== obs.nameAuthorityRevision)
329
+ throw new EpEnvelopeError("conflict", `a name-authority transfer raced this registration: owner "${spec.owner}" is authorized at nameAuthorityRevision ${authorizedNameRevision} but the frozen gate is at ${obs.nameAuthorityRevision}; re-read and re-decide (SPEC 13.9)`);
330
+ current = await kv.get(key);
331
+ if (current && current.operation === "PUT") {
332
+ const stored = parseServiceSpec(decodeJson(current.value, key), { endpoint: spec.endpoint });
333
+ if (stored.owner !== spec.owner)
334
+ throw new EpEnvelopeError("permission-denied", `instance "${args.instanceId}" is registered to owner "${stored.owner}"; a re-registration can never change ownership (SPEC 13.1: instance ids are never reused across identities)`);
335
+ }
336
+ // §13.7 ENDPOINT-WIDE governed-continuity, run on EVERY registration (a first registration
337
+ // included — that is where a fresh-instance strip is caught). The governance head is BOTH the
338
+ // history-bearing imposition record AND the endpoint's registration linearization point:
339
+ // - read it fresh under the frozen gate; refuse if a FOREIGN registration holds its
340
+ // provisional slot (`conflict`, re-read and re-decide — per-instance gates do not mutually
341
+ // exclude across instances, the slot does);
342
+ // - validate the new spec's digest-verified declarations against the BINDING impositions
343
+ // (fresh-instance, remove→re-add, and stripped-but-surviving all refuse here);
344
+ // - CAS-TAKE the slot — every registration takes it, an ungoverned/`changed:false` one
345
+ // included, or a pure head READER could decide against one governance state and publish
346
+ // under another (the cross-instance launder). The slot is stamped with this gate's frozen
347
+ // `generation`; it is HELD through the spec publish and PROMOTED to binding only after the
348
+ // publish commits, so no imposition ever binds for a descriptor that never published (the
349
+ // phantom-obligation orphan) and no publish ever slips past a concurrent imposition.
350
+ // Every failure in this block is a DEFINITE no-spec-write, so the outer catch reopens the
351
+ // ORIGINAL coordinate: a slot-take CAS loss is a raced endpoint registration (`conflict`);
352
+ // an AMBIGUOUS slot-take is also safe to reopen because a committed-but-unacked slot is
353
+ // stamped with the generation this reopen advances PAST — the stale stamp marks it orphaned,
354
+ // this instance's own retry replaces it, and a foreign registration refuses on it until then
355
+ // (fail-closed, reclaimed by retry or by the D13 reconciler; predicate: the stamped gate
356
+ // coordinate is not frozen at that generation).
357
+ const gov = await readEndpointGovernance(kv, spec.endpoint);
358
+ if (gov.provisional) {
359
+ if (gov.provisional.instanceId !== args.instanceId)
360
+ throw new EpEnvelopeError("conflict", `a concurrent registration for endpoint "${spec.endpoint}" (instance "${gov.provisional.instanceId}") holds the governance slot through its spec publication; re-read and re-decide; if its holder aborted pre-publish its stale-generation slot is reclaimed by that instance's retry or by reconciliation (SPEC 13.7/13.8)`);
361
+ if (gov.provisional.generation >= obs.generation)
362
+ throw new EpEnvelopeError("internal", `the governance slot for endpoint "${spec.endpoint}" is held by this very instance at generation ${gov.provisional.generation} while its gate is frozen at ${obs.generation}; a live slot under a re-frozen gate cannot exist (every retry freezes at an advanced generation); reconcile the head before registering (SPEC 13.7)`);
363
+ // else: this instance's OWN orphan from an aborted earlier attempt (the gate has reopened
364
+ // past its stamp since) — the slot-take below replaces it.
365
+ }
366
+ const { governed: next, classes } = await readGovernedDeclarations(args.readClusterArtifact, spec.clusterDigests);
367
+ // §13.6: a VIRTUAL endpoint's commands MUST be journal-class — an ephemeral call to an
368
+ // endpoint with no live instance is an honest `unavailable`, so registering an on-demand
369
+ // activation policy over an ephemeral command would advertise a surface that cannot exist.
370
+ if (spec.activation !== undefined) {
371
+ for (const [name, cls] of classes) {
372
+ if (cls !== "journal")
373
+ throw new EpEnvelopeError("failed-precondition", `endpoint "${spec.endpoint}" registers on-demand activation but declares the ${cls}-class command "${name}"; a virtual endpoint's commands MUST be journal-class (SPEC 13.6)`);
374
+ }
375
+ }
376
+ assertGovernedDeclarationContinuity(gov.commands, next);
377
+ governPromote = { commands: serializeGovernanceCommands(mergeEndpointGovernance(gov.commands, next)) };
378
+ const slotValue = {
379
+ commands: serializeGovernanceCommands(gov.commands),
380
+ provisional: { instanceId: args.instanceId, generation: obs.generation, commands: serializeGovernanceCommands(next) },
381
+ };
382
+ try {
383
+ governSlotRevision = gov.revision === null
384
+ ? await createRecordEntry(kv, govKey, slotValue)
385
+ : await updateRecordEntry(kv, govKey, slotValue, gov.revision);
386
+ }
387
+ catch (e) {
388
+ // createRecordEntry/updateRecordEntry translate the broker CAS loss (err_code 10071/10164)
389
+ // into EpEnvelopeError("conflict") — classify on THAT, the numeric code never reaches here.
390
+ if (e instanceof EpEnvelopeError && e.code === "conflict")
391
+ throw new EpEnvelopeError("conflict", `a concurrent registration for endpoint "${spec.endpoint}" took the governance slot first (a definite no-write CAS loss); re-read and re-decide (SPEC 13.7/13.8)`);
392
+ throw new EpEnvelopeError("unavailable", `the governance slot-take for endpoint "${spec.endpoint}" is ambiguous; the registration aborts before any spec write and the gate reopens; a committed-but-unacked slot self-orphans at the reopened generation and this instance's retry replaces it (SPEC 13.7): ${e?.message ?? String(e)}`);
393
+ }
394
+ }
395
+ catch (err) {
396
+ await reopenGateAfterAbort(args.barrier, token, successorAt(obs.registrationRevision), err);
397
+ throw err;
398
+ }
399
+ // PHASE 2 — revoke + VERIFIED eviction of the superseded family BEFORE publishing the new spec
400
+ // (§13.1 order: old authority must die before new authority is visible). Fail-closed: if any
401
+ // revoke/eviction cannot be verified, leave the gate FROZEN for reconciliation — never reopen,
402
+ // or old credentials could come back to life against a pending re-registration.
403
+ // - revoke every ACTIVE row (an already-`revoked` row was flipped by an earlier barrier);
404
+ // - but verified-evict the distinct holder principals of the ENTIRE enumerated family: an
405
+ // already-revoked row from a PARTIALLY FAILED prior barrier may still have a live connection
406
+ // that was never verified gone, so eviction must not skip it (§13.1).
407
+ try {
408
+ const family = await args.barrier.enumerate();
409
+ for (const row of family)
410
+ if (row.state === "active")
411
+ await args.barrier.revoke(row);
412
+ for (const holderPrincipal of new Set(family.map((row) => row.holderPrincipal)))
413
+ if (!(await args.barrier.evict(holderPrincipal)))
414
+ throw new Error(`principal "${holderPrincipal}" is not verified evicted`);
415
+ }
416
+ catch (err) {
417
+ throw new EpEnvelopeError("unavailable", `re-registration could not revoke + verify-evict the superseded serve family; the gate is left frozen for reconciliation, no new spec published (SPEC 13.1): ${err?.message ?? String(err)}`);
418
+ }
419
+ // PHASE 3 — publish the new spec. ANY write error stays FROZEN for reconciliation, never
420
+ // reopening the old coordinate: the KV may have committed while the ack was lost (an ambiguous
421
+ // outcome), and reopening old would release stale-surface credentials against a spec that
422
+ // advanced. Under the frozen gate THIS barrier is the sole spec-key writer, so a write error is
423
+ // genuinely infra/ambiguous — never a concurrent-CAS loss we could treat as a definite no-write.
424
+ let newRev;
425
+ try {
426
+ newRev = current && current.operation === "PUT"
427
+ ? await updateRecordEntry(kv, key, spec, current.revision)
428
+ : await createRecordEntry(kv, key, spec);
429
+ }
430
+ catch (err) {
431
+ throw new EpEnvelopeError("unavailable", `the re-registration spec-write outcome is ambiguous (it may have committed); the gate is left frozen for reconciliation, never reopened at the old coordinate (SPEC 13.1): ${err?.message ?? String(err)}`);
432
+ }
433
+ // PHASE 3b — PROMOTE the governance slot to binding, now that the spec publish committed: the
434
+ // held provisional impositions merge into the binding map and the slot clears. Only HERE does
435
+ // an imposition become permanent, so a registration that failed in PHASE 2/3 never binds
436
+ // governance for a descriptor that never published (the phantom-obligation orphan), and the
437
+ // slot's hold from decision through publish is what serializes every concurrent registration
438
+ // of this endpoint. Nothing else can have CAS'd the head while we held the slot (a foreign
439
+ // registration refuses on it), so ANY failure — a lost ack, or a CAS loss to a reconciler that
440
+ // stole the slot — leaves the gate FROZEN for reconciliation, consistent with PHASE 3: the
441
+ // spec is published, so reopening without the promote would activate a surface whose
442
+ // imposition never bound. The promote is idempotent for the reconciler (re-CAS the same merge).
443
+ try {
444
+ await updateRecordEntry(kv, govKey, governPromote, governSlotRevision);
445
+ }
446
+ catch (err) {
447
+ throw new EpEnvelopeError("unavailable", `the spec for "${args.instanceId}" is published at revision ${newRev} but the governance promote did not complete; the gate is left frozen for reconciliation (the promote is an idempotent re-CAS of the held slot to binding, SPEC 13.7/13.1): ${err?.message ?? String(err)}`);
448
+ }
449
+ // PHASE 4 — reopen at the successor, TOKEN-pinned: only this barrier (still holding its freeze)
450
+ // may reopen; a lost CAS means a reconciler/newer barrier superseded us → leave frozen.
451
+ try {
452
+ if (!(await args.barrier.reopen(token, successorAt(newRev))))
453
+ throw new Error("the reopen CAS lost its freeze token (a reconciler or newer barrier superseded this one)");
454
+ }
455
+ catch (err) {
456
+ throw new EpEnvelopeError("unavailable", `re-registration wrote the spec at revision ${newRev} but the reopen did not complete; the gate is left frozen for reconciliation (SPEC 13.1): ${err?.message ?? String(err)}`);
457
+ }
458
+ return { registrationRevision: newRev };
459
+ }
460
+ /** Reopen a barrier-frozen gate (token-pinned) after a registration aborted before any spec write
461
+ * or revoke — the gate returns to `open` at the given successor. A lost or failed reopen leaves
462
+ * the gate frozen for reconciliation and is surfaced with the aborting cause attached (§13.1:
463
+ * fail closed, never a silently stuck gate). */
464
+ async function reopenGateAfterAbort(barrier, token, successor, cause) {
465
+ try {
466
+ if (await barrier.reopen(token, successor))
467
+ return;
468
+ throw new Error("the reopen CAS lost its freeze token");
469
+ }
470
+ catch (err) {
471
+ const e = new EpEnvelopeError("unavailable", `registration aborted and the issuance gate could not be reopened; it is left frozen for reconciliation (SPEC 13.1): ${err?.message ?? String(err)}`);
472
+ e.cause = cause;
473
+ throw e;
474
+ }
475
+ }
476
+ function decodeJson(value, key) {
477
+ try {
478
+ return JSON.parse(new TextDecoder().decode(value));
479
+ }
480
+ catch (e) {
481
+ throw new EpEnvelopeError("internal", `record ${key} does not decode as JSON: ${e.message}`);
482
+ }
483
+ }
484
+ /** Write an instance's status with the FULL §13.9 writer fence. `epoch` is the
485
+ * WRITER-AUTHENTICATED epoch — in production the record writer reads it from the
486
+ * broker-authenticated `epr` subject (§13.9), never from the payload; this helper trusts its
487
+ * caller to be that seam and additionally requires the payload to agree. The fence is
488
+ * THREE-part, in order:
489
+ * 1. a registered spec must exist and `observedSpecRevision` must not run AHEAD of it — a
490
+ * spec-less status is the torn record state readers reject (§13.4), never written;
491
+ * 2. the epoch must equal a FRESH read of the authoritative lifecycle mapping's
492
+ * `processEpoch` (`expired` otherwise) — monotonicity against the stored status alone is
493
+ * NOT sufficient: between the takeover CAS (N→N+1) and the completed revoke/evict barrier
494
+ * the superseded N still equals the stored epoch (§13.9);
495
+ * 3. a below-stored epoch is `conflict` (§13.9), distinct from the mapping fence.
496
+ * `readProcessEpoch` is the trusted mapping-reader seam (leader-served, §13.9; the D13
497
+ * lifecycle registry provides the production reader). The racing CAS loss is a loud `conflict`.
498
+ * `expectedStatusRevision` pins the CAS to the CALLER's observed status revision (0 = observed
499
+ * ABSENT) for read-modify-write callers whose new value derives from the stored one (the §13.6
500
+ * restart-intensity history): without the pin, this function's own fresh internal read would
501
+ * let two concurrent derivations silently merge-lose each other's contribution. It is PURELY a
502
+ * CAS pin — the AUTHORITY to originate supervisor-owned state is the separate branded
503
+ * {@link SupervisorWriteGrant} (`supervisor`), never revision presence. Without the grant this
504
+ * is an instance-side write: it may not carry the restart history, the retirement mark, or the
505
+ * `escalated` state (they are stripped on both create and update, and `escalated` refuses),
506
+ * and the stored supervisor fields ride forward untouched. */
507
+ export async function writeServiceStatus(kv, args) {
508
+ // SNAPSHOT the raw input FIRST (a JSON round-trip reads each getter exactly once, so nothing a
509
+ // caller controls flips between here and the awaits below — validate-then-clone would validate
510
+ // the caller object then re-read it during the clone, a getter/Proxy TOCTOU). An instance-side
511
+ // (ungranted) write cannot ORIGINATE the supervisor-owned state: its reserved fields are
512
+ // stripped from the RAW snapshot BEFORE validation (a forged garbage `restarts`/`retiredAt` is
513
+ // dropped, not rejected — the instance's ordinary `ready`/`exited` write still goes through),
514
+ // and it may not originate `escalated`. Only THEN validate the (possibly stripped) snapshot.
515
+ const raw = JSON.parse(JSON.stringify(args.status));
516
+ const bySupervisor = isSupervisorWrite(args.supervisor);
517
+ if (!bySupervisor) {
518
+ if (raw.state === SERVICE_ESCALATED)
519
+ throw new EpEnvelopeError("failed-precondition", `an instance-side status write cannot ORIGINATE "${SERVICE_ESCALATED}" for "${args.endpoint}/${args.instanceId}"; escalation is the supervisor's branded authority (SPEC 13.6)`);
520
+ delete raw[SERVICE_RESTART_HISTORY_FIELD];
521
+ delete raw[SERVICE_RETIRED_MARK_FIELD];
522
+ }
523
+ const status = parseServiceStatus(raw);
524
+ if (status.epoch !== args.epoch)
525
+ throw new EpEnvelopeError("internal", `status.epoch ${status.epoch} disagrees with the writer-authenticated epoch ${args.epoch} (SPEC 13.9: the epoch rides the subject)`);
526
+ assertStatusValue(status);
527
+ // The endpoint NAME rides through: the kind's own qualifier assert tokenizes it exactly once.
528
+ const iId = assertLifecycleToken(args.instanceId, "instanceId");
529
+ const specEntry = await kv.get(recordSpecKey(RECORD_KINDS.svc, [args.endpoint, iId]));
530
+ if (!specEntry || specEntry.operation !== "PUT")
531
+ throw new EpEnvelopeError("failed-precondition", `status write for "${args.endpoint}/${args.instanceId}" has no registered spec; writing it would create the torn record state readers reject (SPEC 13.4)`);
532
+ if (status.observedSpecRevision > specEntry.revision)
533
+ throw new EpEnvelopeError("failed-precondition", `observedSpecRevision ${status.observedSpecRevision} runs AHEAD of the spec revision ${specEntry.revision}; a status can only observe a registration that exists (SPEC 13.4)`);
534
+ const current = await args.readProcessEpoch();
535
+ if (!Number.isSafeInteger(current) || current < 0)
536
+ throw new EpEnvelopeError("internal", `the authoritative mapping read returned ${JSON.stringify(current)}, not an unsigned processEpoch`);
537
+ if (args.epoch !== current)
538
+ throw new EpEnvelopeError("expired", `status write from epoch ${args.epoch} is not the authoritative mapping's current processEpoch ${current}; stored-status monotonicity alone is insufficient during takeover (SPEC 13.9)`);
539
+ const key = recordStatusKey(RECORD_KINDS.svc, [args.endpoint, iId]);
540
+ const stored = await kv.get(key);
541
+ if (args.expectedStatusRevision !== undefined) {
542
+ if (!Number.isSafeInteger(args.expectedStatusRevision) || args.expectedStatusRevision < 0)
543
+ throw new EpEnvelopeError("internal", `expectedStatusRevision ${String(args.expectedStatusRevision)} is not an unsigned integer`);
544
+ const current = stored && stored.operation === "PUT" ? stored.revision : 0;
545
+ if (current !== args.expectedStatusRevision)
546
+ throw new EpEnvelopeError("conflict", `the status for "${args.endpoint}/${args.instanceId}" moved (observed revision ${args.expectedStatusRevision}, stored ${current}); a derived write against a moved base would merge-lose the concurrent write, re-read and re-derive (SPEC 13.6)`);
547
+ }
548
+ if (stored && stored.operation === "PUT") {
549
+ const recorded = parseServiceStatus(decodeJson(stored.value, key));
550
+ // ESCALATED is IRREVERSIBLE here (§13.6): no later write, any epoch, replaces it. The only
551
+ // permitted touch on an escalated row is the supervisor's retirement mark, which the
552
+ // escalation reconciler writes DIRECTLY (never through this writer).
553
+ if (recorded.state === SERVICE_ESCALATED)
554
+ throw new EpEnvelopeError("failed-precondition", `"${args.endpoint}/${args.instanceId}" is escalated; the state is terminal and a status write cannot clear it (SPEC 13.6)`);
555
+ if (args.epoch < recorded.epoch)
556
+ throw new EpEnvelopeError("conflict", `status write from epoch ${args.epoch} is below the stored status epoch ${recorded.epoch} (SPEC 13.9)`);
557
+ if (!bySupervisor) {
558
+ // The supervisor-owned fields ride FORWARD through this instance-side write: they were
559
+ // stripped above, so copy the stored values back (a successor's `ready` convergence
560
+ // survives the history, §13.6).
561
+ const s = status;
562
+ const r = recorded;
563
+ if (r[SERVICE_RESTART_HISTORY_FIELD] !== undefined)
564
+ s[SERVICE_RESTART_HISTORY_FIELD] = r[SERVICE_RESTART_HISTORY_FIELD];
565
+ if (r[SERVICE_RETIRED_MARK_FIELD] !== undefined)
566
+ s[SERVICE_RETIRED_MARK_FIELD] = r[SERVICE_RETIRED_MARK_FIELD];
567
+ }
568
+ return updateRecordEntry(kv, key, status, stored.revision);
569
+ }
570
+ return createRecordEntry(kv, key, status);
571
+ }
572
+ /** Freeze the request-scoped expected set (§13.5): the LIVE instances of a class from the
573
+ * service registry at send time — VALIDATED registered spec, status present and caught up to
574
+ * the current registration (a stale projection is an instance not yet live under it, so
575
+ * freezing `(new registrationRevision, pre-registration epoch)` would combine a registration
576
+ * with liveness it never had), and not {@link SERVICE_EXITED}. An EMPTY or UNREADABLE registry
577
+ * is `failed-precondition`, never an empty success (§13.5); a MALFORMED registry record fails
578
+ * loud (`internal`, §13.9: readers fail loud on invalid mediated-writer state). The read grant
579
+ * this runs under is a §13.9 matrix row. */
580
+ export async function freezeExpectedSet(jsm, kv, space, endpoint) {
581
+ const e = endpointToken(endpoint);
582
+ const frozen = [];
583
+ const instanceIds = [];
584
+ try {
585
+ // Enumeration is a bounded-lag `kv.keys` LIST (which instances exist): a just-registered
586
+ // instance missed here simply is not frozen this round (it falls to the gather/reconcile), so
587
+ // the list read need not be leader-served — but each frozen slot's coordinate reads below ARE.
588
+ const iter = await kv.keys(`svc.${e}.*.spec`);
589
+ for await (const key of iter)
590
+ instanceIds.push(key.split(".")[2]);
591
+ }
592
+ catch (err) {
593
+ const wrapped = new EpEnvelopeError("failed-precondition", `the service registry for "${endpoint}" is unreadable; an unreadable registry is failed-precondition, never an empty success (SPEC 13.5): ${err?.message ?? String(err)}`);
594
+ wrapped.cause = err;
595
+ throw wrapped;
596
+ }
597
+ for (const instanceId of instanceIds) {
598
+ // Leader-served spec + status reads, so the FROZEN registrationRevision comes from the same
599
+ // consistency level as the reconcile's — a follower-stale freeze paired with a leader reconcile
600
+ // would otherwise fabricate registration churn from pure read-skew.
601
+ const spec = await readSvcRecordLeader(jsm, space, recordSpecKey(RECORD_KINDS.svc, [endpoint, instanceId]));
602
+ if (!spec || "deleted" in spec)
603
+ continue; // gone since the enumeration list: not a live member
604
+ parseServiceSpec(spec.value, { endpoint }); // malformed registry state fails LOUD (§13.9)
605
+ const statusRec = await readSvcRecordLeader(jsm, space, recordStatusKey(RECORD_KINDS.svc, [endpoint, instanceId]));
606
+ if (!statusRec || "deleted" in statusRec)
607
+ continue; // registered but never converged: not a live class member
608
+ const status = parseServiceStatus(statusRec.value);
609
+ if (status.state === SERVICE_EXITED)
610
+ continue;
611
+ if (status.state === SERVICE_ESCALATED)
612
+ continue; // terminally not-startable (§13.6): never a live scatter member
613
+ if (status.observedSpecRevision < spec.revision)
614
+ continue; // staleProjection: liveness predates the CURRENT registration
615
+ frozen.push({ instanceId, registrationRevision: spec.revision, epoch: status.epoch });
616
+ }
617
+ if (frozen.length === 0)
618
+ throw new EpEnvelopeError("failed-precondition", `service "${endpoint}" has no live registered instances; an empty registry is never an empty scatter success (SPEC 13.5)`);
619
+ return frozen;
620
+ }
621
+ /** A LEADER-SERVED read of one `svc` record key (§13.1: every authority currency read is a
622
+ * leader-served `STREAM.MSG.GET`, never a follower/mirror Direct Get — independent of how a KV
623
+ * handle was opened, so the public reader seams do not depend on a caller passing a bind-only KV).
624
+ * `svc` records, unlike the never-deleted lifecycle families, MAY be deleted on deregistration
625
+ * (§13.5: a deleted spec is an explicit deregistration), so a DELETE/PURGE marker is reported as
626
+ * `{ deleted: true }`, never a corruption throw. Absent → `undefined`; malformed → loud. */
627
+ async function readSvcRecordLeader(jsm, space, key) {
628
+ const bucket = recordsBucket(space);
629
+ let m;
630
+ try {
631
+ m = await jsm.streams.getMessage(`KV_${bucket}`, { last_by_subj: `$KV.${bucket}.${key}` });
632
+ }
633
+ catch (e) {
634
+ if (e.code === 10037)
635
+ return undefined; // no message on the subject: never registered
636
+ const wrapped = new EpEnvelopeError("failed-precondition", `the service registry key ${key} is unreadable (leader read); an unreadable registry is failed-precondition, never a fabricated verdict (SPEC 13.5): ${e?.message ?? String(e)}`);
637
+ wrapped.cause = e;
638
+ throw wrapped;
639
+ }
640
+ if (!m)
641
+ return undefined;
642
+ if (m.header?.get("KV-Operation"))
643
+ return { deleted: true }; // a deregistration, not corruption (§13.5)
644
+ return { value: decodeJson(m.data, key), revision: m.seq };
645
+ }
646
+ /** The PRODUCTION `reconcileRegistration` hook for {@link epScatter} (§13.5): a bounded post-T
647
+ * LEADER-SERVED read of every frozen slot's CURRENT `svc….spec` key (the same §13.9 read class as
648
+ * the freeze). Per slot: a live spec is `{ registered: true, registrationRevision }` (the key's
649
+ * CURRENT store revision — an advance past the frozen value is what the gather classifies as
650
+ * `registration` churn); an absent OR deleted spec is the EXPLICIT `{ registered: false }` verdict
651
+ * (a mid-scatter deregistration, §13.5: not churn). Only the spec KEY is read — the reconcile
652
+ * compares registration currency, not liveness. A malformed spec fails loud (`internal`, §13.9);
653
+ * an unreadable registry normalizes to `failed-precondition` (§13.5: never a fabricated verdict).
654
+ * Leader-served so a follower-stale read can never miss an advanced revision and falsely retain a
655
+ * counted reply (engineer/distsys). */
656
+ export function registrationReconciler(jsm, space, endpoint, frozen) {
657
+ endpointToken(endpoint); // grammar up front: a malformed endpoint never reaches the read
658
+ return async () => {
659
+ const verdicts = new Map();
660
+ for (const slot of frozen) {
661
+ const rec = await readSvcRecordLeader(jsm, space, recordSpecKey(RECORD_KINDS.svc, [endpoint, slot.instanceId]));
662
+ if (!rec || "deleted" in rec) {
663
+ verdicts.set(slot.instanceId, { registered: false });
664
+ continue;
665
+ }
666
+ parseServiceSpec(rec.value, { endpoint }); // malformed registry state fails LOUD (§13.9)
667
+ verdicts.set(slot.instanceId, { registered: true, registrationRevision: rec.revision });
668
+ }
669
+ return verdicts;
670
+ };
671
+ }
672
+ /** The PRODUCTION `currentEpoch` hook for {@link epCall} on the `one` rail (§13.2): a LEADER-SERVED
673
+ * read of the answering instance's CURRENT `svc….status` epoch. An unregistered instance or one
674
+ * that never converged (no status) has no current epoch to verify a queue winner against and
675
+ * refuses `failed-precondition` — the read's OWN failure, which {@link epCall} never mislabels as
676
+ * responder staleness. Leader-served so a follower-stale status read can never accept a
677
+ * just-superseded queue winner at an old epoch (engineer/distsys). A stale projection does not
678
+ * refuse: `epoch` advances only through a takeover's status write (§13.5). */
679
+ export function serviceEpochReader(jsm, space, endpoint) {
680
+ endpointToken(endpoint);
681
+ return async (instanceId) => {
682
+ const rec = await readSvcRecordLeader(jsm, space, recordStatusKey(RECORD_KINDS.svc, [endpoint, instanceId]));
683
+ if (!rec || "deleted" in rec)
684
+ throw new EpEnvelopeError("failed-precondition", `"${endpoint}/${instanceId}" has no live status; a \`one\` responder with no current epoch cannot be verified (SPEC 13.2)`);
685
+ return parseServiceStatus(rec.value).epoch;
686
+ };
687
+ }
688
+ const AUTHORIZED_SERVE = new WeakMap();
689
+ /**
690
+ * Authorize a serve credential against the REGISTERED service (§13.9: serving is granted
691
+ * authority, dual to calling — the registry is discovery, the serve grant is the authority).
692
+ * Runs inside the provisioner. The fence, in order:
693
+ * 1. the instance must be REGISTERED (its `svc….spec` record exists) — `failed-precondition`;
694
+ * 2. the credential's holder must BE the registered owner (`permission-denied`), and the name
695
+ * authority is re-checked FRESH (`permission-denied` on drift);
696
+ * 3. every registered cluster is read through the two-stage §13.7 content-address protocol:
697
+ * the MANIFEST is fetched at the registered CLOSURE digest and verified, `members` must be
698
+ * empty (P1 single-document clusters; a non-empty closure is the D8 loader's, refused loud
699
+ * until then), then the ROOT cluster document is fetched at `manifest.root` and verified.
700
+ * The verified documents are the ONLY command source — the FULL union of their declared
701
+ * commands is the surface (no caller subset; caller scoping is response-time describe).
702
+ * `describe` is derived by the row builder, never a registered command;
703
+ * 4. the epoch must EQUAL a fresh read of the authoritative mapping's `processEpoch`
704
+ * (`expired`): a serve credential binds the CURRENT incarnation.
705
+ * The returned artifact carries the verified surface, the derived descriptor, and the
706
+ * registration revision. The MINT's fence is the durable issuance gate ({@link
707
+ * finalizeServeIssuance}), NOT this authorization (a read is never a fence, §13.1): this seam
708
+ * produces the surface, the gate serializes its release against takeover and re-registration.
709
+ */
710
+ export async function authorizeServeGrant(kv, args) {
711
+ spacePrefix(args.space); // grammar: a malformed space token never becomes credential rows
712
+ const iId = assertLifecycleToken(args.instanceId, "instanceId");
713
+ if (!Number.isSafeInteger(args.epoch) || args.epoch < 0)
714
+ throw new EpEnvelopeError("internal", `epoch ${args.epoch} is not an unsigned integer`);
715
+ const specKey = recordSpecKey(RECORD_KINDS.svc, [args.endpoint, iId]);
716
+ const specEntry = await kv.get(specKey);
717
+ if (!specEntry || specEntry.operation !== "PUT")
718
+ throw new EpEnvelopeError("failed-precondition", `no registered spec for "${args.endpoint}/${args.instanceId}"; a serve credential is minted only for a REGISTERED instance (SPEC 13.9)`);
719
+ const spec = parseServiceSpec(decodeJson(specEntry.value, specKey), { endpoint: args.endpoint });
720
+ assertBoundedOwner(args.holder.owner, "serve credential holder");
721
+ if (args.holder.owner !== spec.owner)
722
+ throw new EpEnvelopeError("permission-denied", `the serve credential holder "${args.holder.owner}" is not the registered owner "${spec.owner}" of "${args.endpoint}" (SPEC 13.9: serving is the registered owner's authority)`);
723
+ // §13.9 name authority: ONE atomic snapshot binds the owner DECISION and the binding REVISION
724
+ // together (never a torn owner-vs-revision read, engineer/distsys/security). The revision is
725
+ // RECORDED (not fenced here — a read is never a fence, §13.1); the issuance gate carries it and
726
+ // the mint refuses on drift, so a name transfer after this authorization can never release an
727
+ // old-owner credential.
728
+ const nameAuthorityRevision = await assertServiceNameAuthority(spec.endpoint, spec.owner, args.authority);
729
+ // §13.7 two-stage content-address read: the registered digest is a CLOSURE digest naming a
730
+ // MANIFEST; the manifest's `root` names the cluster DOCUMENT. Both fetched, both verified —
731
+ // a raw root document presented at a closure-digest key would conflate the two identities.
732
+ const read = async (digest, what) => {
733
+ let raw;
734
+ try {
735
+ raw = await args.readClusterArtifact(digest);
736
+ }
737
+ catch (err) {
738
+ throw new EpEnvelopeError("unavailable", `the contract-store read seam failed for ${what} ${digest}; serve authorization fails closed (SPEC 13.7): ${err?.message ?? String(err)}`);
739
+ }
740
+ if (raw === undefined)
741
+ throw new EpEnvelopeError("failed-precondition", `${what} ${digest} is not readable from the contract store; an unverifiable registered surface never authorizes (SPEC 13.7)`);
742
+ return raw;
743
+ };
744
+ // Null-prototype: "constructor" is a valid command token, and on a plain `{}` the duplicate
745
+ // check below would resolve the inherited `Object.prototype.constructor` (falsely refusing a
746
+ // legitimate command as "declared twice") while every downstream `surface[command]` lookup
747
+ // (serve construction, governed-surface verification) would leak through the prototype.
748
+ const surface = Object.create(null);
749
+ const commands = [];
750
+ const clusters = [];
751
+ for (const closureDigest of spec.clusterDigests) {
752
+ const manifestRaw = await read(closureDigest, "registered cluster manifest");
753
+ let root;
754
+ try {
755
+ ({ root } = verifyClusterManifest(closureDigest, manifestRaw));
756
+ }
757
+ catch (err) {
758
+ throw new EpEnvelopeError("internal", `registered cluster manifest ${closureDigest} of "${args.endpoint}/${args.instanceId}" fails verification; mediated registered state that does not verify is a writer/store bug, never authority (SPEC 13.7): ${err.message}`);
759
+ }
760
+ const rootRaw = await read(root, "registered cluster document");
761
+ let document;
762
+ try {
763
+ document = verifyClusterRoot(root, rootRaw);
764
+ }
765
+ catch (err) {
766
+ throw new EpEnvelopeError("internal", `registered cluster document ${root} (closure ${closureDigest}) fails verification; mediated registered state that does not verify is never authority (SPEC 13.7): ${err.message}`);
767
+ }
768
+ for (const cmd of document.commands) {
769
+ if (surface[cmd.name] !== undefined)
770
+ throw new EpEnvelopeError("internal", `the registered clusters of "${args.endpoint}" declare command "${cmd.name}" twice; an ambiguous registered surface never authorizes (SPEC 13.7)`);
771
+ surface[cmd.name] = Object.freeze({
772
+ clusterDigest: closureDigest,
773
+ class: cmd.class,
774
+ targeted: cmd.targeted,
775
+ modes: Object.freeze([...(cmd.modes ?? [])]),
776
+ capability: cmd.capability,
777
+ inputDigest: cmd.inputDigest,
778
+ outputDigest: cmd.outputDigest,
779
+ traits: Object.freeze([...(cmd.traits ?? [])]),
780
+ });
781
+ commands.push(cmd.name);
782
+ }
783
+ // The inline copy for describe is the verified ROOT cluster DOCUMENT (its command
784
+ // declarations), never the manifest: a consumer verifies it against the advertised closure
785
+ // digest by reconstructing the single-member manifest `{v:1, root: digest(document),
786
+ // members:[]}` (§13.7 two-digest read). Inlining the manifest would ship bytes whose
787
+ // `commands` disagree with the sibling command list.
788
+ clusters.push({ digest: closureDigest, document, raw: rootRaw });
789
+ }
790
+ commands.sort(); // deterministic full surface
791
+ // §13.9 bind-row inputs: journal class is REGISTERED truth (derived from the verified
792
+ // surface, never caller-asserted); pools are PROVISIONING truth (the authorizing provisioner
793
+ // asserts exactly the pool durables it pre-created — no registered record enumerates pool
794
+ // names, routes are per-acceptance policy decisions, §13.6). Pools without a journal-class
795
+ // surface are refused: only journal acceptances route to pools, so the combination is a
796
+ // caller bug, never a silent no-op.
797
+ const journalClass = commands.some((cmd) => surface[cmd].class === "journal");
798
+ const pools = [...(args.pools ?? [])].map((p) => assertPoolToken(p)).sort();
799
+ if (new Set(pools).size !== pools.length)
800
+ throw new EpEnvelopeError("internal", `the pools list for "${args.endpoint}/${args.instanceId}" carries duplicates; the provisioner enumerates each pre-created pool once (SPEC 13.9)`);
801
+ if (pools.length > 0 && !journalClass)
802
+ throw new EpEnvelopeError("failed-precondition", `"${args.endpoint}" registers no journal-class command but the provisioner asserts pools [${pools.join(", ")}]; only journal acceptances route to work pools (SPEC 13.6/13.9)`);
803
+ const current = await args.readProcessEpoch();
804
+ if (!Number.isSafeInteger(current) || current < 0)
805
+ throw new EpEnvelopeError("internal", `the authoritative mapping read returned ${JSON.stringify(current)}, not an unsigned processEpoch`);
806
+ if (args.epoch !== current)
807
+ throw new EpEnvelopeError("expired", `serve grant for epoch ${args.epoch} but the authoritative mapping's current processEpoch is ${current}; a serve credential binds the CURRENT incarnation only (SPEC 13.1/13.9)`);
808
+ const grant = Object.freeze({
809
+ space: args.space,
810
+ endpoint: spec.endpoint,
811
+ instanceId: iId,
812
+ epoch: args.epoch,
813
+ owner: spec.owner,
814
+ registrationRevision: specEntry.revision,
815
+ nameAuthorityRevision,
816
+ commands: Object.freeze([...commands]),
817
+ surface: Object.freeze(surface),
818
+ journalClass,
819
+ pools: Object.freeze([...pools]),
820
+ descriptor: deriveDescriptor({ endpoint: spec.endpoint, owner: spec.owner, ...(spec.endpointType !== undefined ? { endpointType: spec.endpointType } : {}) }, clusters),
821
+ });
822
+ AUTHORIZED_SERVE.set(grant, {
823
+ space: args.space, endpoint: spec.endpoint, instanceId: iId, epoch: args.epoch,
824
+ owner: spec.owner, registrationRevision: specEntry.revision, nameAuthorityRevision, commands: [...commands],
825
+ journalClass, pools: [...pools],
826
+ });
827
+ return grant;
828
+ }
829
+ /** The brand check every consuming seam runs: `serve` must be the ARTIFACT
830
+ * {@link authorizeServeGrant} returned, field-for-field equal to its authorized snapshot. A
831
+ * structural copy, a raw literal, or a diverging value refuses — serve authority flows only
832
+ * THROUGH the registry authorization. Returns the immutable snapshot (space/owner/epoch/
833
+ * registrationRevision the release fence checks against). */
834
+ export function assertServeGrantAuthorized(serve) {
835
+ const snap = AUTHORIZED_SERVE.get(serve);
836
+ if (!snap)
837
+ throw new EpEnvelopeError("permission-denied", "the serve artifact was not authorized against the registered service (authorizeServeGrant); a raw or copied value never carries serve authority (SPEC 13.9)");
838
+ if (snap.space !== serve.space || snap.endpoint !== serve.endpoint || snap.instanceId !== serve.instanceId
839
+ || snap.epoch !== serve.epoch || snap.owner !== serve.owner || snap.registrationRevision !== serve.registrationRevision
840
+ || snap.nameAuthorityRevision !== serve.nameAuthorityRevision
841
+ || snap.commands.length !== serve.commands.length || snap.commands.some((cmd, i) => serve.commands[i] !== cmd)
842
+ || snap.journalClass !== serve.journalClass
843
+ || snap.pools.length !== serve.pools.length || snap.pools.some((p, i) => serve.pools[i] !== p))
844
+ throw new EpEnvelopeError("permission-denied", "the serve artifact diverges from its authorized snapshot; refusing mutated serve authority (SPEC 13.9)");
845
+ return snap;
846
+ }
847
+ /** The mint-side CONTEXT binding (`permissionsFor`, profile `endpoint-serve`): brand + snapshot
848
+ * equality plus the mint context bound to the artifact (same space, and the minted principal
849
+ * IS the registered owner — an authorized artifact for space A/owner X emits rows for no other
850
+ * space or principal). This is NOT the freshness fence: {@link finalizeServeIssuance} is, and
851
+ * `mintCreds` runs it before releasing the credential. */
852
+ export function assertServeGrantMintable(serve, mint) {
853
+ const snap = assertServeGrantAuthorized(serve);
854
+ if (mint.space !== snap.space)
855
+ throw new EpEnvelopeError("permission-denied", `the serve artifact was authorized for space "${snap.space}", not "${mint.space}"; serve authority never crosses spaces (SPEC 13.9)`);
856
+ if (mint.holderOwner !== snap.owner)
857
+ throw new EpEnvelopeError("permission-denied", `the serve artifact belongs to the registered owner "${snap.owner}"; principal "${mint.holderOwner}" cannot mint from it (SPEC 13.9)`);
858
+ return snap;
859
+ }
860
+ /** A §13.1 source-chain element, EXACT grammar: the `root` anchor, a handle-redemption step
861
+ * `handle.<issuerKeyId>.<id>` (exactly two record-grammar id segments), or a session step
862
+ * `session.<sessionId>` (exactly one). Owner/actor principal components are NOT a lineage; the
863
+ * mint records `["root"]` for a serve credential minted directly by the provisioner authority.
864
+ * Ids are the record grammar `[A-Za-z0-9_-]` (uppercase admitted), bounded, and every segment is
865
+ * non-empty — so `handle.x`, `handle.x.`, and `session.x.y` all refuse. */
866
+ const SOURCE_CHAIN_ID = "[A-Za-z0-9_-]{1,64}"; // the §13.2:1248 / assertIdToken id bound
867
+ const SOURCE_CHAIN_ELEMENT = new RegExp(`^(root|handle\\.${SOURCE_CHAIN_ID}\\.${SOURCE_CHAIN_ID}|session\\.${SOURCE_CHAIN_ID})$`);
868
+ /**
869
+ * The serve-credential release fence (§13.1 "observe gate → write rows → CAS the gate →
870
+ * release"). `mintCreds` calls this AFTER building the credential and BEFORE returning it, so a
871
+ * credential is released only when its ledger row is durably written and its winning CAS proves
872
+ * the gate was still `open` at the SAME `(processEpoch, registrationRevision, nameAuthorityRevision)`
873
+ * the artifact was verified against:
874
+ * - observe the gate; a missing gate or a `frozen`/`retired` state refuses (`expired`);
875
+ * - the observed `processEpoch`, `registrationRevision`, and `nameAuthorityRevision` MUST each
876
+ * equal the artifact's — a takeover (epoch), a re-registration (revision), or a name transfer
877
+ * (name authority) that already froze+reopened advanced one of them, and this mint's surface
878
+ * or its owner is superseded (`expired`);
879
+ * - stage the NORMATIVE ledger row (`holderPrincipal`/`lifecycleUid`/`sourceChain`/`state`/`exp`
880
+ * plus the three currency coordinates), then revision-pinned CAS the gate; a LOSS (a
881
+ * concurrent barrier's freeze CAS won the single key) revokes the staged row and releases
882
+ * nothing (`expired`).
883
+ * The race is closed by serialization on ONE key: a mint that wins wrote its row before its
884
+ * winning CAS, so a later barrier enumerates and revokes/evicts it by `holderPrincipal`; a mint
885
+ * that loses never released.
886
+ */
887
+ export async function finalizeServeIssuance(gate, serve, credential) {
888
+ const snap = assertServeGrantAuthorized(serve);
889
+ const boundedId = (v) => typeof v === "string" && v.length > 0 && v.length <= 128;
890
+ if (!boundedId(credential.credentialId))
891
+ throw new EpEnvelopeError("internal", "credentialId must be a bounded non-empty per-JWT identifier");
892
+ if (!boundedId(credential.credentialKey))
893
+ throw new EpEnvelopeError("internal", "credentialKey must be a bounded non-empty identifier (the minted credential's nkey)");
894
+ assertBoundedOwner(credential.holderActor, "serve credential holder actor");
895
+ if (!Array.isArray(credential.sourceChain) || credential.sourceChain.length === 0
896
+ || !credential.sourceChain.every((p) => typeof p === "string" && SOURCE_CHAIN_ELEMENT.test(p)))
897
+ throw new EpEnvelopeError("internal", "the serve credential sourceChain must be a non-empty §13.1 issuance lineage (root | handle.<issuer>.<id> | session.<id>), never principal components");
898
+ if (credential.exp !== undefined && (!Number.isSafeInteger(credential.exp) || credential.exp < 0))
899
+ throw new EpEnvelopeError("internal", `the serve credential exp ${JSON.stringify(credential.exp)} is not an unsigned unix timestamp`);
900
+ const obs = await gate.observe();
901
+ if (obs === null)
902
+ throw new EpEnvelopeError("expired", `no issuance gate for "${snap.endpoint}/${snap.instanceId}"; a serve credential never mints against a missing gate (SPEC 13.1)`);
903
+ // Gate IDENTITY `(space, endpoint, lifecycleUid)`: the instance token is unique only within
904
+ // `(space, endpoint)`, so ALL must match — a caller that handed a foreign gate (another space's,
905
+ // a different endpoint sharing the instance token, or any wrong gate) with coincidentally
906
+ // matching coordinates is refused (the per-space auth bucket is the production space fence; this
907
+ // is the seam's defense-in-depth).
908
+ if (obs.space !== snap.space || obs.endpoint !== snap.endpoint || obs.lifecycleUid !== snap.instanceId)
909
+ throw new EpEnvelopeError("internal", `the issuance gate is for "${obs.space}/${obs.endpoint}/${obs.lifecycleUid}", not the authorized instance "${snap.space}/${snap.endpoint}/${snap.instanceId}"; a serve credential mints only against its OWN gate (SPEC 13.1)`);
910
+ if (obs.state !== "open")
911
+ throw new EpEnvelopeError("expired", `the issuance gate for "${snap.endpoint}/${snap.instanceId}" is ${obs.state}; minting is closed (SPEC 13.1)`);
912
+ // JOINT currency on ONE key: a takeover advances processEpoch, a re-registration advances
913
+ // registrationRevision, a name transfer advances nameAuthorityRevision; any one that has
914
+ // already frozen+reopened the gate supersedes the branded surface or its owner, and the read
915
+ // below is safe only because the CAS re-checks the same key.
916
+ if (obs.processEpoch !== snap.epoch)
917
+ throw new EpEnvelopeError("expired", `the issuance gate is at processEpoch ${obs.processEpoch}, not the authorized ${snap.epoch}; a takeover superseded this incarnation (SPEC 13.1)`);
918
+ if (obs.registrationRevision !== snap.registrationRevision)
919
+ throw new EpEnvelopeError("expired", `the issuance gate is at registrationRevision ${obs.registrationRevision}, not the authorized ${snap.registrationRevision}; a re-registration superseded the branded surface (SPEC 13.5/13.9)`);
920
+ if (obs.nameAuthorityRevision !== snap.nameAuthorityRevision)
921
+ throw new EpEnvelopeError("expired", `the issuance gate is at nameAuthorityRevision ${obs.nameAuthorityRevision}, not the authorized ${snap.nameAuthorityRevision}; a name transfer superseded the serving owner (SPEC 13.9)`);
922
+ // SERVING-PRINCIPAL BINDING (§13.1:1056-1069): the mint is bound to the gate's REGISTERED serving
923
+ // principal, not merely the registered owner. authorizeServeGrant proves owner == registered
924
+ // owner, but a SIBLING ACTOR under that owner would otherwise win the real gate and be
925
+ // ledgered/evicted in place of the registered serving instance. The minted `owner.actor` MUST
926
+ // equal `epgate.principal`; on any mismatch the mint releases nothing and writes no active row.
927
+ const mintedPrincipal = principalKey(snap.owner, credential.holderActor).key;
928
+ if (mintedPrincipal !== obs.principal)
929
+ throw new EpEnvelopeError("permission-denied", `the serve mint's principal "${mintedPrincipal}" is not the gate's registered serving principal "${obs.principal}" for "${snap.endpoint}/${snap.instanceId}"; a sibling actor under the registered owner cannot win the endpoint gate (SPEC 13.1)`);
930
+ const row = {
931
+ credentialId: credential.credentialId,
932
+ credentialKey: credential.credentialKey,
933
+ // The eviction target is the OBSERVED gate principal (== the minted principal, checked above),
934
+ // serialized through the ONE principal serializer the eviction feed keys on (subjects.ts
935
+ // principalKey invariant), so the barrier's enumeration key can never drift from the
936
+ // credential's and always names the registered serving principal (§13.1).
937
+ holderPrincipal: obs.principal,
938
+ endpoint: snap.endpoint,
939
+ lifecycleUid: snap.instanceId,
940
+ sourceChain: Object.freeze([...credential.sourceChain]),
941
+ state: "active",
942
+ ...(credential.exp !== undefined ? { exp: credential.exp } : {}),
943
+ generation: obs.generation,
944
+ processEpoch: obs.processEpoch,
945
+ registrationRevision: obs.registrationRevision,
946
+ nameAuthorityRevision: obs.nameAuthorityRevision,
947
+ };
948
+ await gate.stage(row);
949
+ // Best-effort revoke of the staged row on any non-win, ALWAYS surfacing a revoke failure (never
950
+ // swallowed) so the reconciliation debt is visible — the credential is released only on a win.
951
+ const revokeStaged = async () => {
952
+ try {
953
+ await gate.revoke(row);
954
+ return undefined;
955
+ }
956
+ catch (err) {
957
+ return err?.message ?? String(err);
958
+ }
959
+ };
960
+ let won;
961
+ try {
962
+ won = await gate.commit(obs.revision);
963
+ }
964
+ catch (err) {
965
+ const revokeFailed = await revokeStaged();
966
+ throw new EpEnvelopeError("unavailable", `the issuance-gate CAS failed; refusing to release a serve credential (SPEC 13.1): ${err?.message ?? String(err)}${revokeFailed ? `; ALSO the staged-row revoke failed and the row needs barrier reconciliation: ${revokeFailed}` : ""}`);
967
+ }
968
+ if (!won) {
969
+ const revokeFailed = await revokeStaged();
970
+ throw new EpEnvelopeError("expired", `the issuance gate advanced during mint (a takeover, re-registration, or name transfer won the serialization on ${snap.endpoint}/${snap.instanceId}); this mint released nothing (SPEC 13.1)${revokeFailed ? `; ALSO the staged-row revoke failed and the row needs barrier reconciliation: ${revokeFailed}` : ""}`);
971
+ }
972
+ }
973
+ //# sourceMappingURL=endpoint-service.js.map