@cotal-ai/auth 0.11.6 → 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 (102) 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/commands.js +16 -12
  18. package/dist/commands.js.map +1 -1
  19. package/dist/connect-reader.d.ts +72 -0
  20. package/dist/connect-reader.d.ts.map +1 -0
  21. package/dist/connect-reader.js +154 -0
  22. package/dist/connect-reader.js.map +1 -0
  23. package/dist/continuity.d.ts +23 -0
  24. package/dist/continuity.d.ts.map +1 -0
  25. package/dist/continuity.js +145 -0
  26. package/dist/continuity.js.map +1 -0
  27. package/dist/credential-ledger.d.ts +343 -0
  28. package/dist/credential-ledger.d.ts.map +1 -0
  29. package/dist/credential-ledger.js +732 -0
  30. package/dist/credential-ledger.js.map +1 -0
  31. package/dist/drain-repair.d.ts +95 -0
  32. package/dist/drain-repair.d.ts.map +1 -0
  33. package/dist/drain-repair.js +305 -0
  34. package/dist/drain-repair.js.map +1 -0
  35. package/dist/idp.d.ts +19 -0
  36. package/dist/idp.d.ts.map +1 -1
  37. package/dist/idp.js +17 -0
  38. package/dist/idp.js.map +1 -1
  39. package/dist/index.d.ts +7 -3
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +31 -3
  42. package/dist/index.js.map +1 -1
  43. package/dist/issuer.d.ts +8 -0
  44. package/dist/issuer.d.ts.map +1 -1
  45. package/dist/issuer.js +4 -2
  46. package/dist/issuer.js.map +1 -1
  47. package/dist/ledger-scanner.d.ts +98 -0
  48. package/dist/ledger-scanner.d.ts.map +1 -0
  49. package/dist/ledger-scanner.js +352 -0
  50. package/dist/ledger-scanner.js.map +1 -0
  51. package/dist/ledger.d.ts +12 -0
  52. package/dist/ledger.d.ts.map +1 -1
  53. package/dist/ledger.js +103 -13
  54. package/dist/ledger.js.map +1 -1
  55. package/dist/lifecycle-registry.d.ts +397 -0
  56. package/dist/lifecycle-registry.d.ts.map +1 -0
  57. package/dist/lifecycle-registry.js +819 -0
  58. package/dist/lifecycle-registry.js.map +1 -0
  59. package/dist/permissions.d.ts +5 -1
  60. package/dist/permissions.d.ts.map +1 -1
  61. package/dist/permissions.js +29 -6
  62. package/dist/permissions.js.map +1 -1
  63. package/dist/plane-claim.d.ts +83 -0
  64. package/dist/plane-claim.d.ts.map +1 -0
  65. package/dist/plane-claim.js +290 -0
  66. package/dist/plane-claim.js.map +1 -0
  67. package/dist/provider.d.ts.map +1 -1
  68. package/dist/provider.js +45 -16
  69. package/dist/provider.js.map +1 -1
  70. package/dist/records-scanner.d.ts +78 -0
  71. package/dist/records-scanner.d.ts.map +1 -0
  72. package/dist/records-scanner.js +320 -0
  73. package/dist/records-scanner.js.map +1 -0
  74. package/dist/retirement-barrier.d.ts +217 -0
  75. package/dist/retirement-barrier.d.ts.map +1 -0
  76. package/dist/retirement-barrier.js +624 -0
  77. package/dist/retirement-barrier.js.map +1 -0
  78. package/dist/retirement-cleaner.d.ts +54 -0
  79. package/dist/retirement-cleaner.d.ts.map +1 -0
  80. package/dist/retirement-cleaner.js +158 -0
  81. package/dist/retirement-cleaner.js.map +1 -0
  82. package/dist/root-credential.d.ts +26 -0
  83. package/dist/root-credential.d.ts.map +1 -0
  84. package/dist/root-credential.js +116 -0
  85. package/dist/root-credential.js.map +1 -0
  86. package/dist/service.d.ts +73 -5
  87. package/dist/service.d.ts.map +1 -1
  88. package/dist/service.js +349 -24
  89. package/dist/service.js.map +1 -1
  90. package/dist/session-ledger.d.ts +224 -0
  91. package/dist/session-ledger.d.ts.map +1 -0
  92. package/dist/session-ledger.js +713 -0
  93. package/dist/session-ledger.js.map +1 -0
  94. package/dist/store.d.ts +18 -8
  95. package/dist/store.d.ts.map +1 -1
  96. package/dist/store.js +74 -51
  97. package/dist/store.js.map +1 -1
  98. package/dist/token.d.ts +17 -0
  99. package/dist/token.d.ts.map +1 -1
  100. package/dist/token.js +30 -2
  101. package/dist/token.js.map +1 -1
  102. package/package.json +7 -4
@@ -0,0 +1,819 @@
1
+ /**
2
+ * The D13 lifecycle registry, slice (1)+(2a) (SPEC §13.1, as amended): the sealed storage
3
+ * authority for lifecycle identity — the space-global UID reservation, the three-state alias
4
+ * head (`active | retiring | retired`), the activation saga over both stores, the leader-served
5
+ * mapping reader that backs the `readProcessEpoch` seam D11 (restart supervision, status
6
+ * commit) and D7 (session redemption) inject, and the issuance-gate CAS PRIMITIVES
7
+ * (create/observe/freeze/op-pinned reopen/retire) for the agent gate family `gate.<lifecycleUid>`.
8
+ *
9
+ * DELIBERATELY NOT HERE (the later gated slices, §13.1 barrier order):
10
+ * - the (3) normative credential ledger and the (2b) takeover barrier live in the SIBLING
11
+ * module `credential-ledger.ts` (the same authority over the same stores, reached through
12
+ * {@link registryStores}); this module keeps only the barrier's module-internal epoch seam
13
+ * ({@link advanceEpochWithinTakeover} — there is still NO public epoch advance);
14
+ * - no head retirement: `active → retiring → retired` is a finalization step of the
15
+ * RETIREMENT barrier (a later slice), and exposing it without the completed barrier would
16
+ * recreate the half-fence D13 exists to remove;
17
+ * - no reachable production activation wiring and no credential release (the head is written
18
+ * WITHOUT `currentCredentialId` until production issuance mints under the reopened gate).
19
+ *
20
+ * SURFACE: the activation saga and the gate primitives are PACKAGE-INTERNAL (not exported from
21
+ * the package index) until the (3) ledger slice completes them into real operations; "no
22
+ * production callsite" is not the same as no reachable API, so the API is not reachable. The
23
+ * executor seam is the sealed registry itself: it is constructed only over the minting
24
+ * authority's own authenticated connection, so holding a gate's `opId` string grants nothing
25
+ * to a caller that cannot open the registry (the opId is an identifier, §13.1).
26
+ *
27
+ * The model (§13.1):
28
+ * - the UID is entropy, never order: before anything else the minting authority WINS the
29
+ * create-only, never-deleted, SPACE-GLOBAL reservation `uid.<lifecycleUid>`; a create
30
+ * conflict burns the candidate (the alias head alone cannot reject the same UID under a
31
+ * different alias), and a DEL/PURGE marker is corruption, never reusable absence;
32
+ * - the head is a SINGLE unsplit key and `mappingRevision` IS its STORE revision (one leader
33
+ * read returns `{ mapping, revision }`; the value carries no revision field);
34
+ * - `active` is the ONLY current state: every currency seam (the epoch reader here) yields no
35
+ * current mapping and no current epoch for `retiring` AND `retired` alike;
36
+ * - activation is a CROSS-BUCKET SAGA with a durable op intent, in the normative order:
37
+ * reserve UID → create the gate `frozen` carrying the activation op (unmintable from
38
+ * birth) → CAS the alias head → reopen the gate LAST; a head-CAS loser terminalizes its own
39
+ * orphan gate and its UID stays burned; a crash resumes the SAME op ({@link resumeActivation}),
40
+ * never minting a second UID for one activation;
41
+ * - the head and the reservation are NEVER-DELETED: a reader treats only TRUE ABSENCE as
42
+ * virgin, and a deletion marker refuses LOUDLY as corruption;
43
+ * - a `frozen` gate MUST carry its durable operation intent `{ opId, kind }`: a resumer
44
+ * advances only the SAME `opId` (the opId is an identifier, never a bearer capability — the
45
+ * op-pinned CAS plus the caller's own authenticated authority is what advances the gate),
46
+ * and a stranger can neither reopen nor terminalize another operation's freeze.
47
+ */
48
+ import { jetstream, jetstreamManager } from "@nats-io/jetstream";
49
+ import { Kvm } from "@nats-io/kv";
50
+ import { EpEnvelopeError, LIFECYCLE_HEAD, UID_RESERVATION, recordAtomicKey, createRecordEntry, updateRecordEntry, readRecordLeader, mintLifecycleUid, assertLifecycleToken, epAuthBucket, isCasLoss as isRawCasLoss, workPoolContext, } from "@cotal-ai/core";
51
+ import { assertScannerSpace } from "./ledger-scanner.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
+ const isCasLoss = (e) => e instanceof EpEnvelopeError && e.code === "conflict";
58
+ const REGISTRIES = new WeakMap();
59
+ const READERS = new WeakMap();
60
+ function internals(reg) {
61
+ const i = REGISTRIES.get(reg);
62
+ if (!i)
63
+ throw new EpEnvelopeError("failed-precondition", "the lifecycle registry was not constructed by openLifecycleRegistry(); a hand-assembled context never authorizes (SPEC 13.12)");
64
+ return i;
65
+ }
66
+ function readerInternals(rd) {
67
+ const i = READERS.get(rd);
68
+ if (!i)
69
+ throw new EpEnvelopeError("failed-precondition", "the mapping reader was not constructed by openLifecycleMappingReader(); a hand-assembled context never authorizes (SPEC 13.12)");
70
+ return i;
71
+ }
72
+ /** Prove an authority store's stream shape at bind (SPEC 13.12): PRIMARY (never a
73
+ * mirror/sourced copy), LIMITS retention (an Interest/WorkQueue stream deletes a message once
74
+ * consumers have interest/ack it — an authority row would silently vanish after a barrier's
75
+ * point-in-time enumeration reads it), and NO silent-eviction limit — no age retention and no
76
+ * finite global message/byte cap (under DiscardOld a finite global limit evicts a PRIOR
77
+ * authority key's latest row the moment an unrelated key is written). A store that cannot be
78
+ * proved never serves. (A per-subject cap is NOT a vector: NATS keeps at least the latest value
79
+ * per subject for any cap ≥ 1, and 0/-1 mean unlimited, so no setting drops a key's own row.) */
80
+ export function assertAuthorityStreamShape(cfg, bucket) {
81
+ if (cfg.mirror !== undefined || (Array.isArray(cfg.sources) && cfg.sources.length > 0))
82
+ throw new EpEnvelopeError("failed-precondition", `the store ${bucket} is a mirror/sourced stream; a follower copy cannot serve authority reads or CAS (SPEC 13.12); bind the primary`);
83
+ // A KV bucket is Limits-retention by construction, but the backing stream config is what
84
+ // actually governs eviction, so prove it (a stream reprovisioned as Interest/WorkQueue under
85
+ // the KV_ name would delete authority rows on consumer interest/ack — the barrier's throwaway
86
+ // enumeration consumer would itself trigger the deletion).
87
+ if (typeof cfg.retention === "string" && cfg.retention !== "limits")
88
+ throw new EpEnvelopeError("failed-precondition", `the store ${bucket} has ${cfg.retention} retention, not limits; a non-Limits stream deletes authority rows on consumer interest/ack (SPEC 13.12); reprovision as a KV bucket`);
89
+ if (typeof cfg.max_age === "number" && cfg.max_age > 0)
90
+ throw new EpEnvelopeError("failed-precondition", `the store ${bucket} carries bucket-wide age eviction (max_age ${cfg.max_age}); an age-evicted authority row silently drops a fence (SPEC 13.12); reprovision`);
91
+ if (typeof cfg.max_msgs === "number" && cfg.max_msgs >= 0)
92
+ throw new EpEnvelopeError("failed-precondition", `the store ${bucket} carries a finite global message cap (max_msgs ${cfg.max_msgs}); under discard-old it silently evicts never-deleted authority keys (SPEC 13.12); reprovision`);
93
+ if (typeof cfg.max_bytes === "number" && cfg.max_bytes >= 0)
94
+ throw new EpEnvelopeError("failed-precondition", `the store ${bucket} carries a finite global byte cap (max_bytes ${cfg.max_bytes}); under discard-old it silently evicts never-deleted authority keys (SPEC 13.12); reprovision`);
95
+ // STORE-BINDING (SPEC 13.12): the stream must BE the claimed KV bucket, not merely wear its
96
+ // name — exactly the one `$KV.<bucket>.>` subject (an extra captured subject would put foreign
97
+ // bodies inside every body-selected MSG.GET grant on this stream, breaking the metadata-only
98
+ // residual claim) and durable file storage (a memory authority store forgets every fence and
99
+ // revocation on broker restart). Both are REQUIRED, not skipped-when-absent: every caller
100
+ // proves a real `streams.info` config, and an absent field here is an unproved store.
101
+ const expectedSubject = `$KV.${bucket}.>`;
102
+ if (!Array.isArray(cfg.subjects) || cfg.subjects.length !== 1 || cfg.subjects[0] !== expectedSubject)
103
+ throw new EpEnvelopeError("failed-precondition", `the store ${bucket} does not carry exactly the subject ${expectedSubject} (got ${JSON.stringify(cfg.subjects)}); a stream that captures anything else is not this KV bucket, and its body-selected reads are not bounded to authority metadata (SPEC 13.12); reprovision`);
104
+ if (cfg.storage !== "file")
105
+ throw new EpEnvelopeError("failed-precondition", `the store ${bucket} has storage ${JSON.stringify(cfg.storage)}, not file; a non-durable authority store forgets fences and revocations on restart (SPEC 13.12); reprovision`);
106
+ }
107
+ /** Open the minting authority's sealed lifecycle registry: binds the space's primary records
108
+ * bucket AND its auth bucket. BOTH are shape-proved at bind (SPEC 13.12): primary, un-aged,
109
+ * no finite global eviction cap; the auth store additionally leader-only `allow_direct=false`. */
110
+ export async function openLifecycleRegistry(nc, space, scanner, recordsScanner) {
111
+ const jsm = await jetstreamManager(nc);
112
+ const kvm = new Kvm(nc);
113
+ const recordsBucket = `cotal_records_${space}`;
114
+ const authBucket = epAuthBucket(space);
115
+ let recordsKv, authKv;
116
+ let recordsCfg;
117
+ try {
118
+ recordsKv = await kvm.open(recordsBucket);
119
+ recordsCfg = (await jsm.streams.info(`KV_${recordsBucket}`)).config;
120
+ }
121
+ catch (e) {
122
+ throw new EpEnvelopeError("failed-precondition", `the records store ${recordsBucket} is not provisioned (run space setup; SPEC 13.12): ${e?.message ?? String(e)}`);
123
+ }
124
+ assertAuthorityStreamShape(recordsCfg, recordsBucket);
125
+ let authCfg;
126
+ try {
127
+ authKv = await kvm.open(authBucket);
128
+ authCfg = (await jsm.streams.info(`KV_${authBucket}`)).config;
129
+ }
130
+ catch (e) {
131
+ throw new EpEnvelopeError("failed-precondition", `the auth store ${authBucket} is not provisioned (run space setup; SPEC 13.12): ${e?.message ?? String(e)}`);
132
+ }
133
+ assertAuthorityStreamShape(authCfg, authBucket);
134
+ if (authCfg.allow_direct !== false)
135
+ throw new EpEnvelopeError("failed-precondition", `the auth store ${authBucket} has allow_direct=${String(authCfg.allow_direct)}, not false; a Direct-Get-capable gate store defeats read-your-writes (SPEC 13.1); reprovision`);
136
+ // The injected scanner must be a REAL scanner (built by ledger-scanner.ts) bonded to THIS space —
137
+ // a hand-assembled structural object or a foreign-space scanner would enumerate an empty/wrong
138
+ // family and let a barrier advance over live descendants (SPEC 13.1/13.12). Same anti-hand-
139
+ // assembly + space-bond discipline the registry itself carries.
140
+ if (scanner !== undefined)
141
+ assertScannerSpace(scanner, space);
142
+ // Same anti-hand-assembly + space-bond discipline for the records scanner (SPEC 13.9, site 3): a
143
+ // foreign-space or hand-built records scanner would let the retirement barrier's obligation drain
144
+ // declare quiescence over live obligations it never read.
145
+ if (recordsScanner !== undefined)
146
+ assertRecordsScannerSpace(recordsScanner, space);
147
+ const reg = Object.freeze({ space });
148
+ REGISTRIES.set(reg, { space, recordsKv, authKv, jsm, js: jetstream(nc), work: await workPoolContext(nc, space), scanner, recordsScanner });
149
+ return reg;
150
+ }
151
+ /** Open the sealed read-only mapping reader. Its scoped credential holds EXACTLY the records
152
+ * leader read (`STREAM.MSG.GET`) plus the records `STREAM.INFO` for the bind-time shape proof
153
+ * (SPEC 13.9/13.12): a reader that cannot prove it is bound to the primary, non-evicting
154
+ * records store refuses to serve authority reads (it could otherwise leader-read a MIRROR's
155
+ * leader and call that the mapping). */
156
+ export async function openLifecycleMappingReader(nc, space) {
157
+ const recordsBucket = `cotal_records_${space}`;
158
+ let jsm;
159
+ let recordsCfg;
160
+ try {
161
+ jsm = await jetstreamManager(nc);
162
+ recordsCfg = (await jsm.streams.info(`KV_${recordsBucket}`)).config;
163
+ }
164
+ catch (e) {
165
+ throw new EpEnvelopeError("failed-precondition", `the mapping reader cannot bind + shape-prove the records store ${recordsBucket} (the reader profile holds the API probe, STREAM.INFO, and its own connection-scoped inbox, SPEC 13.9): ${e?.message ?? String(e)}`);
166
+ }
167
+ assertAuthorityStreamShape(recordsCfg, recordsBucket);
168
+ const rd = Object.freeze({ space });
169
+ READERS.set(rd, { space, jsm });
170
+ return rd;
171
+ }
172
+ const HEAD_STATES = new Set(["active", "retiring", "retired"]);
173
+ function headKey(owner, actor) {
174
+ return recordAtomicKey(LIFECYCLE_HEAD, [owner, actor]);
175
+ }
176
+ function uidKey(lifecycleUid) {
177
+ return recordAtomicKey(UID_RESERVATION, [lifecycleUid]);
178
+ }
179
+ /** Validate a head value at the consuming boundary — CLOSED schema (nested `op` included), and
180
+ * the embedded owner/actor MUST agree with the key so a key-mismatched row never authorizes. */
181
+ function parseMapping(raw, key, owner, actor) {
182
+ let o;
183
+ try {
184
+ o = JSON.parse(dec.decode(raw));
185
+ }
186
+ catch {
187
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} is not JSON; garbled trusted-path state never authorizes (SPEC 13.1)`);
188
+ }
189
+ if (!isRec(o))
190
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} is not an object`);
191
+ const allowed = new Set(["owner", "actor", "lifecycleUid", "managerInstance", "processEpoch", "state", "currentCredentialId", "lastTakeoverOpId", "op"]);
192
+ for (const k of Object.keys(o))
193
+ if (!allowed.has(k))
194
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} carries the unknown field "${k}" (closed schema, SPEC 13.1)`);
195
+ if (o.owner !== owner || o.actor !== actor ||
196
+ typeof o.lifecycleUid !== "string" || typeof o.managerInstance !== "string" || o.managerInstance.length === 0 ||
197
+ !uint(o.processEpoch) || o.processEpoch < 1 || typeof o.state !== "string" || !HEAD_STATES.has(o.state) ||
198
+ (o.currentCredentialId !== undefined && (typeof o.currentCredentialId !== "string" || o.currentCredentialId.length === 0)) ||
199
+ (o.lastTakeoverOpId !== undefined && typeof o.lastTakeoverOpId !== "string"))
200
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} does not validate (owner/actor/uid/epoch/state); a garbled or key-mismatched head never authorizes (SPEC 13.1/13.3)`);
201
+ try {
202
+ assertLifecycleToken(o.lifecycleUid);
203
+ if (o.lastTakeoverOpId !== undefined)
204
+ assertLifecycleToken(o.lastTakeoverOpId);
205
+ }
206
+ catch {
207
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} carries a malformed lifecycleUid/lastTakeoverOpId (SPEC 13.1)`);
208
+ }
209
+ // The retirement op intent: REQUIRED at `retiring`, forbidden elsewhere; itself closed.
210
+ if (o.state === "retiring") {
211
+ if (!isRec(o.op))
212
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} is retiring without its durable op intent (SPEC 13.1: retiring is op-bound)`);
213
+ }
214
+ else if (o.op !== undefined) {
215
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} carries an op intent in state "${o.state}" (SPEC 13.1: only retiring is op-bound)`);
216
+ }
217
+ if (o.op !== undefined) {
218
+ const op = o.op;
219
+ for (const k of Object.keys(op))
220
+ if (k !== "opId" && k !== "kind")
221
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} op intent carries the unknown field "${k}" (closed schema)`);
222
+ if (typeof op.opId !== "string" || op.kind !== "retirement")
223
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} op intent does not validate (SPEC 13.1)`);
224
+ try {
225
+ assertLifecycleToken(op.opId);
226
+ }
227
+ catch {
228
+ throw new EpEnvelopeError("internal", `the lifecycle head ${key} op intent carries a malformed opId (SPEC 13.1)`);
229
+ }
230
+ }
231
+ return o;
232
+ }
233
+ /** Candidate read for a CAS-fenced mutation (raw `kv.get`; the auth decision is the CAS itself,
234
+ * §13.1: a read is never a fence). A DEL/PURGE marker is CORRUPTION, never absence. */
235
+ async function readHeadCandidate(kv, owner, actor) {
236
+ const key = headKey(owner, actor);
237
+ const entry = await kv.get(key);
238
+ if (!entry)
239
+ return undefined;
240
+ if (entry.operation !== "PUT")
241
+ throw new EpEnvelopeError("failed-precondition", `the lifecycle head ${key} carries a ${entry.operation} marker; an authority head is never deleted (a deletion is corruption, not absence, SPEC 13.12)`);
242
+ return { mapping: parseMapping(entry.value, key, owner, actor), revision: entry.revision };
243
+ }
244
+ /** PACKAGE-INTERNAL accessor for the trusted auth path's sibling modules (the credential
245
+ * ledger + issuance barrier, which are the SAME authority over the SAME stores). Deliberately
246
+ * never re-exported from the package index: a sealed registry stays the only door. */
247
+ export function registryStores(reg) {
248
+ return internals(reg);
249
+ }
250
+ /** PACKAGE-INTERNAL: the barrier's CLOSED enumeration seam (SPEC 13.9). Returns the sealed
251
+ * auth-ledger scanner or throws — a registry constructed WITHOUT one (the mint writer) never
252
+ * enumerates, so asking it to is a composition bug, not a silent degrade. Exposes only the closed
253
+ * scan ops; the raw scanner/connection/credential stay inside {@link openAuthLedgerScanner}. */
254
+ export function registryScanner(reg) {
255
+ const s = internals(reg).scanner;
256
+ if (s === undefined)
257
+ throw new EpEnvelopeError("failed-precondition", "this lifecycle registry was opened without a sealed auth-ledger scanner; only the barrier registry enumerates (SPEC 13.9); open it with openAuthLedgerScanner");
258
+ return s;
259
+ }
260
+ /** PACKAGE-INTERNAL: the retirement barrier's CLOSED obligation-enumeration seam (SPEC 13.9,
261
+ * site 3). Returns the sealed records scanner or throws — a registry constructed WITHOUT one never
262
+ * drains obligations, so asking it to is a composition bug, not a silent degrade. Exposes only the
263
+ * closed scan op; the raw scanner/connection/credential stay inside {@link openRecordsScanner}. */
264
+ export function registryRecordsScanner(reg) {
265
+ const s = internals(reg).recordsScanner;
266
+ if (s === undefined)
267
+ throw new EpEnvelopeError("failed-precondition", "this lifecycle registry was opened without a sealed records scanner; the retirement barrier's obligation drain requires one (SPEC 13.9); open it with openRecordsScanner");
268
+ return s;
269
+ }
270
+ /** PACKAGE-INTERNAL: the barrier's candidate read of an alias head (the credential-ledger
271
+ * takeover barrier captures its `fromEpoch` coordinate from it, and its epoch CAS re-reads
272
+ * through {@link advanceEpochWithinTakeover}). Same never-deleted discipline as every head
273
+ * read; never re-exported from the package index. */
274
+ export async function readLifecycleHeadForOperation(reg, owner, actor) {
275
+ return readHeadCandidate(internals(reg).recordsKv, owner, actor);
276
+ }
277
+ /** The takeover barrier's epoch-advance head CAS (SPEC 13.1: NO public epoch advance exists;
278
+ * this is a finalization step of the takeover barrier, module-internal for the credential
279
+ * ledger's barrier only, and idempotent for the barrier's crash-resume). Advances the epoch by
280
+ * exactly one, revision-pinned, only while the head is ACTIVE at the SAME uid. */
281
+ export async function advanceEpochWithinTakeover(reg, args) {
282
+ const { recordsKv } = internals(reg);
283
+ const cur = await readHeadCandidate(recordsKv, args.owner, args.actor);
284
+ if (cur === undefined || cur.mapping.state !== "active" || cur.mapping.lifecycleUid !== args.lifecycleUid)
285
+ throw new EpEnvelopeError("failed-precondition", `the takeover epoch advance for "${args.owner}/${args.actor}" requires an ACTIVE head at uid ${args.lifecycleUid}; found ${cur === undefined ? "no head" : `${cur.mapping.state} at ${cur.mapping.lifecycleUid}`} (SPEC 13.1)`);
286
+ if (cur.mapping.processEpoch === args.fromEpoch + 1) {
287
+ // Idempotent ONLY for our OWN completed advance: the epoch stamp binds the completion to one
288
+ // op, so a LOSING concurrent takeover that captured the same fromEpoch finds a foreign opId
289
+ // and refuses, never claiming the winner's advance (SPEC 13.1).
290
+ if (cur.mapping.lastTakeoverOpId !== args.opId)
291
+ throw new EpEnvelopeError("conflict", `the head for "${args.owner}/${args.actor}" is at epoch ${args.fromEpoch + 1} advanced by operation ${cur.mapping.lastTakeoverOpId ?? "<none>"}, not ${args.opId}; a concurrent takeover won and this operation lost (SPEC 13.1)`);
292
+ // C1 (panel HIGH): our own completed advance MUST have cleared the revoked root stamp too. If
293
+ // the epoch advanced under our opId but `currentCredentialId` is still set, that is IMPOSSIBLE
294
+ // persisted state (a partial write or an old binary) — fail loud rather than report success
295
+ // over a head that still wedges the successor mint.
296
+ if (cur.mapping.currentCredentialId !== undefined)
297
+ throw new EpEnvelopeError("failed-precondition", `the head for "${args.owner}/${args.actor}" advanced under takeover ${args.opId} but still names root credential ${cur.mapping.currentCredentialId}; the epoch CAS clears it atomically, so a residual stamp is impossible persisted state (SPEC 13.1)`);
298
+ return "already-advanced";
299
+ }
300
+ if (cur.mapping.processEpoch !== args.fromEpoch)
301
+ throw new EpEnvelopeError("failed-precondition", `the head for "${args.owner}/${args.actor}" is at epoch ${cur.mapping.processEpoch}, not the takeover's captured epoch ${args.fromEpoch} (or its +1); a foreign operation moved it (SPEC 13.1)`);
302
+ // C1 (panel HIGH, all lanes): advance the epoch AND clear `currentCredentialId` in the SAME CAS.
303
+ // The takeover's family revoke marked the incarnation's root row `revoked`; leaving the head
304
+ // still naming that revoked root permanently wedges the successor mint (`ensureRootCredential`'s
305
+ // fast path reads the stamped id, refuses its revoked state, and `setCurrentRootCredential`
306
+ // refuses a value flip). Clearing the stamp makes the head's root slot ABSENT, so the successor's
307
+ // release-last stamp can win. Root rotation stays a barrier's job — this CAS IS that barrier step.
308
+ const { currentCredentialId: _revoked, ...rest } = cur.mapping;
309
+ void _revoked;
310
+ await updateRecordEntry(recordsKv, headKey(args.owner, args.actor), { ...rest, processEpoch: args.fromEpoch + 1, lastTakeoverOpId: assertLifecycleToken(args.opId) }, cur.revision);
311
+ return "advanced";
312
+ }
313
+ /**
314
+ * The issuance path's head CAS stamping the incarnation's ROOT credential (SPEC 13.1: the head's
315
+ * `currentCredentialId` is what the connect arm's root-path equality check reads, so a superseded
316
+ * root issuance is denied even while its old row still reads active). The mint protocol's
317
+ * RELEASE-LAST final step: the active `cred.` row is durable and its gate finalize has won BEFORE
318
+ * this runs, and the bearer bytes release only after it.
319
+ *
320
+ * ABSENT → value ONLY (idempotent for the SAME value): a head that already names a DIFFERENT root
321
+ * credential REFUSES — flipping `currentCredentialId` without the full family revoke would leave
322
+ * the old root's descendants connectable under the leaf check, so root ROTATION is exclusively a
323
+ * barrier's job (takeover/retirement), never this seam's. Revision-pinned; ACTIVE at the SAME uid
324
+ * only; a foreign head movement between the caller's read and this CAS loses fail-closed.
325
+ */
326
+ export async function setCurrentRootCredential(reg, args) {
327
+ const { recordsKv } = internals(reg);
328
+ if (typeof args.credentialId !== "string" || args.credentialId.length === 0)
329
+ throw new EpEnvelopeError("failed-precondition", "setCurrentRootCredential requires a credentialId");
330
+ const cur = await readHeadCandidate(recordsKv, args.owner, args.actor);
331
+ if (cur === undefined || cur.mapping.state !== "active" || cur.mapping.lifecycleUid !== args.lifecycleUid)
332
+ throw new EpEnvelopeError("failed-precondition", `stamping the root credential for "${args.owner}/${args.actor}" requires an ACTIVE head at uid ${args.lifecycleUid}; found ${cur === undefined ? "no head" : `${cur.mapping.state} at ${cur.mapping.lifecycleUid}`} (SPEC 13.1)`);
333
+ if (cur.mapping.currentCredentialId === args.credentialId)
334
+ return; // our own completed stamp
335
+ if (cur.mapping.currentCredentialId !== undefined)
336
+ throw new EpEnvelopeError("permission-denied", `the head for "${args.owner}/${args.actor}" already names root credential ${cur.mapping.currentCredentialId}; rotating it takes the full family-revoke barrier, never a bare head flip (the old root's descendants would stay connectable under the leaf check, SPEC 13.1)`);
337
+ await updateRecordEntry(recordsKv, headKey(args.owner, args.actor), { ...cur.mapping, currentCredentialId: args.credentialId }, cur.revision);
338
+ }
339
+ /** The retirement barrier's head CONTAINMENT CAS (SPEC 13.1: `active → retiring`, bound to the
340
+ * retirement operation's durable intent — from this point every currency seam yields no current
341
+ * mapping and no current epoch, and the alias is NOT replaceable). PACKAGE-INTERNAL for the
342
+ * barrier only (no public retire seam exists); idempotent for the barrier's crash-resume. */
343
+ export async function beginHeadRetirementWithinBarrier(reg, args) {
344
+ const { recordsKv } = internals(reg);
345
+ const cur = await readHeadCandidate(recordsKv, args.owner, args.actor);
346
+ if (cur === undefined || cur.mapping.lifecycleUid !== args.lifecycleUid)
347
+ throw new EpEnvelopeError("failed-precondition", `the retirement of uid ${args.lifecycleUid} requires the head for "${args.owner}/${args.actor}" to name it; found ${cur === undefined ? "no head" : `uid ${cur.mapping.lifecycleUid}`} (SPEC 13.1)`);
348
+ if (cur.mapping.state === "retiring") {
349
+ if (cur.mapping.op?.opId !== args.opId)
350
+ throw new EpEnvelopeError("permission-denied", `the head for "${args.owner}/${args.actor}" is retiring under operation ${cur.mapping.op?.opId ?? "<none>"}, not ${args.opId}; one retirement at a time, and a stranger never advances it (SPEC 13.1)`);
351
+ return "already-retiring";
352
+ }
353
+ if (cur.mapping.state !== "active")
354
+ throw new EpEnvelopeError("failed-precondition", `the head for "${args.owner}/${args.actor}" is "${cur.mapping.state}", not active; only an active head enters retirement containment (a completed terminal is decided at the gate, never re-entered here, SPEC 13.1)`);
355
+ await updateRecordEntry(recordsKv, headKey(args.owner, args.actor), { ...cur.mapping, state: "retiring", op: { opId: assertLifecycleToken(args.opId), kind: "retirement" } }, cur.revision);
356
+ return "retiring";
357
+ }
358
+ /** The retirement barrier's TERMINAL head CAS (`retiring → retired`, op-pinned) — the barrier's
359
+ * LAST step (SPEC 13.1: `retired` ASSERTS completed cleanup, which is what makes the alias
360
+ * replaceable). The op intent is dropped (it belongs to `retiring` only); idempotence at
361
+ * `retired` is decided by the CALLER against the gate's terminal op (the retired head itself
362
+ * carries no retirement stamp). PACKAGE-INTERNAL for the barrier only. */
363
+ export async function completeHeadRetirementWithinBarrier(reg, args) {
364
+ const { recordsKv } = internals(reg);
365
+ const cur = await readHeadCandidate(recordsKv, args.owner, args.actor);
366
+ if (cur === undefined || cur.mapping.lifecycleUid !== args.lifecycleUid)
367
+ throw new EpEnvelopeError("failed-precondition", `the retirement terminal for uid ${args.lifecycleUid} requires the head for "${args.owner}/${args.actor}" to name it; found ${cur === undefined ? "no head" : `uid ${cur.mapping.lifecycleUid}`}; a replaced head is settled at the gate, never here (SPEC 13.1)`);
368
+ if (cur.mapping.state === "retired")
369
+ return "already-retired";
370
+ if (cur.mapping.state !== "retiring" || cur.mapping.op?.opId !== args.opId)
371
+ throw new EpEnvelopeError("permission-denied", `the head for "${args.owner}/${args.actor}" is ${cur.mapping.state === "retiring" ? `retiring under operation ${cur.mapping.op?.opId ?? "<none>"}` : `"${cur.mapping.state}"`}, not retiring under ${args.opId}; only the containing operation terminalizes its own retirement (SPEC 13.1)`);
372
+ const { op: _op, ...rest } = cur.mapping;
373
+ await updateRecordEntry(recordsKv, headKey(args.owner, args.actor), { ...rest, state: "retired" }, cur.revision);
374
+ return "retired";
375
+ }
376
+ /** PACKAGE-INTERNAL: read a UID reservation's audit `{ owner, actor }` (the minting authority
377
+ * recorded it at {@link tryReserveUid}). The credential ledger uses it to BIND a mint's
378
+ * `holderPrincipal` to the reserved identity, so a trusted caller cannot ledger a row that
379
+ * names a foreign principal for the barrier to evict. A DEL/PURGE marker refuses loudly. */
380
+ export async function readUidReservation(reg, lifecycleUid) {
381
+ const { recordsKv } = internals(reg);
382
+ const entry = await recordsKv.get(uidKey(assertLifecycleToken(lifecycleUid)));
383
+ if (!entry)
384
+ return undefined;
385
+ if (entry.operation !== "PUT")
386
+ throw new EpEnvelopeError("failed-precondition", `the uid reservation for ${lifecycleUid} carries a ${entry.operation} marker; a reservation is never deleted (corruption, SPEC 13.12)`);
387
+ let o;
388
+ try {
389
+ o = JSON.parse(dec.decode(entry.value));
390
+ }
391
+ catch {
392
+ throw new EpEnvelopeError("internal", `the uid reservation for ${lifecycleUid} is not JSON (SPEC 13.1)`);
393
+ }
394
+ if (!isRec(o) || typeof o.owner !== "string" || typeof o.actor !== "string" || o.owner.length === 0 || o.actor.length === 0)
395
+ throw new EpEnvelopeError("internal", `the uid reservation for ${lifecycleUid} does not carry a valid owner/actor audit (SPEC 13.1)`);
396
+ return { owner: o.owner, actor: o.actor };
397
+ }
398
+ // ---- the space-global UID reservation (records store) ----------------------------------------
399
+ /** Try to reserve ONE explicit candidate UID (test/provisioning-internal; production paths use
400
+ * {@link reserveLifecycleUid}). Create-only: `"won"` reserves it forever; `"burned"` means the
401
+ * candidate already exists OR carries a deletion marker — either way it is unusable, per the
402
+ * never-reuse rule. NOT exported from the package index: an explicit candidate is only for
403
+ * probes and migration tooling, never a caller-chosen identity. */
404
+ export async function tryReserveUid(reg, lifecycleUid, audit) {
405
+ const { recordsKv } = internals(reg);
406
+ assertLifecycleToken(lifecycleUid);
407
+ try {
408
+ await createRecordEntry(recordsKv, uidKey(lifecycleUid), { owner: audit.owner, actor: audit.actor, mintedBy: audit.mintedBy });
409
+ return "won";
410
+ }
411
+ catch (e) {
412
+ if (isCasLoss(e))
413
+ return "burned";
414
+ throw e;
415
+ }
416
+ }
417
+ /** Reserve a fresh lifecycle UID space-globally (§13.1): mint a CSPRNG candidate, win its
418
+ * create-only reservation, and on a collision burn the candidate and draw another. At ≥128
419
+ * bits a collision is effectively adversarial, so a handful of retries is a correctness
420
+ * formality, not a capacity plan; exhausting them refuses loudly. */
421
+ export async function reserveLifecycleUid(reg, audit) {
422
+ for (let i = 0; i < 4; i++) {
423
+ const candidate = mintLifecycleUid();
424
+ if ((await tryReserveUid(reg, candidate, audit)) === "won")
425
+ return candidate;
426
+ }
427
+ throw new EpEnvelopeError("internal", "four fresh 128-bit UID candidates collided with existing reservations; that is not chance; inspect the uid.> family (SPEC 13.1)");
428
+ }
429
+ const GATE_STATES = new Set(["open", "frozen", "retired"]);
430
+ const GATE_OP_KINDS = new Set(["activation", "takeover", "registration", "retirement"]);
431
+ function gateKey(lifecycleUid) {
432
+ return `gate.${assertLifecycleToken(lifecycleUid)}`;
433
+ }
434
+ function parseGate(raw, key, lifecycleUid) {
435
+ let o;
436
+ try {
437
+ o = JSON.parse(dec.decode(raw));
438
+ }
439
+ catch {
440
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} is not JSON; garbled trusted-path state never authorizes (SPEC 13.1)`);
441
+ }
442
+ if (!isRec(o))
443
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} is not an object`);
444
+ for (const k of Object.keys(o))
445
+ if (k !== "lifecycleUid" && k !== "state" && k !== "generation" && k !== "op")
446
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} carries the unknown field "${k}" (closed schema, SPEC 13.1)`);
447
+ if (o.lifecycleUid !== lifecycleUid || typeof o.state !== "string" || !GATE_STATES.has(o.state) || !uint(o.generation))
448
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} does not validate (uid/state/generation); a garbled or key-mismatched gate never authorizes (SPEC 13.1)`);
449
+ if ((o.state === "frozen" || o.state === "retired") && !isRec(o.op))
450
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} is ${o.state} without its durable op intent (SPEC 13.1: a frozen gate is op-bound, and a retired gate retains its terminalizing op)`);
451
+ if (o.state === "open" && o.op !== undefined)
452
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} is open but carries an op intent (SPEC 13.1: open gates are not op-bound)`);
453
+ if (o.op !== undefined) {
454
+ const op = o.op;
455
+ for (const k of Object.keys(op))
456
+ if (k !== "opId" && k !== "kind" && k !== "successor")
457
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} op intent carries the unknown field "${k}" (closed schema)`);
458
+ if (typeof op.opId !== "string" || typeof op.kind !== "string" || !GATE_OP_KINDS.has(op.kind))
459
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} op intent does not validate (SPEC 13.1)`);
460
+ // STATE x KIND invariant (SPEC 13.1 per-kind transition sets): only an activation orphan or
461
+ // a retirement produces a `retired` gate, so a persisted `retired` gate carrying a
462
+ // takeover/registration kind is IMPOSSIBLE state — refuse it at parse, never let the terminal
463
+ // idempotence path return it as a settled success (fail-closed on corruption, not open).
464
+ if (o.state === "retired" && op.kind !== "activation" && op.kind !== "retirement")
465
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} is retired under a ${op.kind} op; only an activation orphan or a retirement terminalizes (SPEC 13.1); impossible persisted state, refused`);
466
+ if (op.successor !== undefined && (typeof op.successor !== "string" || op.successor.length === 0 || (op.kind !== "takeover" && op.kind !== "registration")))
467
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} op intent carries an invalid successor (SPEC 13.1: only takeover/registration stage successors, and the summary is a non-empty token)`);
468
+ try {
469
+ assertLifecycleToken(op.opId);
470
+ }
471
+ catch {
472
+ throw new EpEnvelopeError("internal", `the issuance gate ${key} op intent carries a malformed opId (SPEC 13.1)`);
473
+ }
474
+ }
475
+ return o;
476
+ }
477
+ /** Observe the gate (the candidate read feeding a revision-pinned CAS; the auth store is
478
+ * leader-only by shape, `allow_direct=false`). A DEL/PURGE marker refuses loudly. */
479
+ export async function observeGate(reg, lifecycleUid) {
480
+ const { authKv } = internals(reg);
481
+ const key = gateKey(lifecycleUid);
482
+ const entry = await authKv.get(key);
483
+ if (!entry)
484
+ return undefined;
485
+ if (entry.operation !== "PUT")
486
+ throw new EpEnvelopeError("failed-precondition", `the issuance gate ${key} carries a ${entry.operation} marker; a gate is never deleted (a deletion is corruption, not absence, SPEC 13.12)`);
487
+ return { row: parseGate(entry.value, key, lifecycleUid), revision: entry.revision };
488
+ }
489
+ async function putGate(authKv, lifecycleUid, row, expectedRevision) {
490
+ try {
491
+ return await authKv.put(gateKey(lifecycleUid), enc.encode(JSON.stringify(row)), { previousSeq: expectedRevision });
492
+ }
493
+ catch (e) {
494
+ if (isRawCasLoss(e))
495
+ throw new EpEnvelopeError("conflict", `the issuance gate CAS for ${gateKey(lifecycleUid)} lost (expected revision ${expectedRevision}); re-read and re-decide (SPEC 13.8)`);
496
+ throw e;
497
+ }
498
+ }
499
+ /** Create the gate FROZEN under its operation's durable intent (create-only: conflicts on an
500
+ * existing gate or a deletion marker). A gate is BORN only under an ACTIVATION intent (SPEC
501
+ * 13.1 per-kind transition sets: takeover/registration/retirement freeze an EXISTING open
502
+ * gate), and only for a UID whose space-global reservation was already WON: a gate over an
503
+ * unreserved UID would mint outside the never-reuse fence. Born unmintable at generation 0:
504
+ * no credential can be released until the operation's own reopen (§13.1 activation saga). */
505
+ export async function createGateFrozen(reg, args) {
506
+ const { authKv, recordsKv } = internals(reg);
507
+ if (args.op.kind !== "activation")
508
+ throw new EpEnvelopeError("failed-precondition", `an issuance gate is born only under an activation intent, not "${String(args.op.kind)}" (SPEC 13.1: other operations freeze an existing open gate)`);
509
+ const reservation = await recordsKv.get(uidKey(assertLifecycleToken(args.lifecycleUid)));
510
+ if (!reservation)
511
+ throw new EpEnvelopeError("failed-precondition", `no uid reservation exists for ${args.lifecycleUid}; the reservation is won BEFORE any gate or head write (SPEC 13.1)`);
512
+ if (reservation.operation !== "PUT")
513
+ throw new EpEnvelopeError("failed-precondition", `the uid reservation for ${args.lifecycleUid} carries a ${reservation.operation} marker; a reservation is never deleted (corruption, not absence, SPEC 13.12)`);
514
+ const row = { lifecycleUid: args.lifecycleUid, state: "frozen", generation: 0, op: { opId: assertLifecycleToken(args.op.opId), kind: "activation" } };
515
+ const revision = await putGate(authKv, args.lifecycleUid, row, 0);
516
+ return { row, revision };
517
+ }
518
+ /** CAS the gate `open → frozen` carrying the freezing operation's durable intent, at the
519
+ * observed revision. The bar of every barrier: a staged mint's own finalize CAS loses. */
520
+ export async function freezeGate(reg, args) {
521
+ const { authKv } = internals(reg);
522
+ if (args.op.successor !== undefined && args.op.kind === "retirement")
523
+ throw new EpEnvelopeError("failed-precondition", "a retirement freeze carries no successor (SPEC 13.1: a retirement has none)");
524
+ if (args.op.successor !== undefined && args.op.successor.length === 0)
525
+ throw new EpEnvelopeError("failed-precondition", "the freeze carries an empty successor token; a summary token is a non-empty stage.<opId> reference or absent (SPEC 13.1); validate before the CAS, never persist corruption");
526
+ const current = await observeGate(reg, args.lifecycleUid);
527
+ if (current === undefined)
528
+ throw new EpEnvelopeError("not-found", `the issuance gate for ${args.lifecycleUid} does not exist (SPEC 13.1)`);
529
+ if (current.row.state !== "open")
530
+ throw new EpEnvelopeError("failed-precondition", `the issuance gate for ${args.lifecycleUid} is "${current.row.state}", not open; only an open gate freezes (a frozen/retired gate belongs to its own operation, SPEC 13.1)`);
531
+ const op = { opId: assertLifecycleToken(args.op.opId), kind: args.op.kind };
532
+ if (args.op.successor !== undefined)
533
+ op.successor = args.op.successor;
534
+ const row = { lifecycleUid: current.row.lifecycleUid, state: "frozen", generation: current.row.generation, op };
535
+ const revision = await putGate(authKv, args.lifecycleUid, row, args.revision);
536
+ return { row, revision };
537
+ }
538
+ /** CAS the gate `frozen → open` at the NEXT generation — op-pinned: only the freeze's own
539
+ * operation (the same `opId`) reopens, as its barrier's final step; a stranger or a stale
540
+ * reconciler refuses before the CAS is even attempted. Per-kind (SPEC 13.1): a reopen
541
+ * belongs to activation, takeover, and a registration abort — NEVER retirement (a
542
+ * retirement freeze never reopens; its only exit is the terminal). */
543
+ export async function reopenGate(reg, args) {
544
+ const { authKv } = internals(reg);
545
+ const current = await observeGate(reg, args.lifecycleUid);
546
+ if (current === undefined)
547
+ throw new EpEnvelopeError("not-found", `the issuance gate for ${args.lifecycleUid} does not exist (SPEC 13.1)`);
548
+ if (current.row.state !== "frozen")
549
+ throw new EpEnvelopeError("failed-precondition", `the issuance gate for ${args.lifecycleUid} is "${current.row.state}", not frozen; there is no freeze to reopen (SPEC 13.1)`);
550
+ if (current.row.op?.opId !== args.opId)
551
+ throw new EpEnvelopeError("permission-denied", `the issuance gate for ${args.lifecycleUid} is frozen by operation ${current.row.op?.opId ?? "<none>"}, not ${args.opId}; only the completing operation reopens its own freeze (SPEC 13.1)`);
552
+ if (current.row.op.kind === "retirement")
553
+ throw new EpEnvelopeError("failed-precondition", `the issuance gate for ${args.lifecycleUid} is frozen by a RETIREMENT; a retirement freeze never reopens (SPEC 13.1: its only exit is the terminal)`);
554
+ const row = { lifecycleUid: current.row.lifecycleUid, state: "open", generation: current.row.generation + 1 };
555
+ const revision = await putGate(authKv, args.lifecycleUid, row, args.revision);
556
+ return { row, revision };
557
+ }
558
+ /** CAS the gate `frozen → retired` (terminal; never reopened) — op-pinned like the reopen. The
559
+ * activation saga uses it to terminalize a head-CAS loser's orphan gate; the retirement
560
+ * barrier uses it as its own gate terminalization step. Per-kind (SPEC 13.1): only an
561
+ * ACTIVATION orphan or a RETIREMENT terminalizes — a takeover/registration freeze aborts by
562
+ * reopening, never by the terminal. Idempotence at `retired` is SAME-OP idempotence: a
563
+ * stranger's retry on a terminal gate refuses, it does not "succeed". */
564
+ export async function retireGate(reg, args) {
565
+ const { authKv } = internals(reg);
566
+ const current = await observeGate(reg, args.lifecycleUid);
567
+ if (current === undefined)
568
+ throw new EpEnvelopeError("not-found", `the issuance gate for ${args.lifecycleUid} does not exist (SPEC 13.1)`);
569
+ if (current.row.state === "retired") {
570
+ if (current.row.op?.opId !== args.opId)
571
+ throw new EpEnvelopeError("permission-denied", `the issuance gate for ${args.lifecycleUid} was terminalized by operation ${current.row.op?.opId ?? "<none>"}, not ${args.opId}; terminal idempotence is same-op idempotence (SPEC 13.1)`);
572
+ return current; // idempotent terminal, same op
573
+ }
574
+ if (current.row.state !== "frozen")
575
+ throw new EpEnvelopeError("failed-precondition", `the issuance gate for ${args.lifecycleUid} is "${current.row.state}"; only a frozen gate terminalizes (freeze first; the bar precedes the terminal, SPEC 13.1)`);
576
+ if (current.row.op?.opId !== args.opId)
577
+ throw new EpEnvelopeError("permission-denied", `the issuance gate for ${args.lifecycleUid} is frozen by operation ${current.row.op?.opId ?? "<none>"}, not ${args.opId}; only the owning operation terminalizes its freeze (SPEC 13.1)`);
578
+ if (current.row.op.kind !== "activation" && current.row.op.kind !== "retirement")
579
+ throw new EpEnvelopeError("failed-precondition", `the issuance gate for ${args.lifecycleUid} is frozen by a ${current.row.op.kind}; only an activation orphan or a retirement terminalizes (a ${current.row.op.kind} aborts by reopening, SPEC 13.1)`);
580
+ const row = { lifecycleUid: current.row.lifecycleUid, state: "retired", generation: current.row.generation, op: current.row.op };
581
+ const revision = await putGate(authKv, args.lifecycleUid, row, args.revision);
582
+ return { row, revision };
583
+ }
584
+ // ---- the activation saga (§13.1: reserve → gate frozen → head CAS → reopen LAST) -------------
585
+ /**
586
+ * Activate the alias `(owner, actor)`: the full §13.1 initial-activation saga. Refuses an
587
+ * `active` head (`already-exists`: a takeover advances the epoch through its barrier, never a
588
+ * new incarnation) and a `retiring` head (`failed-precondition`: a retiring alias is NOT
589
+ * replaceable until its barrier completes). A virgin alias activates by create-only head CAS; a
590
+ * `retired` predecessor is replaced by revision-pinned CAS with a FRESH reserved UID. The
591
+ * head-CAS loser terminalizes its own orphan gate (its UID stays burned forever) and rethrows
592
+ * the `conflict`. Returns the new mapping, its store revision (= `mappingRevision`), and the
593
+ * saga's `opId` (the durable intent a crashed caller resumes with, {@link resumeActivation}).
594
+ *
595
+ * NOT wired to any production spawn path in this slice; the ledger slice adds credential
596
+ * minting under the reopened gate before this becomes reachable.
597
+ */
598
+ export async function activateLifecycle(reg, args) {
599
+ const { recordsKv } = internals(reg);
600
+ const { owner, actor } = args;
601
+ if (typeof args.managerInstance !== "string" || args.managerInstance.length === 0)
602
+ throw new EpEnvelopeError("failed-precondition", "activateLifecycle requires a managerInstance (the minting authority)");
603
+ const current = await readHeadCandidate(recordsKv, owner, actor);
604
+ if (current !== undefined && current.mapping.state === "active")
605
+ throw new EpEnvelopeError("already-exists", `lifecycle "${owner}/${actor}" is already active (uid ${current.mapping.lifecycleUid}); a takeover advances the epoch through its barrier, it does not re-activate (SPEC 13.1)`);
606
+ if (current !== undefined && current.mapping.state === "retiring")
607
+ throw new EpEnvelopeError("failed-precondition", `lifecycle "${owner}/${actor}" is retiring (op ${current.mapping.op?.opId}); a retiring alias is not replaceable until its barrier completes (SPEC 13.1)`);
608
+ const opId = mintLifecycleUid();
609
+ // 1. Win the space-global UID reservation.
610
+ const lifecycleUid = await reserveLifecycleUid(reg, { owner, actor, mintedBy: args.managerInstance });
611
+ // 2. Create the gate FROZEN under this activation's durable intent (unmintable from birth).
612
+ const gate = await createGateFrozen(reg, { lifecycleUid, op: { opId, kind: "activation" } });
613
+ // 3. CAS the alias head (create-only for virgin; revision-pinned over the retired predecessor).
614
+ const mapping = { owner, actor, lifecycleUid, managerInstance: args.managerInstance, processEpoch: 1, state: "active" };
615
+ let revision;
616
+ try {
617
+ revision = current === undefined
618
+ ? await createRecordEntry(recordsKv, headKey(owner, actor), mapping)
619
+ : await updateRecordEntry(recordsKv, headKey(owner, actor), mapping, current.revision);
620
+ }
621
+ catch (e) {
622
+ if (isCasLoss(e)) {
623
+ // The loser terminalizes ITS OWN orphan gate; its UID stays burned (never deleted, never
624
+ // reused). A cleanup failure is NEVER swallowed as success: the caller gets the durable
625
+ // coordinates and resumes the SAME op (resumeActivation) to finish the terminalization.
626
+ try {
627
+ await retireGate(reg, { lifecycleUid, revision: gate.revision, opId });
628
+ }
629
+ catch (cleanup) {
630
+ // The resume coordinates ride STRUCTURED details (never only the prose message): a
631
+ // recovery path reads {uid, opId} from `details`, it does not parse a sentence.
632
+ throw new EpEnvelopeError("unavailable", `lifecycle activation for "${owner}/${actor}" lost the head CAS AND terminalizing its orphan gate failed; the uid ${lifecycleUid} is burned but its gate is still frozen by op ${opId}; resume the same op with resumeActivation: ${cleanup?.message ?? String(cleanup)}`, [{ kind: "resume-activation", owner, actor, lifecycleUid, opId }]);
633
+ }
634
+ throw new EpEnvelopeError("conflict", `lifecycle activation for "${owner}/${actor}" lost the head CAS (a concurrent activation won); this saga's uid ${lifecycleUid} is burned and its gate terminalized (SPEC 13.1)`);
635
+ }
636
+ throw e;
637
+ }
638
+ // 4. Reopen the gate at its first mintable generation — the saga's LAST step.
639
+ await reopenGate(reg, { lifecycleUid, revision: gate.revision, opId });
640
+ return { mapping, revision, opId };
641
+ }
642
+ /**
643
+ * Activate the alias `(owner, actor)` AT THE CALLER'S uid — the production ISSUANCE activation
644
+ * (SPEC 13.1). The grant row already minted the incarnation's uid and every bearer's
645
+ * lifecycle-equality is bound to it, so a fresh reservation ({@link activateLifecycle}) would
646
+ * strand the grant. Same saga order (reserve → gate frozen → head CAS → reopen LAST) with
647
+ * ADOPT-instead-of-burn resume semantics: a reservation or frozen ACTIVATION gate already carried
648
+ * by OUR alias at this uid is a prior attempt's durable progress and is adopted — burning the
649
+ * grant's uid would permanently brick the grant — and a CAS loss to a SIBLING (same alias, same
650
+ * uid) converges on the winner's state instead of refusing.
651
+ *
652
+ * Returns with the head ACTIVE at `lifecycleUid` and the issuance gate OPEN. Refuses loudly: an
653
+ * active head at a DIFFERENT uid (`already-exists` — retiring a live predecessor is the takeover
654
+ * barrier's job, which production issuance does not run in R1), a retiring head, a reservation
655
+ * held by a FOREIGN alias, a foreign-operation freeze, and a terminally retired gate.
656
+ */
657
+ export async function activateLifecycleAtUid(reg, args) {
658
+ const { recordsKv } = internals(reg);
659
+ const { owner, actor, lifecycleUid } = args;
660
+ assertLifecycleToken(lifecycleUid);
661
+ if (typeof args.managerInstance !== "string" || args.managerInstance.length === 0)
662
+ throw new EpEnvelopeError("failed-precondition", "activateLifecycleAtUid requires a managerInstance (the minting authority)");
663
+ const current = await readHeadCandidate(recordsKv, owner, actor);
664
+ if (current !== undefined && current.mapping.state === "active" && current.mapping.lifecycleUid !== lifecycleUid)
665
+ throw new EpEnvelopeError("already-exists", `lifecycle "${owner}/${actor}" is active at uid ${current.mapping.lifecycleUid}, not this grant's ${lifecycleUid}; retiring a live predecessor is the takeover barrier's job and production issuance does not run it (R1) - despawn/retire the predecessor first, or grant a fresh actor name (SPEC 13.1)`);
666
+ if (current !== undefined && current.mapping.state === "retiring")
667
+ throw new EpEnvelopeError("failed-precondition", `lifecycle "${owner}/${actor}" is retiring (op ${current.mapping.op?.opId}); a retiring alias is not replaceable until its barrier completes (SPEC 13.1)`);
668
+ const headIsOurs = current !== undefined && current.mapping.state === "active"; // same uid, by the guard above
669
+ // 1. The uid reservation: win it, or adopt a prior attempt's — SAME alias only.
670
+ if (!headIsOurs && (await tryReserveUid(reg, lifecycleUid, { owner, actor, mintedBy: args.managerInstance })) === "burned") {
671
+ const res = await readUidReservation(reg, lifecycleUid);
672
+ if (res === undefined || res.owner !== owner || res.actor !== actor)
673
+ throw new EpEnvelopeError("permission-denied", `uid ${lifecycleUid} is reserved by ${res ? `"${res.owner}/${res.actor}"` : "an unreadable reservation"}, not "${owner}/${actor}"; a grant's uid is never adopted across aliases (SPEC 13.1)`);
674
+ }
675
+ // 2. The activation gate: create frozen, or adopt OUR prior attempt's frozen activation gate
676
+ // (the reservation above already binds this uid to this alias, so any activation freeze on
677
+ // it is this alias's own activation). A retry loop absorbs the sibling-race CAS losses.
678
+ for (let attempt = 0;; attempt++) {
679
+ if (attempt > 4)
680
+ throw new EpEnvelopeError("unavailable", `activation for "${owner}/${actor}" at uid ${lifecycleUid} keeps losing its gate/head CASes to concurrent movement; re-read and re-decide (SPEC 13.1)`);
681
+ let gate = await observeGate(reg, lifecycleUid);
682
+ let opId;
683
+ if (gate === undefined) {
684
+ try {
685
+ gate = await createGateFrozen(reg, { lifecycleUid, op: { opId: mintLifecycleUid(), kind: "activation" } });
686
+ }
687
+ catch (e) {
688
+ if (isCasLoss(e))
689
+ continue; // a sibling created it; re-observe and adopt
690
+ throw e;
691
+ }
692
+ opId = gate.row.op.opId;
693
+ }
694
+ else if (gate.row.state === "frozen") {
695
+ if (gate.row.op?.kind !== "activation")
696
+ throw new EpEnvelopeError("failed-precondition", `the issuance gate for ${lifecycleUid} is frozen by a ${gate.row.op?.kind ?? "<unknown>"} (op ${gate.row.op?.opId ?? "<none>"}); a barrier is in flight - issuance activation neither adopts nor overrides it (SPEC 13.1)`);
697
+ opId = gate.row.op.opId;
698
+ }
699
+ else if (gate.row.state === "retired") {
700
+ throw new EpEnvelopeError("permission-denied", `uid ${lifecycleUid} has a terminally retired issuance gate; a burned uid never re-activates - re-grant the actor for a fresh incarnation (SPEC 13.1)`);
701
+ }
702
+ else {
703
+ // Open gate: the saga writes the head BEFORE its reopen, so an open gate with the head
704
+ // active at our uid is a COMPLETED activation; anything else is foreign movement.
705
+ const head = await readHeadCandidate(recordsKv, owner, actor);
706
+ if (head !== undefined && head.mapping.state === "active" && head.mapping.lifecycleUid === lifecycleUid)
707
+ return;
708
+ throw new EpEnvelopeError("failed-precondition", `the issuance gate for ${lifecycleUid} is open but the head for "${owner}/${actor}" is ${head === undefined ? "absent" : `${head.mapping.state} at ${head.mapping.lifecycleUid}`}; an activation reopens only AFTER its head CAS - this is foreign movement or corruption, refuse (SPEC 13.1/13.12)`);
709
+ }
710
+ // 3. The head CAS (create-only for virgin; revision-pinned over a retired predecessor). A
711
+ // loss converges if the sibling won for the SAME uid, refuses on a foreign winner.
712
+ if (!headIsOurs) {
713
+ const mapping = { owner, actor, lifecycleUid, managerInstance: args.managerInstance, processEpoch: 1, state: "active" };
714
+ try {
715
+ if (current === undefined)
716
+ await createRecordEntry(recordsKv, headKey(owner, actor), mapping);
717
+ else
718
+ await updateRecordEntry(recordsKv, headKey(owner, actor), mapping, current.revision);
719
+ }
720
+ catch (e) {
721
+ if (!isCasLoss(e))
722
+ throw e;
723
+ const head = await readHeadCandidate(recordsKv, owner, actor);
724
+ if (!(head !== undefined && head.mapping.state === "active" && head.mapping.lifecycleUid === lifecycleUid))
725
+ throw new EpEnvelopeError("conflict", `activation for "${owner}/${actor}" at uid ${lifecycleUid} lost the head CAS to a foreign movement (now ${head === undefined ? "absent" : `${head.mapping.state} at ${head.mapping.lifecycleUid}`}); re-grant raced this exchange - re-exchange (SPEC 13.1)`);
726
+ }
727
+ }
728
+ // 4. Reopen the gate — the saga's LAST step. A loss to the sibling's reopen is convergence.
729
+ try {
730
+ await reopenGate(reg, { lifecycleUid, revision: gate.revision, opId });
731
+ return;
732
+ }
733
+ catch (e) {
734
+ const g = await observeGate(reg, lifecycleUid);
735
+ if (g !== undefined && g.row.state === "open")
736
+ return; // the sibling finished it
737
+ if (e instanceof EpEnvelopeError && e.code === "conflict")
738
+ continue; // revision moved; re-observe
739
+ throw e;
740
+ }
741
+ }
742
+ }
743
+ /**
744
+ * Resume a crashed activation saga from its durable coordinates (`{alias, lifecycleUid, opId}`,
745
+ * the intent the minting authority persists before step 1 and the gate carries from step 2).
746
+ * Reads the durable state and finishes the SAME operation deterministically:
747
+ * - head active at OUR uid → finish step 4 (reopen the gate) if it is still frozen by us;
748
+ * - head absent / retired / owned by another uid → our head CAS never won (or never ran):
749
+ * terminalize our orphan gate; the uid stays burned.
750
+ * Idempotent; never advances another operation's freeze (the op-pinned CAS refuses). Returns
751
+ * what it did.
752
+ */
753
+ export async function resumeActivation(reg, args) {
754
+ const { recordsKv } = internals(reg);
755
+ const head = await readHeadCandidate(recordsKv, args.owner, args.actor);
756
+ const gate = await observeGate(reg, args.lifecycleUid);
757
+ const won = head !== undefined && head.mapping.state === "active" && head.mapping.lifecycleUid === args.lifecycleUid;
758
+ if (gate === undefined) {
759
+ // Crash before step 2: nothing durable beyond the reservation; the uid stays burned.
760
+ if (won)
761
+ throw new EpEnvelopeError("internal", `the head names uid ${args.lifecycleUid} but its gate does not exist; an active head without a gate is corruption (SPEC 13.1)`);
762
+ return "already-settled";
763
+ }
764
+ if (gate.row.state === "retired") {
765
+ // Terminal idempotence is SAME-OP idempotence: a stranger cannot claim another
766
+ // operation's terminal as its own settlement.
767
+ if (gate.row.op?.opId !== args.opId)
768
+ throw new EpEnvelopeError("permission-denied", `the gate for uid ${args.lifecycleUid} was terminalized by operation ${gate.row.op?.opId ?? "<none>"}, not ${args.opId} (SPEC 13.1)`);
769
+ return "already-settled";
770
+ }
771
+ if (gate.row.state === "open") {
772
+ if (!won)
773
+ throw new EpEnvelopeError("internal", `the gate for uid ${args.lifecycleUid} is open but the head does not name it; an open gate without its active head is corruption (SPEC 13.1)`);
774
+ return "already-settled";
775
+ }
776
+ // frozen: only OUR op may advance it (reopen/retire are op-pinned and will refuse a stranger).
777
+ if (won) {
778
+ await reopenGate(reg, { lifecycleUid: args.lifecycleUid, revision: gate.revision, opId: args.opId });
779
+ return "completed";
780
+ }
781
+ await retireGate(reg, { lifecycleUid: args.lifecycleUid, revision: gate.revision, opId: args.opId });
782
+ return "terminalized";
783
+ }
784
+ // ---- the leader-served mapping reader (the currency seam) ------------------------------------
785
+ /**
786
+ * The LEADER-SERVED mapping read (§13.1: `mappingRevision` IS the returned store revision; the
787
+ * records bucket allows Direct Get for non-fencing reads, but an authority read of the head is
788
+ * leader-served `STREAM.MSG.GET`, never a follower get). Returns the mapping REGARDLESS of
789
+ * state — currency is the CALLER's rule, and the epoch seam below applies it. A DEL/PURGE
790
+ * marker refuses loudly.
791
+ */
792
+ export async function readLifecycleMappingLeader(rd, owner, actor) {
793
+ const { jsm, space } = readerInternals(rd);
794
+ const key = headKey(owner, actor);
795
+ let entry;
796
+ try {
797
+ entry = await readRecordLeader(jsm, space, key);
798
+ }
799
+ catch (e) {
800
+ if (e instanceof EpEnvelopeError)
801
+ throw e; // incl. the DEL/PURGE refusal
802
+ throw new EpEnvelopeError("unavailable", `the leader-served lifecycle-head read for "${owner}/${actor}" failed; an authority read fails closed, never open (SPEC 13.1): ${e?.message ?? String(e)}`);
803
+ }
804
+ if (entry === undefined)
805
+ return undefined;
806
+ return { mapping: parseMapping(enc.encode(JSON.stringify(entry.value)), key, owner, actor), revision: entry.revision };
807
+ }
808
+ /**
809
+ * The `readProcessEpoch` PRODUCTION SEAM the D11/D7 paths inject: the CURRENT incarnation's
810
+ * fenced epoch, leader-served — and current means `state: "active"` ONLY (§13.1, amended):
811
+ * `retiring` and `retired` alike yield `undefined`, so a superseded process's status write, a
812
+ * containment-phase mint, or a leaked session grant all fail the epoch fence. Bind it as
813
+ * `readProcessEpoch: () => lifecycleProcessEpochReader(reader, owner, actor)`.
814
+ */
815
+ export async function lifecycleProcessEpochReader(rd, owner, actor) {
816
+ const read = await readLifecycleMappingLeader(rd, owner, actor);
817
+ return read !== undefined && read.mapping.state === "active" ? read.mapping.processEpoch : undefined;
818
+ }
819
+ //# sourceMappingURL=lifecycle-registry.js.map