@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.
- package/dist/admission-mediator.d.ts +319 -0
- package/dist/admission-mediator.d.ts.map +1 -0
- package/dist/admission-mediator.js +1033 -0
- package/dist/admission-mediator.js.map +1 -0
- package/dist/auth-admin.d.ts +29 -0
- package/dist/auth-admin.d.ts.map +1 -0
- package/dist/auth-admin.js +224 -0
- package/dist/auth-admin.js.map +1 -0
- package/dist/authority-client.d.ts +215 -0
- package/dist/authority-client.d.ts.map +1 -0
- package/dist/authority-client.js +346 -0
- package/dist/authority-client.js.map +1 -0
- package/dist/barrier-evict.d.ts +16 -0
- package/dist/barrier-evict.d.ts.map +1 -0
- package/dist/barrier-evict.js +100 -0
- package/dist/barrier-evict.js.map +1 -0
- package/dist/commands.js +16 -12
- package/dist/commands.js.map +1 -1
- package/dist/connect-reader.d.ts +72 -0
- package/dist/connect-reader.d.ts.map +1 -0
- package/dist/connect-reader.js +154 -0
- package/dist/connect-reader.js.map +1 -0
- package/dist/continuity.d.ts +23 -0
- package/dist/continuity.d.ts.map +1 -0
- package/dist/continuity.js +145 -0
- package/dist/continuity.js.map +1 -0
- package/dist/credential-ledger.d.ts +343 -0
- package/dist/credential-ledger.d.ts.map +1 -0
- package/dist/credential-ledger.js +732 -0
- package/dist/credential-ledger.js.map +1 -0
- package/dist/drain-repair.d.ts +95 -0
- package/dist/drain-repair.d.ts.map +1 -0
- package/dist/drain-repair.js +305 -0
- package/dist/drain-repair.js.map +1 -0
- package/dist/idp.d.ts +19 -0
- package/dist/idp.d.ts.map +1 -1
- package/dist/idp.js +17 -0
- package/dist/idp.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -3
- package/dist/index.js.map +1 -1
- package/dist/issuer.d.ts +8 -0
- package/dist/issuer.d.ts.map +1 -1
- package/dist/issuer.js +4 -2
- package/dist/issuer.js.map +1 -1
- package/dist/ledger-scanner.d.ts +98 -0
- package/dist/ledger-scanner.d.ts.map +1 -0
- package/dist/ledger-scanner.js +352 -0
- package/dist/ledger-scanner.js.map +1 -0
- package/dist/ledger.d.ts +12 -0
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +103 -13
- package/dist/ledger.js.map +1 -1
- package/dist/lifecycle-registry.d.ts +397 -0
- package/dist/lifecycle-registry.d.ts.map +1 -0
- package/dist/lifecycle-registry.js +819 -0
- package/dist/lifecycle-registry.js.map +1 -0
- package/dist/permissions.d.ts +5 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +29 -6
- package/dist/permissions.js.map +1 -1
- package/dist/plane-claim.d.ts +83 -0
- package/dist/plane-claim.d.ts.map +1 -0
- package/dist/plane-claim.js +290 -0
- package/dist/plane-claim.js.map +1 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +45 -16
- package/dist/provider.js.map +1 -1
- package/dist/records-scanner.d.ts +78 -0
- package/dist/records-scanner.d.ts.map +1 -0
- package/dist/records-scanner.js +320 -0
- package/dist/records-scanner.js.map +1 -0
- package/dist/retirement-barrier.d.ts +217 -0
- package/dist/retirement-barrier.d.ts.map +1 -0
- package/dist/retirement-barrier.js +624 -0
- package/dist/retirement-barrier.js.map +1 -0
- package/dist/retirement-cleaner.d.ts +54 -0
- package/dist/retirement-cleaner.d.ts.map +1 -0
- package/dist/retirement-cleaner.js +158 -0
- package/dist/retirement-cleaner.js.map +1 -0
- package/dist/root-credential.d.ts +26 -0
- package/dist/root-credential.d.ts.map +1 -0
- package/dist/root-credential.js +116 -0
- package/dist/root-credential.js.map +1 -0
- package/dist/service.d.ts +73 -5
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +349 -24
- package/dist/service.js.map +1 -1
- package/dist/session-ledger.d.ts +224 -0
- package/dist/session-ledger.d.ts.map +1 -0
- package/dist/session-ledger.js +713 -0
- package/dist/session-ledger.js.map +1 -0
- package/dist/store.d.ts +18 -8
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +74 -51
- package/dist/store.js.map +1 -1
- package/dist/token.d.ts +17 -0
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +30 -2
- package/dist/token.js.map +1 -1
- 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
|
|
90
|
-
* multi-space-ready layout; nothing user-auth lives flat in
|
|
91
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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`);
|