@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,346 @@
1
+ /**
2
+ * The auth service's SELF-MINTED data-account connections (R1). The service already holds the
3
+ * data account's signing seed (minting scoped users IS its function), so its own infrastructure
4
+ * access is signed from that seed directly — a ledgered `cred.`/`epcred.` row would create no
5
+ * enforcement point (connect never re-reads the reader's own row) and would be enumerated by the
6
+ * lifecycle barriers as if it were an agent's. The revoke story is the D5 class: stop the service
7
+ * or rotate the data-account signing seed. A genuinely LEDGERED infra-credential family is the
8
+ * deferred infra-mint work; migrate onto it there, do not invent it here.
9
+ *
10
+ * Two connections, least-privilege by construction:
11
+ * - the CONNECT READER (`cotal:auth-reader:<space>`): read-only over both authority stores
12
+ * ({@link authConnectReaderGrants}), supervised — shape-proved on EVERY (re)bind, unproved
13
+ * while disconnected, so a reader that cannot currently prove its stores DENIES every connect.
14
+ * - the MINT WRITER (`cotal:auth-mint:<space>`): the exchange-time issuance executor
15
+ * ({@link authorityWriterGrants}) — store ensure + the activation saga + the root-credential
16
+ * mint protocol. Its unfenced reads only feed revision-pinned CASes (a stale read LOSES) or a
17
+ * bearer stamp the leader-served connect arm re-checks, so bind-time proof suffices.
18
+ *
19
+ * RENEWAL IS A FAIL-CLOSED BOUNDARY: each connection authenticates with a SHORT-exp user JWT over
20
+ * a stable nkey, re-minted in-process at half-life; the broker disconnects the connection at JWT
21
+ * expiry and the reconnect presents the freshest mint. A renewal-mint failure fires
22
+ * {@link AuthorityClientOpts.onRenewalFailure} IMMEDIATELY — the supervised reader downs itself
23
+ * on it (unproved + connection closed), so connects DENY from the moment the renewal fails, not
24
+ * only when the old credential eventually expires. A downed reader never comes back without a
25
+ * service restart; there is no file-only fallback.
26
+ */
27
+ import { connect, jwtAuthenticator } from "@nats-io/transport-node";
28
+ import { encodeUser } from "@nats-io/jwt";
29
+ import { fromPublic, fromSeed } from "@nats-io/nkeys";
30
+ import { EpEnvelopeError, assertInboxConnId, endpointToken, epAuthBucket, epfStreamName, newIdentity, recordsBucket, retirementFrontierStreams, spacePrefix, assertPoolToken, principalTags, principalKey } from "@cotal-ai/core";
31
+ import { authConnectReaderGrants, openConnectReader } from "./connect-reader.js";
32
+ /** Self-minted infra-credential TTL (fact-3 pin: SHORT expiry + in-process renewal, a bounded
33
+ * in-memory credential that closes with the service — never an unbounded persisted one). */
34
+ export const AUTHORITY_CLIENT_TTL_SEC = 15 * 60;
35
+ /**
36
+ * The MINT WRITER's scoped credential grant (SPEC 13.9): the exchange-time issuance executor.
37
+ * Exactly the store-ensure + activation-saga + root-mint surface:
38
+ *
39
+ * - `$JS.API.STREAM.CREATE` on both authority KV streams and `STREAM.UPDATE` on records (the
40
+ * boot-time `ensureAuthorityStores`), `STREAM.INFO` on both (ensure verify + §13.12 bind
41
+ * proof), `STREAM.MSG.GET` on both (auth-store KV reads are leader-only by shape; fenced
42
+ * record reads), and records `DIRECT.GET` (the records bucket is Direct-capable and its KV
43
+ * reads here only feed revision-pinned CASes, which a stale read LOSES — fail-closed).
44
+ * - `$KV` writes on EXACTLY the issuance keys: auth `gate.` / `cred.` / `bysrc.` (the gate CAS
45
+ * fence and the mint's rows) and records `lifecycle.` / `uid.` (the alias head and the
46
+ * space-global uid reservation). No `srcgate.`/`stage.`/`session.` writes: root issuance
47
+ * presents no handles and runs no takeover; barriers are NOT this credential's job.
48
+ *
49
+ * Named residuals (D32 class, not pretend-confined): `STREAM.MSG.GET` is body-selected, so this
50
+ * is a stream-wide METADATA read of both stores; the `$KV` prefixes span every uid/alias (broker
51
+ * ACLs cannot scope "only the row you are minting"), so a compromised writer can forge issuance
52
+ * rows — it is the ISSUANCE AUTHORITY, that authority is exactly what it holds; and every raw
53
+ * MSG.GET/DIRECT.GET profile carries the caller-selected-reply injection class.
54
+ */
55
+ export function authorityWriterGrants(space, connId) {
56
+ const auth = `KV_${epAuthBucket(space)}`;
57
+ const records = `KV_${recordsBucket(space)}`;
58
+ // Same connId hygiene as authConnectReaderGrants: the only untrusted subject-forming input goes
59
+ // through the shared inbox grammar so it can never widen the scoped inbox.
60
+ const inbox = assertInboxConnId(connId);
61
+ return {
62
+ publish: [
63
+ "$JS.API.INFO",
64
+ `$JS.API.STREAM.CREATE.${auth}`,
65
+ `$JS.API.STREAM.CREATE.${records}`,
66
+ `$JS.API.STREAM.UPDATE.${records}`,
67
+ `$JS.API.STREAM.INFO.${auth}`,
68
+ `$JS.API.STREAM.INFO.${records}`,
69
+ `$JS.API.STREAM.MSG.GET.${auth}`,
70
+ `$JS.API.STREAM.MSG.GET.${records}`,
71
+ `$JS.API.DIRECT.GET.${records}`,
72
+ `$JS.API.DIRECT.GET.${records}.>`,
73
+ `$KV.${epAuthBucket(space)}.gate.>`,
74
+ `$KV.${epAuthBucket(space)}.cred.>`,
75
+ `$KV.${epAuthBucket(space)}.bysrc.>`,
76
+ `$KV.${recordsBucket(space)}.lifecycle.>`,
77
+ `$KV.${recordsBucket(space)}.uid.>`,
78
+ ],
79
+ subscribe: [`_INBOX_${inbox}.>`],
80
+ };
81
+ }
82
+ /**
83
+ * The BARRIER EXECUTOR's scoped credential grant (SPEC 13.9): the lifecycle-barrier surface —
84
+ * exactly what the takeover barrier (and its retirement sibling) reaches through the sealed
85
+ * registry, on its OWN connection so the latency-sensitive mint writer never carries barrier
86
+ * authority (its own doc pins "barriers are NOT this credential's job"):
87
+ *
88
+ * - `STREAM.INFO` on both stores (the registry's §13.12 bind proof), `STREAM.MSG.GET` on both
89
+ * (leader-served gate/cred/intent and head reads), and records `DIRECT.GET` (reads that only
90
+ * feed revision-pinned CASes — the mint writer's own discipline).
91
+ * - NO auth-stream `CONSUMER.CREATE` at all (46e778f re-verify): a consumer-create BODY is not
92
+ * subject-ACL confinable, so any create grant admits a durable PUSH exporter surviving revoke
93
+ * (nats-server#8274). This profile therefore does NOT enumerate: the barrier's
94
+ * family/intent/lineage LastPerSubject scans run on the SEALED auth-ledger scanner, a SEPARATE
95
+ * dedicated credential that holds the only auth-stream `CONSUMER.CREATE` and never escapes the
96
+ * process (`ledger-scanner.ts`). The `STREAM.MSG.GET` here is only this profile's leader-served
97
+ * POINT reads (a specific gate/cred/intent/head `last_by_subj`), never a family scan. No consumer
98
+ * authority on either stream.
99
+ * - `$KV` writes on EXACTLY the barrier keys: auth `gate.` (the freeze/reopen CAS), `cred.`
100
+ * (the family revokes), `stage.` (the durable operation intent), and records `lifecycle.`
101
+ * (the containment/epoch head CASes). No `bysrc.`/`uid.` (a barrier never mints), no
102
+ * `srcgate.`/`session.` (handle revocation and session reconcile are injected seams, not
103
+ * this credential's job).
104
+ *
105
+ * Named residuals (D32 class, not pretend-confined): the stream-wide body-selected `MSG.GET`
106
+ * metadata read (a ONE-SHOT read to the requester's own inbox while the credential is live — not
107
+ * a durable/push exporter) and the caller-selected-reply injection class ride this profile like
108
+ * its siblings; and the `$KV` prefixes span every uid (broker ACLs cannot scope "only the family
109
+ * you are containing") — the executor IS the barrier authority, and that authority is exactly
110
+ * what it holds.
111
+ */
112
+ export function authorityBarrierGrants(space, connId) {
113
+ const auth = `KV_${epAuthBucket(space)}`;
114
+ const records = `KV_${recordsBucket(space)}`;
115
+ // Same connId hygiene as its siblings: the only untrusted subject-forming input goes through
116
+ // the shared inbox grammar so it can never widen the scoped inbox.
117
+ const inbox = assertInboxConnId(connId);
118
+ // The §13.1 frontier step reads the last sequence of every stream in the retirement's
119
+ // `frontierStreams` (a subset of the CLOSED {@link retirementFrontierStreams} set). Grant
120
+ // STREAM.INFO for exactly that set, from the same source the intent is validated against, so the
121
+ // frontier authority and the frontier-writable set never drift (a real callout broker denies an
122
+ // ungranted INFO and the resume wedges forever). records INFO is already listed; the frontier set
123
+ // adds EPF/EPW/EPE (records dedups below).
124
+ const frontierInfo = retirementFrontierStreams(space).map((s) => `$JS.API.STREAM.INFO.${s}`);
125
+ return {
126
+ publish: [
127
+ "$JS.API.INFO",
128
+ `$JS.API.STREAM.INFO.${auth}`,
129
+ `$JS.API.STREAM.INFO.${records}`,
130
+ ...frontierInfo.filter((r) => r !== `$JS.API.STREAM.INFO.${records}`),
131
+ `$JS.API.STREAM.MSG.GET.${auth}`,
132
+ `$JS.API.STREAM.MSG.GET.${records}`,
133
+ `$JS.API.DIRECT.GET.${records}`,
134
+ `$JS.API.DIRECT.GET.${records}.>`,
135
+ // NO auth-stream `CONSUMER.CREATE` (security/fact/distsys/freelance, 46e778f re-verify): the
136
+ // extended `CONSUMER.CREATE.<auth>.<name>.<filter>` API pins name+filter on the SUBJECT but
137
+ // the request BODY is not subject-ACL confinable — a holder can set `durable_name`
138
+ // (== the subject name token) + PUSH `deliver_subject` (nats-server#8274) and leave a DURABLE
139
+ // exporter of every current/future gate/cred/stage row that SURVIVES this connection and JWT
140
+ // revoke. Subject-token pinning cannot stop it; only holding no create can. The barrier's
141
+ // family/intent/lineage enumerations therefore run on the SEALED auth-ledger scanner — a
142
+ // SEPARATE dedicated credential that holds the only auth-stream `CONSUMER.CREATE` and never
143
+ // escapes the process (ledger-scanner.ts) — NOT on this profile. The `STREAM.MSG.GET` granted
144
+ // above is only this profile's leader-served POINT reads (a specific gate/cred/intent/head
145
+ // `last_by_subj`). Remaining D32 residual (unchanged, accepted): that stream-wide `MSG.GET`
146
+ // while the credential is LIVE is a one-shot read to the requester's own inbox carrying the
147
+ // caller-selected-reply injection class — never a durable/push exporter.
148
+ `$KV.${epAuthBucket(space)}.gate.>`,
149
+ `$KV.${epAuthBucket(space)}.cred.>`,
150
+ // ONE token: the barrier's own durable operation intents `stage.<opId>` — never `stage.>`,
151
+ // which would also reach the session ledger's `stage.session.<sid>.<c|s>` release pins (a
152
+ // family the SESSION writer owns). The documented-future takeover/registration successor
153
+ // artifacts (`stage.<opId>.…`) have no writer yet; compose `stage.*.>` in the slice that
154
+ // introduces them, so forgetting fails loud here instead of widening silently now.
155
+ `$KV.${epAuthBucket(space)}.stage.*`,
156
+ // The PLANE CLAIM (#29 HIGH 3, SPEC 13.13): the ONE exact never-deleted key the plane-open
157
+ // create/CAS protocol writes (`plane` — no `.>`/`.*` widen; the row binds the two sealed
158
+ // scanner connection tuples and its held/released state). Reads ride the leader-served
159
+ // `STREAM.MSG.GET` above. Only this profile writes it; the claim IS the cross-process
160
+ // single-plane exclusion, so a wider row here would hand takeover authority to whatever
161
+ // else composes onto this profile.
162
+ `$KV.${epAuthBucket(space)}.plane`,
163
+ `$KV.${recordsBucket(space)}.lifecycle.>`,
164
+ // The retirement FRONTIER record `frontier.<lifecycleUid>` (§13.7: create-only, never
165
+ // deleted, one key per retired lifecycle, recorded once under its own operation's opId
166
+ // BEFORE the gate/head terminals): the barrier is its one writer, and the exact-arity
167
+ // `frontier.*` row is that authority (#29 piece 4; a barrier without it is broker-denied
168
+ // at step 6 on a real callout broker, masked by super-user smoke brokers).
169
+ `$KV.${recordsBucket(space)}.frontier.*`,
170
+ ],
171
+ subscribe: [`_INBOX_${inbox}.>`],
172
+ };
173
+ }
174
+ /** The RETIREMENT SETTLEMENT EXECUTOR's op-bounded rows (SPEC 13.9 "Retirement settlement",
175
+ * the ceee1a1 authority split): COMPOSED onto {@link authorityBarrierGrants} for ONE retirement
176
+ * operation over its EFFECTIVE INVENTORY endpoint/pools (the target's accepted obligation routes
177
+ * UNIONED with the durable intent's trusted additive hints) — never a standing grant, never
178
+ * minted without a live intent. Per effective-inventory pool: the lease-record CAS write
179
+ * (`lease.<endpoint>.<pool>.…` — create for the worker-less expiry sentinel, revision-pinned
180
+ * update otherwise) and the lease-derived `wrk` terminal create-only publish
181
+ * (`epf.<endpoint>.wrk.<pool>.>`, first terminal wins). Plus the leader-served EPF fencing
182
+ * reads (`STREAM.MSG.GET.EPF_<space>`: the acceptance-decision re-derivation and the
183
+ * terminal-observe/CAS-loser reads); the records-side fencing/CAS reads ride the barrier
184
+ * profile this composes onto. The `epw.>` ENQUEUE row is deliberately absent: the executor's
185
+ * reconcile is reachable only for items at/past their own `workExpiry` (the settlement seam
186
+ * refuses `expired` before the horizon), which structurally never takes the re-enqueue repair
187
+ * branch — if a code change ever reached it, the broker denies and the barrier fails loud.
188
+ *
189
+ * D32 residuals, EXPLICIT (the relocated forge the bounded cleaner does NOT carry): KV subject
190
+ * permissions cannot distinguish CAS from overwrite or DEL/PURGE markers, and the `wrk`
191
+ * publish is payload-blind, so a compromised executor can forge a lease settlement or work
192
+ * terminal WITHIN its EFFECTIVE-INVENTORY pools (never beyond them); the space-wide EPF
193
+ * `STREAM.MSG.GET` exposure and the caller-selected-reply injection ride it like every
194
+ * API-holding profile. Op-bounded, effective-inventory-confined, revoked with the operation. */
195
+ export function barrierExecutorSettlementGrants(space, endpoint, pools) {
196
+ if (!Array.isArray(pools) || pools.length === 0)
197
+ throw new EpEnvelopeError("failed-precondition", "an executor settlement grant lists at least one exact pool (SPEC 13.9: the op intent enumerates them; a poolless settlement authority is none)");
198
+ const e = endpointToken(endpoint);
199
+ const publish = [];
200
+ for (const pool of pools) {
201
+ const p = assertPoolToken(pool);
202
+ publish.push(`${spacePrefix(space)}.epf.${e}.wrk.${p}.>`, `$KV.${recordsBucket(space)}.lease.${e}.${p}.>`);
203
+ }
204
+ publish.push(`$JS.API.STREAM.MSG.GET.${epfStreamName(space)}`);
205
+ return { publish };
206
+ }
207
+ /**
208
+ * Open one self-minted data-account connection: a stable user nkey, a short-exp user JWT signed
209
+ * by the data account's signing seed, re-minted in-process at half-life. The authenticator
210
+ * presents the FRESHEST mint on every (re)connect; see the module header for why a failed
211
+ * renewal fail-closes instead of retrying on a lapsed credential.
212
+ */
213
+ export async function openAuthorityClient(opts) {
214
+ const identity = newIdentity();
215
+ const grants = opts.grants(identity.id);
216
+ const signer = fromSeed(new TextEncoder().encode(opts.dataAccount.signingSeed));
217
+ const mint = async () => encodeUser(opts.label, fromPublic(identity.id), fromPublic(opts.dataAccount.pub), { pub: { allow: grants.publish }, sub: { allow: grants.subscribe },
218
+ ...(opts.principal ? { tags: principalTags(opts.principal.owner, opts.principal.actor) } : {}) },
219
+ // planeCandidate: NON-EXPIRING by design (see the option's doc — the named fact-3 residual);
220
+ // every other authority connection keeps the short-exp + half-life-renewal boundary.
221
+ opts.planeCandidate ? { signer } : { signer, exp: Math.floor(Date.now() / 1000) + AUTHORITY_CLIENT_TTL_SEC });
222
+ let currentJwt = await mint();
223
+ const renew = async () => {
224
+ if (opts.probeRenewal?.fail)
225
+ throw new Error("probe-forced renewal failure");
226
+ return mint();
227
+ };
228
+ // No renewal timer for a planeCandidate: nothing expires, and a fresh mint could only be
229
+ // presented on the reconnect the non-reconnecting shape forbids.
230
+ const renewal = opts.planeCandidate ? undefined : setInterval(() => {
231
+ void renew().then((jwt) => { currentJwt = jwt; }, (e) => {
232
+ opts.log(`${opts.label}: credential renewal FAILED (${e?.message ?? String(e)}) - failing closed NOW (fail-closed renewal boundary)`);
233
+ opts.onRenewalFailure?.();
234
+ });
235
+ }, opts.probeRenewal?.intervalMs ?? (AUTHORITY_CLIENT_TTL_SEC / 2) * 1000);
236
+ let nc;
237
+ try {
238
+ nc = await connect({
239
+ servers: opts.server,
240
+ name: opts.label,
241
+ authenticator: jwtAuthenticator(() => currentJwt, new TextEncoder().encode(identity.seed)),
242
+ inboxPrefix: `_INBOX_${identity.id}`,
243
+ // planeCandidate and the per-call repair connections (#4) never auto-reconnect; every standing
244
+ // authority connection reconnects infinitely so a broker blip never downs it silently.
245
+ ...(opts.planeCandidate || opts.noReconnect ? { reconnect: false, maxReconnectAttempts: 0 } : { maxReconnectAttempts: -1 }),
246
+ });
247
+ }
248
+ catch (e) {
249
+ if (renewal !== undefined)
250
+ clearInterval(renewal);
251
+ throw e;
252
+ }
253
+ let tuple;
254
+ let gone;
255
+ if (opts.planeCandidate) {
256
+ // The claim row pins this exact identity; a connect whose INFO cannot supply it is unusable
257
+ // as an ownership-bearing connection — fail loud, never claim over a hole.
258
+ const info = nc.info;
259
+ if (typeof info?.server_id !== "string" || info.server_id.length === 0 ||
260
+ typeof info.client_id !== "number" || !Number.isSafeInteger(info.client_id) || info.client_id <= 0) {
261
+ await nc.close().catch(() => { });
262
+ throw new EpEnvelopeError("unavailable", `${opts.label}: the broker INFO carries no usable (server_id, client_id) for the plane claim (got server_id=${JSON.stringify(info?.server_id)}, client_id=${JSON.stringify(info?.client_id)}); an ownership-bearing scanner connection must be claim-pinnable (SPEC 13.13)`);
263
+ }
264
+ tuple = { serverId: info.server_id, cid: info.client_id, userNkey: identity.id };
265
+ gone = nc.closed().then(() => undefined, () => undefined);
266
+ }
267
+ return {
268
+ nc,
269
+ connId: identity.id,
270
+ ...(opts.principal ? { principal: principalKey(opts.principal.owner, opts.principal.actor).key } : {}),
271
+ ...(tuple ? { tuple } : {}),
272
+ ...(gone ? { gone } : {}),
273
+ close: async () => {
274
+ if (renewal !== undefined)
275
+ clearInterval(renewal);
276
+ await nc.close().catch(() => { });
277
+ },
278
+ };
279
+ }
280
+ /**
281
+ * Open the supervised connect reader: the self-minted reader connection plus the §13.12 bind
282
+ * discipline ACROSS RECONNECTS. The shape proof re-runs on EVERY (re)bind, not just startup — a
283
+ * dropped reader reconnecting could land on a mirror/follower, so it is UNPROVED from the moment
284
+ * the connection drops until {@link openConnectReader} passes again, and `current()` refuses in
285
+ * between. The initial proof is awaited: this constructor resolving IS the readiness signal.
286
+ */
287
+ export async function openSupervisedConnectReader(opts) {
288
+ const label = `cotal:auth-reader:${opts.space}`;
289
+ let reader;
290
+ let closed = false;
291
+ let client;
292
+ client = await openAuthorityClient({
293
+ ...opts,
294
+ label,
295
+ grants: (connId) => authConnectReaderGrants(opts.space, connId),
296
+ // Renewal failure is a fail-closed boundary NOW: down the reader the instant a remint
297
+ // rejects, so connects DENY immediately instead of riding the old credential to its expiry.
298
+ onRenewalFailure: () => {
299
+ reader = undefined;
300
+ opts.log(`${label}: credential renewal failed - reader downed NOW, connects deny until the service restarts (fail-closed)`);
301
+ if (!closed)
302
+ void client?.close();
303
+ },
304
+ });
305
+ const c = client;
306
+ try {
307
+ reader = await openConnectReader(c.nc, opts.space);
308
+ }
309
+ catch (e) {
310
+ await c.close();
311
+ throw e;
312
+ }
313
+ void (async () => {
314
+ for await (const s of c.nc.status()) {
315
+ if (closed)
316
+ break;
317
+ if (s.type === "disconnect" || s.type === "error") {
318
+ reader = undefined;
319
+ opts.log(`${label}: connection lost (${s.type}) - connects DENY until the rebind shape proof passes`);
320
+ }
321
+ else if (s.type === "reconnect") {
322
+ try {
323
+ reader = await openConnectReader(c.nc, opts.space);
324
+ opts.log(`${label}: rebound and shape-proved; connect reads resume`);
325
+ }
326
+ catch (e) {
327
+ reader = undefined;
328
+ opts.log(`${label}: REBIND SHAPE PROOF FAILED (${e?.message ?? String(e)}) - connects stay denied`);
329
+ }
330
+ }
331
+ }
332
+ })();
333
+ return {
334
+ current: () => {
335
+ if (reader === undefined)
336
+ throw new EpEnvelopeError("unavailable", "the connect-credential reader is not currently bound + shape-proved; a connect is never authorized without the live credential row (deny-new is fail-closed, SPEC 13.1/13.12)");
337
+ return reader;
338
+ },
339
+ close: async () => {
340
+ closed = true;
341
+ reader = undefined; // a closed reader refuses immediately, not on the next status event
342
+ await c.close();
343
+ },
344
+ };
345
+ }
346
+ //# sourceMappingURL=authority-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authority-client.js","sourceRoot":"","sources":["../src/authority-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { EvictPrincipal } from "./credential-ledger.js";
2
+ /**
3
+ * Build the barrier executor's `evictPrincipal` capability over the delivery daemon's
4
+ * `ctl.delivery-admin` rail. Per-call connection (an eviction is a rare, heavyweight barrier
5
+ * step; a standing privileged connection would be a wider surface holding nothing).
6
+ */
7
+ export declare function makeDeliveryAdminEvictor(opts: {
8
+ space: string;
9
+ server: string;
10
+ dataAccount: {
11
+ pub: string;
12
+ signingSeed: string;
13
+ };
14
+ log: (line: string) => void;
15
+ }): EvictPrincipal;
16
+ //# sourceMappingURL=barrier-evict.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"barrier-evict.d.ts","sourceRoot":"","sources":["../src/barrier-evict.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAO7D;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B,GAAG,cAAc,CAkEjB"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The auth service's VERIFIED-EVICTION seam (SPEC 13.1/13.9). The lifecycle barriers require
3
+ * `verifiedGone === true` before any epoch advance or head terminal, and the `$SYS` scan → KICK →
4
+ * verify capability lives with the DELIVERY DAEMON (co-located with the broker) — NEVER in this
5
+ * process: the auth service holds the data-account signing seed, and the D5 rail-split exists
6
+ * precisely so no seed-holding process also holds broker-admin `$SYS` material. This module
7
+ * reaches the daemon over the wire on the privileged `ctl.delivery-admin` control rail (the
8
+ * exact mechanism `cotal actor revoke` uses), with a caller credential self-minted per call from
9
+ * the data-account seed the service already holds.
10
+ *
11
+ * FAIL-CLOSED MAPPING: every failure — no daemon (NoResponders/timeout), a refusal, a garbled
12
+ * reply, or a reply describing a DIFFERENT principal — returns `verifiedGone:false` with an
13
+ * honest note. The calling barrier then throws and its gate stays frozen/resumable; this seam
14
+ * never fabricates success and never throws raw (the barrier owns the canonical error copy).
15
+ *
16
+ * Named residual (panel-reviewable): the caller credential rides the SUPERVISOR profile (the
17
+ * `cotal actor revoke` precedent) — only that profile holds the `ctl.delivery-admin` publish
18
+ * grant. The service holds the account signing seed, so this confers no authority it does not
19
+ * already have; a dedicated narrow delivery-admin infra profile is the cleaner target once the
20
+ * ledgered infra-mint family (#30) lands — migrate there, do not invent it here.
21
+ */
22
+ import { CotalEndpoint, mintCreds, newIdentity } from "@cotal-ai/core";
23
+ /** The per-eviction credential's TTL (Security H3): one delivery-admin call runs in a 15s request
24
+ * budget; 60s covers connect + the call + teardown with margin, and bounds the blast radius of a
25
+ * copied supervisor credential to a minute rather than the profile's default 24h. */
26
+ const EVICTOR_CRED_TTL_SECONDS = 60;
27
+ /**
28
+ * Build the barrier executor's `evictPrincipal` capability over the delivery daemon's
29
+ * `ctl.delivery-admin` rail. Per-call connection (an eviction is a rare, heavyweight barrier
30
+ * step; a standing privileged connection would be a wider surface holding nothing).
31
+ */
32
+ export function makeDeliveryAdminEvictor(opts) {
33
+ // The stripped mint view (core's stripSpaceAuth shape): mintCreds reads ONLY space +
34
+ // account.pub + account.signingSeed; the operator/system material is genuinely absent here.
35
+ const auth = {
36
+ space: opts.space,
37
+ operator: { seed: "", jwt: "" },
38
+ account: { pub: opts.dataAccount.pub, seed: "", jwt: "", signingSeed: opts.dataAccount.signingSeed, signingPub: "" },
39
+ sys: { pub: "", jwt: "" },
40
+ };
41
+ const failClosed = (principal, note) => {
42
+ opts.log(`auth-barrier-evict: ${principal}: ${note}`);
43
+ return { principal, kicked: 0, remaining: 0, verifiedGone: false, scanComplete: false, note };
44
+ };
45
+ return async (principal) => {
46
+ const id = newIdentity();
47
+ let ep;
48
+ try {
49
+ // Security H3: this is a per-eviction credential for ONE ~15s delivery-admin call, not a
50
+ // standing connection. The `supervisor` profile is default standing-renewable (24h); mint it
51
+ // with a tight TTL so a COPIED credential expires almost immediately instead of carrying a
52
+ // day of full manager-control-tier + lease/presence authority. (The dedicated narrow
53
+ // delivery-admin infra profile is the #30 target; until then, bound the lifetime here.)
54
+ const creds = await mintCreds(auth, id, "supervisor", { expiresInSeconds: EVICTOR_CRED_TTL_SECONDS });
55
+ ep = new CotalEndpoint({
56
+ space: opts.space,
57
+ servers: opts.server,
58
+ creds,
59
+ card: { id: id.id, name: "auth-barrier-evict", kind: "endpoint" },
60
+ channels: [],
61
+ consume: false,
62
+ watchChannels: false,
63
+ watchPresence: false,
64
+ registerPresence: false,
65
+ });
66
+ ep.on("error", () => { });
67
+ await ep.start();
68
+ const r = await ep.requestDeliveryAdmin("evictPrincipal", { principal }, 15_000);
69
+ if (!r.ok)
70
+ return failClosed(principal, `the delivery daemon refused the eviction: ${r.error ?? "(no error copy)"}`);
71
+ const d = r.data;
72
+ if (d === undefined || d === null || d.principal !== principal ||
73
+ typeof d.kicked !== "number" || !Number.isSafeInteger(d.kicked) || d.kicked < 0 ||
74
+ typeof d.remaining !== "number" || !Number.isSafeInteger(d.remaining) || d.remaining < 0 ||
75
+ typeof d.verifiedGone !== "boolean" || typeof d.scanComplete !== "boolean")
76
+ return failClosed(principal, `the delivery daemon returned a garbled or foreign eviction result (${JSON.stringify(r.data ?? null)}); a result that does not verifiably describe this principal never authorizes`);
77
+ // An internally CONTRADICTORY success never authorizes: the barriers gate on
78
+ // `verifiedGone === true` alone, so a tuple claiming verified-gone while admitting an
79
+ // incomplete scan or surviving connections would launder a partial answer into an epoch
80
+ // advance / head terminal. Verified-gone is conclusive only as (scan complete, none remain).
81
+ if (d.verifiedGone === true && (d.scanComplete !== true || d.remaining !== 0))
82
+ return failClosed(principal, `the delivery daemon claimed verifiedGone with scanComplete=${String(d.scanComplete)} and remaining=${d.remaining}; an internally contradictory eviction result never authorizes`);
83
+ return {
84
+ principal,
85
+ kicked: d.kicked,
86
+ remaining: d.remaining,
87
+ verifiedGone: d.verifiedGone,
88
+ scanComplete: d.scanComplete,
89
+ ...(typeof d.note === "string" ? { note: d.note } : {}),
90
+ };
91
+ }
92
+ catch (e) {
93
+ return failClosed(principal, `the delivery-admin rail is unreachable (${e instanceof Error ? e.message : String(e)}); eviction is UNKNOWN and the barrier fails closed`);
94
+ }
95
+ finally {
96
+ await ep?.stop().catch(() => { });
97
+ }
98
+ };
99
+ }
100
+ //# sourceMappingURL=barrier-evict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"barrier-evict.js","sourceRoot":"","sources":["../src/barrier-evict.ts"],"names":[],"mappings":"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"}
package/dist/commands.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { readFileSync, writeFileSync } from "node:fs";
9
9
  import { CotalEndpoint, mintCreds, newIdentity, registry } from "@cotal-ai/core";
10
- import { authDir, CLI_USER_ACTOR, findCotalRoot, homeCotalDir, loadMeshes, loadSpaceAuth, resolveSpace, userAuthStateDir } from "@cotal-ai/workspace";
10
+ import { authDir, CLI_USER_ACTOR, findCotalRoot, homeCotalDir, loadMeshes, loadSpaceAuth, resolveSpace, userAuthStateDir, workspaceSecretStore } from "@cotal-ai/workspace";
11
11
  import { deleteIdpSession, establishIdpSession, loadIdpSession, normalizeIdpUrl, revokeIdpSession, } from "./login.js";
12
12
  import { deriveOwnerForIdpSubject } from "./derive.js";
13
13
  import { findManagedActor, grantActor, loadActorLedger, revokeActor } from "./ledger.js";
@@ -86,11 +86,14 @@ async function runLogout(args) {
86
86
  console.log(`Logged out of ${idp} - server-side session revoked, local cache cleared.`);
87
87
  });
88
88
  }
89
- /** The space-scoped provider state dir the `actor` commands operate on (`userAuthStateDir` the
90
- * multi-space-ready layout; nothing user-auth lives flat in `.cotal/auth/`). */
91
- function actorStateDir(space) {
89
+ /** The space-scoped provider state the `actor` commands operate on: the ledger dir
90
+ * (`userAuthStateDir` — the multi-space-ready layout; nothing user-auth lives flat in
91
+ * `.cotal/auth/`) plus the local secret store (these are workstation commands — the ledger
92
+ * machine — so the workspace FS composition IS the correct one, not a fallback). */
93
+ function actorState(space) {
92
94
  const s = space ?? resolveSpace(process.cwd());
93
- return { dir: userAuthStateDir(findCotalRoot(), s), space: s };
95
+ const root = findCotalRoot();
96
+ return { dir: userAuthStateDir(root, s), space: s, store: workspaceSecretStore(root) };
94
97
  }
95
98
  /** Resolve the operator's target (owner) for `actor grant`/`revoke`: an explicit derived `--owner`
96
99
  * token, or `--sub` (the IdP subject `cotal login` prints) derived through the SAME frozen encoding
@@ -98,15 +101,15 @@ function actorStateDir(space) {
98
101
  * IS the machine that ran `cotal up --user-auth`, where the authoritative ledger lives. Without
99
102
  * them a grant would write an INERT LOCAL row and print a misleading success while the mesh's real
100
103
  * ledger is untouched (an off-machine `--owner` cannot mutate authority), so we refuse instead. */
101
- function resolveGrantOwner(dir, values) {
104
+ async function resolveGrantOwner(st, values) {
102
105
  if (values.owner && values.sub)
103
106
  throw new Error("pass --owner OR --sub, not both");
104
107
  if (!values.owner && !values.sub)
105
108
  throw new Error("say who: --sub <IdP subject> (shown by `cotal login`) or --owner <u_…>");
106
- const secret = loadOwnerSecret(dir);
107
- const idp = loadPinnedIdp(dir);
109
+ const secret = await loadOwnerSecret(st.store, st.space);
110
+ const idp = loadPinnedIdp(st.dir);
108
111
  if (!secret || !idp)
109
- throw new Error(`user auth is not enabled for this space here (no owner secret/IdP pin under ${dir}). The actor ledger lives on the machine that ran \`cotal up --user-auth --idp <url>\`; run the \`actor\` commands there`);
112
+ throw new Error(`user auth is not enabled for this space here (no owner secret/IdP pin for "${st.space}"). The actor ledger lives on the machine that ran \`cotal up --user-auth --idp <url>\`; run the \`actor\` commands there`);
110
113
  if (values.owner)
111
114
  return values.owner;
112
115
  return deriveOwnerForIdpSubject(secret, idp.issuer, values.sub);
@@ -160,7 +163,8 @@ async function evictRevokedPrincipal(space, principal) {
160
163
  async function runActor(args) {
161
164
  const [sub, actor] = args.positionals;
162
165
  const values = args.values;
163
- const { dir, space } = actorStateDir(values.space);
166
+ const st = actorState(values.space);
167
+ const { dir, space } = st;
164
168
  await legibly(async () => {
165
169
  if (sub === "list") {
166
170
  const rows = loadActorLedger(dir);
@@ -175,7 +179,7 @@ async function runActor(args) {
175
179
  if (sub === "grant") {
176
180
  if (!actor)
177
181
  throw new Error("usage: cotal actor grant <actor> --sub <IdP subject> [--scope a,b] [--allow-subscribe a,b] [--allow-publish a,b] [--role r] [--label l]");
178
- const owner = resolveGrantOwner(dir, values);
182
+ const owner = await resolveGrantOwner(st, values);
179
183
  // Default = the FULL grant (all channels, spawn + the stock role): `actor grant` is an
180
184
  // operator act of letting a user in, so omitting flags means "fully", and narrowing is the
181
185
  // explicit choice (`--allow-subscribe general --scope ''`). The user's agents are still
@@ -196,7 +200,7 @@ async function runActor(args) {
196
200
  if (sub === "revoke") {
197
201
  if (!actor)
198
202
  throw new Error("usage: cotal actor revoke <actor> --sub <IdP subject>|--owner <u_…>");
199
- const owner = resolveGrantOwner(dir, values);
203
+ const owner = await resolveGrantOwner(st, values);
200
204
  if (!revokeActor(dir, owner, actor)) {
201
205
  if (findManagedActor(dir, owner, actor))
202
206
  throw new Error(`${owner}.${actor} is a managed agent - its grant lives with its process: stop it if manager-owned (\`cotal stop --name ${actor}\`), or if it was a killed foreground spawn, respawn the same name (\`cotal spawn\`) to rotate the grant`);