@cotal-ai/auth 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,14 +47,10 @@
47
47
  */
48
48
  import { jetstream, jetstreamManager } from "@nats-io/jetstream";
49
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";
50
+ import { EpEnvelopeError, recordsBucket as coreRecordsBucket, lifecycleHeadKey as headKey, parseLifecycleHead as parseMapping, headCandidate, gateObserve, gateCreateFrozen, gateFreeze, gateReopen, gateRetire, uidTryReserve, uidReserveFresh, uidReadReservation, headAdvanceEpochWithinTakeover, headSetCurrentRootCredential, headBeginRetirement, headCompleteRetirement, runActivationSaga, runActivationSagaAtUid, resumeActivationSaga, createRecordEntry, updateRecordEntry, readRecordLeader, epAuthBucket, workPoolContext, } from "@cotal-ai/core";
51
51
  import { assertScannerSpace } from "./ledger-scanner.js";
52
52
  import { assertRecordsScannerSpace } from "./records-scanner.js";
53
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
54
  const REGISTRIES = new WeakMap();
59
55
  const READERS = new WeakMap();
60
56
  function internals(reg) {
@@ -110,7 +106,10 @@ export function assertAuthorityStreamShape(cfg, bucket) {
110
106
  export async function openLifecycleRegistry(nc, space, scanner, recordsScanner) {
111
107
  const jsm = await jetstreamManager(nc);
112
108
  const kvm = new Kvm(nc);
113
- const recordsBucket = `cotal_records_${space}`;
109
+ // ONE encoder for the records-bucket name (core `recordsBucket`): the previous inline
110
+ // `cotal_records_${space}` skipped `token(space)` — identical for every legal space name, but
111
+ // a second encoder of a load-bearing name is exactly the drift class the shared grammar bans.
112
+ const recordsBucket = coreRecordsBucket(space);
114
113
  const authBucket = epAuthBucket(space);
115
114
  let recordsKv, authKv;
116
115
  let recordsCfg;
@@ -154,7 +153,10 @@ export async function openLifecycleRegistry(nc, space, scanner, recordsScanner)
154
153
  * records store refuses to serve authority reads (it could otherwise leader-read a MIRROR's
155
154
  * leader and call that the mapping). */
156
155
  export async function openLifecycleMappingReader(nc, space) {
157
- const recordsBucket = `cotal_records_${space}`;
156
+ // ONE encoder for the records-bucket name (core `recordsBucket`): the previous inline
157
+ // `cotal_records_${space}` skipped `token(space)` — identical for every legal space name, but
158
+ // a second encoder of a load-bearing name is exactly the drift class the shared grammar bans.
159
+ const recordsBucket = coreRecordsBucket(space);
158
160
  let jsm;
159
161
  let recordsCfg;
160
162
  try {
@@ -169,77 +171,20 @@ export async function openLifecycleMappingReader(nc, space) {
169
171
  READERS.set(rd, { space, jsm });
170
172
  return rd;
171
173
  }
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 };
174
+ /** The sealed registry's write transport for the shared §13.1 saga sequencing
175
+ * (@cotal-ai/core `lifecycle-saga.ts`): constructed ONLY from this module's sealed internals,
176
+ * so the WeakMap brand still gates every write; the transport carries no sequencing decisions
177
+ * of its own (the three-way split: adapters are TRANSPORT, never a second saga). */
178
+ function transportOf(reg) {
179
+ const { recordsKv, authKv } = internals(reg);
180
+ const entryOf = (e) => e === null ? undefined : { value: e.value, revision: e.revision, operation: e.operation };
181
+ return {
182
+ getRecord: async (key) => entryOf(await recordsKv.get(key)),
183
+ createRecord: (key, value) => createRecordEntry(recordsKv, key, value),
184
+ updateRecord: (key, value, rev) => updateRecordEntry(recordsKv, key, value, rev),
185
+ getAuth: async (key) => entryOf(await authKv.get(key)),
186
+ putAuth: (key, payload, rev) => authKv.put(key, payload, { previousSeq: rev }),
187
+ };
243
188
  }
244
189
  /** PACKAGE-INTERNAL accessor for the trusted auth path's sibling modules (the credential
245
190
  * ledger + issuance barrier, which are the SAME authority over the SAME stores). Deliberately
@@ -272,43 +217,14 @@ export function registryRecordsScanner(reg) {
272
217
  * through {@link advanceEpochWithinTakeover}). Same never-deleted discipline as every head
273
218
  * read; never re-exported from the package index. */
274
219
  export async function readLifecycleHeadForOperation(reg, owner, actor) {
275
- return readHeadCandidate(internals(reg).recordsKv, owner, actor);
220
+ return headCandidate(transportOf(reg), owner, actor);
276
221
  }
277
222
  /** 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. */
223
+ * module-internal for the credential ledger's barrier only) delegates to the shared §13.1
224
+ * sequencing (core `headAdvanceEpochWithinTakeover`, which also carries the C1 same-CAS
225
+ * root-stamp clear) over this registry's sealed transport. */
281
226
  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";
227
+ return headAdvanceEpochWithinTakeover(transportOf(reg), args);
312
228
  }
313
229
  /**
314
230
  * The issuance path's head CAS stamping the incarnation's ROOT credential (SPEC 13.1: the head's
@@ -324,36 +240,14 @@ export async function advanceEpochWithinTakeover(reg, args) {
324
240
  * only; a foreign head movement between the caller's read and this CAS loses fail-closed.
325
241
  */
326
242
  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);
243
+ return headSetCurrentRootCredential(transportOf(reg), args);
338
244
  }
339
245
  /** The retirement barrier's head CONTAINMENT CAS (SPEC 13.1: `active → retiring`, bound to the
340
246
  * retirement operation's durable intent — from this point every currency seam yields no current
341
247
  * mapping and no current epoch, and the alias is NOT replaceable). PACKAGE-INTERNAL for the
342
248
  * barrier only (no public retire seam exists); idempotent for the barrier's crash-resume. */
343
249
  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";
250
+ return headBeginRetirement(transportOf(reg), args);
357
251
  }
358
252
  /** The retirement barrier's TERMINAL head CAS (`retiring → retired`, op-pinned) — the barrier's
359
253
  * LAST step (SPEC 13.1: `retired` ASSERTS completed cleanup, which is what makes the alias
@@ -361,39 +255,14 @@ export async function beginHeadRetirementWithinBarrier(reg, args) {
361
255
  * `retired` is decided by the CALLER against the gate's terminal op (the retired head itself
362
256
  * carries no retirement stamp). PACKAGE-INTERNAL for the barrier only. */
363
257
  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";
258
+ return headCompleteRetirement(transportOf(reg), args);
375
259
  }
376
260
  /** PACKAGE-INTERNAL: read a UID reservation's audit `{ owner, actor }` (the minting authority
377
261
  * recorded it at {@link tryReserveUid}). The credential ledger uses it to BIND a mint's
378
262
  * `holderPrincipal` to the reserved identity, so a trusted caller cannot ledger a row that
379
263
  * names a foreign principal for the barrier to evict. A DEL/PURGE marker refuses loudly. */
380
264
  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 };
265
+ return uidReadReservation(transportOf(reg), lifecycleUid);
397
266
  }
398
267
  // ---- the space-global UID reservation (records store) ----------------------------------------
399
268
  /** Try to reserve ONE explicit candidate UID (test/provisioning-internal; production paths use
@@ -402,99 +271,23 @@ export async function readUidReservation(reg, lifecycleUid) {
402
271
  * never-reuse rule. NOT exported from the package index: an explicit candidate is only for
403
272
  * probes and migration tooling, never a caller-chosen identity. */
404
273
  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
- }
274
+ return uidTryReserve(transportOf(reg), lifecycleUid, audit);
416
275
  }
417
276
  /** Reserve a fresh lifecycle UID space-globally (§13.1): mint a CSPRNG candidate, win its
418
277
  * create-only reservation, and on a collision burn the candidate and draw another. At ≥128
419
278
  * bits a collision is effectively adversarial, so a handful of retries is a correctness
420
279
  * formality, not a capacity plan; exhausting them refuses loudly. */
421
280
  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;
281
+ return uidReserveFresh(transportOf(reg), audit);
476
282
  }
283
+ // ---- the issuance-gate CAS primitives (auth store, agent family `gate.<lifecycleUid>`) -------
284
+ // The gate row schema (`EpGateRow`), key builder, and boundary parser live in the shared §13.1
285
+ // STATE GRAMMAR in @cotal-ai/core (`lifecycle-state.ts`), imported above as
286
+ // `gateKey`/`parseGate`; the type is re-exported beside `LifecycleMapping`.
477
287
  /** Observe the gate (the candidate read feeding a revision-pinned CAS; the auth store is
478
288
  * leader-only by shape, `allow_direct=false`). A DEL/PURGE marker refuses loudly. */
479
289
  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
- }
290
+ return gateObserve(transportOf(reg), lifecycleUid);
498
291
  }
499
292
  /** Create the gate FROZEN under its operation's durable intent (create-only: conflicts on an
500
293
  * existing gate or a deletion marker). A gate is BORN only under an ACTIVATION intent (SPEC
@@ -503,37 +296,12 @@ async function putGate(authKv, lifecycleUid, row, expectedRevision) {
503
296
  * unreserved UID would mint outside the never-reuse fence. Born unmintable at generation 0:
504
297
  * no credential can be released until the operation's own reopen (§13.1 activation saga). */
505
298
  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 };
299
+ return gateCreateFrozen(transportOf(reg), args);
517
300
  }
518
301
  /** CAS the gate `open → frozen` carrying the freezing operation's durable intent, at the
519
302
  * observed revision. The bar of every barrier: a staged mint's own finalize CAS loses. */
520
303
  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 };
304
+ return gateFreeze(transportOf(reg), args);
537
305
  }
538
306
  /** CAS the gate `frozen → open` at the NEXT generation — op-pinned: only the freeze's own
539
307
  * operation (the same `opId`) reopens, as its barrier's final step; a stranger or a stale
@@ -541,19 +309,7 @@ export async function freezeGate(reg, args) {
541
309
  * belongs to activation, takeover, and a registration abort — NEVER retirement (a
542
310
  * retirement freeze never reopens; its only exit is the terminal). */
543
311
  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 };
312
+ return gateReopen(transportOf(reg), args);
557
313
  }
558
314
  /** CAS the gate `frozen → retired` (terminal; never reopened) — op-pinned like the reopen. The
559
315
  * activation saga uses it to terminalize a head-CAS loser's orphan gate; the retirement
@@ -562,24 +318,7 @@ export async function reopenGate(reg, args) {
562
318
  * reopening, never by the terminal. Idempotence at `retired` is SAME-OP idempotence: a
563
319
  * stranger's retry on a terminal gate refuses, it does not "succeed". */
564
320
  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 };
321
+ return gateRetire(transportOf(reg), args);
583
322
  }
584
323
  // ---- the activation saga (§13.1: reserve → gate frozen → head CAS → reopen LAST) -------------
585
324
  /**
@@ -596,48 +335,7 @@ export async function retireGate(reg, args) {
596
335
  * minting under the reopened gate before this becomes reachable.
597
336
  */
598
337
  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 };
338
+ return runActivationSaga(transportOf(reg), args);
641
339
  }
642
340
  /**
643
341
  * Activate the alias `(owner, actor)` AT THE CALLER'S uid — the production ISSUANCE activation
@@ -655,90 +353,7 @@ export async function activateLifecycle(reg, args) {
655
353
  * held by a FOREIGN alias, a foreign-operation freeze, and a terminally retired gate.
656
354
  */
657
355
  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
- }
356
+ return runActivationSagaAtUid(transportOf(reg), args);
742
357
  }
743
358
  /**
744
359
  * Resume a crashed activation saga from its durable coordinates (`{alias, lifecycleUid, opId}`,
@@ -751,35 +366,7 @@ export async function activateLifecycleAtUid(reg, args) {
751
366
  * what it did.
752
367
  */
753
368
  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";
369
+ return resumeActivationSaga(transportOf(reg), args);
783
370
  }
784
371
  // ---- the leader-served mapping reader (the currency seam) ------------------------------------
785
372
  /**