@cotal-ai/auth 0.0.0 → 0.11.1
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/LICENSE +202 -0
- package/dist/callout.d.ts +108 -0
- package/dist/callout.d.ts.map +1 -0
- package/dist/callout.js +219 -0
- package/dist/callout.js.map +1 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +359 -0
- package/dist/commands.js.map +1 -0
- package/dist/derive.d.ts +15 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +72 -0
- package/dist/derive.js.map +1 -0
- package/dist/idp.d.ts +63 -0
- package/dist/idp.d.ts.map +1 -0
- package/dist/idp.js +125 -0
- package/dist/idp.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/issuer.d.ts +78 -0
- package/dist/issuer.d.ts.map +1 -0
- package/dist/issuer.js +137 -0
- package/dist/issuer.js.map +1 -0
- package/dist/ledger.d.ts +108 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +399 -0
- package/dist/ledger.js.map +1 -0
- package/dist/login.d.ts +69 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +338 -0
- package/dist/login.js.map +1 -0
- package/dist/permissions.d.ts +28 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +50 -0
- package/dist/permissions.js.map +1 -0
- package/dist/provider.d.ts +18 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +213 -0
- package/dist/provider.js.map +1 -0
- package/dist/service.d.ts +10 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +288 -0
- package/dist/service.js.map +1 -0
- package/dist/store.d.ts +82 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +208 -0
- package/dist/store.js.map +1 -0
- package/dist/token.d.ts +68 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +128 -0
- package/dist/token.js.map +1 -0
- package/package.json +35 -5
- package/README.md +0 -4
package/dist/provider.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core `auth-provider` extension — how a composition root gets user-mode auth WITHOUT importing
|
|
3
|
+
* this package from the CLI (`bin/cotal.ts` imports `@cotal-ai/auth`; `@cotal-ai/cli` resolves the
|
|
4
|
+
* provider from the registry, generically).
|
|
5
|
+
*
|
|
6
|
+
* `prepareServer` is the `cotal up --user-auth` hook. It receives the NARROW provisioning input
|
|
7
|
+
* (core's {@link AuthPrepareInput}: operator seed + data-account pub/signingSeed + the space-scoped
|
|
8
|
+
* state dir — never the whole space bundle), makes all persisted material exist, projects the ONE
|
|
9
|
+
* signing seed the daemon may hold into `service-keys.json`, and hands back:
|
|
10
|
+
* - the callout account for the broker config preload,
|
|
11
|
+
* - the non-secret client metadata (trust pins) the workstation registry records ({@link
|
|
12
|
+
* assertUserAuthInfo} shape — typed in workspace, opaque to core),
|
|
13
|
+
* - the service handle: the `auth-service` command name + the readiness contract (poll the
|
|
14
|
+
* discovery file the daemon writes only after BOTH planes are bound, then confirm /health).
|
|
15
|
+
*/
|
|
16
|
+
import { registry } from "@cotal-ai/core";
|
|
17
|
+
import { assertUserAuthInfo, homeCotalDir } from "@cotal-ai/workspace";
|
|
18
|
+
import { fetchIdpJwt, loadIdpSession, probeIdpJwks, requireIdpSession } from "./login.js";
|
|
19
|
+
import { deriveOwnerForIdpSubject } from "./derive.js";
|
|
20
|
+
import { findActorUnified, findInteractiveActor, grantManagedActor, newActorToken, revokeManagedActor } from "./ledger.js";
|
|
21
|
+
import { ensureCalloutAuth, ensureIssuer, ensureOwnerSecret, ensurePinnedIdp, loadAuthServiceInfo, loadCalloutAuth, loadOwnerSecret, loadPinnedIdp, saveServiceKeys, } from "./store.js";
|
|
22
|
+
const READY_TIMEOUT_MS = 15_000;
|
|
23
|
+
function pidAlive(pid) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export const cotalAuthProvider = {
|
|
33
|
+
kind: "auth-provider",
|
|
34
|
+
name: "cotal",
|
|
35
|
+
async prepareServer(input) {
|
|
36
|
+
const { space, dir, idpUrl } = input;
|
|
37
|
+
// On a FRESH enable, prove the IdP actually serves a JWKS before we pin + provision a space
|
|
38
|
+
// around it — a dead or typo'd `--idp` must fail loud here, not silently boot a broken space
|
|
39
|
+
// that only errors at the first user connect. Skip on re-up (an already-pinned IdP was validated
|
|
40
|
+
// at first enable; re-probing every boot would couple mesh liveness to IdP liveness).
|
|
41
|
+
if (idpUrl && !loadPinnedIdp(dir))
|
|
42
|
+
await probeIdpJwks(idpUrl);
|
|
43
|
+
// Pin the IdP FIRST, so a fresh `up --user-auth` without --idp fails on the config error before
|
|
44
|
+
// any key material is generated.
|
|
45
|
+
const idp = ensurePinnedIdp(dir, idpUrl);
|
|
46
|
+
ensureOwnerSecret(dir);
|
|
47
|
+
await ensureIssuer(dir, space);
|
|
48
|
+
const callout = await ensureCalloutAuth(dir, { space, operatorSeed: input.operatorSeed, accountPub: input.account.pub });
|
|
49
|
+
// The daemon's ONLY signing material: the data-account user-minting seed. Written by this
|
|
50
|
+
// (briefly privileged) call; the long-lived service loads this file, never the space bundle.
|
|
51
|
+
saveServiceKeys(dir, { dataAccount: { pub: input.account.pub, signingSeed: input.account.signingSeed } });
|
|
52
|
+
const publicAuth = assertUserAuthInfo({
|
|
53
|
+
provider: "cotal",
|
|
54
|
+
idp: { url: idp.url, issuer: idp.issuer, audience: idp.audience },
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
extraAccounts: [{ pub: callout.account.pub, jwt: callout.account.jwt }],
|
|
58
|
+
publicAuth: publicAuth,
|
|
59
|
+
service: {
|
|
60
|
+
command: "auth-service",
|
|
61
|
+
// Readiness = the daemon wrote its discovery file (which it does only after the callout SUB
|
|
62
|
+
// is flushed AND the HTTP listener is bound) and /health answers. Poll until timeoutMs, then
|
|
63
|
+
// THROW with the reason — the caller (`up`) surfaces it loudly (U5), never records a usable
|
|
64
|
+
// user mesh on a half-started service.
|
|
65
|
+
async ready({ dir: stateDir, timeoutMs = READY_TIMEOUT_MS }) {
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
67
|
+
let lastReason = "the auth service has not written its discovery file yet";
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
try {
|
|
70
|
+
const info = loadAuthServiceInfo(stateDir);
|
|
71
|
+
if (info && pidAlive(info.pid)) {
|
|
72
|
+
// pid-liveness first: a STALE file from a dead prior daemon must never satisfy
|
|
73
|
+
// this poll (the daemon also scrubs it at startup and on exit — belt and braces).
|
|
74
|
+
const res = await fetch(`${info.url}/health`, { signal: AbortSignal.timeout(2000) });
|
|
75
|
+
if (res.ok)
|
|
76
|
+
return { url: info.url };
|
|
77
|
+
lastReason = `health probe at ${info.url}/health returned HTTP ${res.status}`;
|
|
78
|
+
}
|
|
79
|
+
else if (info) {
|
|
80
|
+
lastReason = `discovery file names pid ${info.pid}, which is not running (stale entry)`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
lastReason = e instanceof Error ? e.message : String(e);
|
|
85
|
+
}
|
|
86
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
87
|
+
}
|
|
88
|
+
// No log-path guess here — the CALLER owns the daemon's log location and appends it.
|
|
89
|
+
throw new Error(`auth service not ready after ${timeoutMs}ms (${lastReason})`);
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
/** Client side: this machine's login session → a fresh IdP JWT → the local auth service's
|
|
95
|
+
* exchange → the Cotal bearer, plus the space's sentinel creds. NO fallback anywhere; each
|
|
96
|
+
* failure is one sentence with the exact operator action (U1/U10/U11 acceptance strings). */
|
|
97
|
+
async userCredentials({ dir, space, actor, view }) {
|
|
98
|
+
const idp = loadPinnedIdp(dir);
|
|
99
|
+
const callout = loadCalloutAuth(dir);
|
|
100
|
+
if (!idp || !callout)
|
|
101
|
+
throw new Error(`space "${space}" has no user-auth material on this machine - user-mode connects run where \`cotal up --user-auth\` provisioned the space (remote discovery is not supported yet)`);
|
|
102
|
+
// The no-fallback login gate: throws the exact `cotal login --idp …` line when not signed in.
|
|
103
|
+
const session = requireIdpSession(homeCotalDir(), idp.url);
|
|
104
|
+
// Daemon liveness BEFORE the IdP round-trip: a down auth service must surface its exact
|
|
105
|
+
// restart recovery (U10) without spending an IdP /token call — and without an unrelated
|
|
106
|
+
// IdP/network failure masking it. Missing-login stays primary (the session gate above).
|
|
107
|
+
const info = loadAuthServiceInfo(dir);
|
|
108
|
+
if (!info || !pidAlive(info.pid))
|
|
109
|
+
throw new Error(`the user-auth service for space "${space}" is not running - restart it with \`cotal up\` (or \`cotal auth-service --space ${space} --server <broker>\`)`);
|
|
110
|
+
// Fresh short-lived IdP proof per connect — IdP-side revocation bites HERE, at the next fetch.
|
|
111
|
+
const idpJwt = await fetchIdpJwt(idp.url, session.token);
|
|
112
|
+
let res;
|
|
113
|
+
try {
|
|
114
|
+
res = await fetch(`${info.url}/exchange`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${info.cap}` },
|
|
117
|
+
body: JSON.stringify({ idpToken: idpJwt, actor, ...(view !== undefined ? { view } : {}) }),
|
|
118
|
+
signal: AbortSignal.timeout(15_000),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
throw new Error(`the user-auth service for space "${space}" did not answer at ${info.url} (${e instanceof Error ? e.message : String(e)}) - restart it with \`cotal up\``);
|
|
123
|
+
}
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
const body = (await res.json().catch(() => ({})));
|
|
126
|
+
// A refused exchange is an authenticated denial with the reason (an ungranted actor names
|
|
127
|
+
// the grant command); surface it verbatim — the service's copy is already operator-exact.
|
|
128
|
+
throw new Error(`signed in, but the exchange for actor "${actor}"${view ? ` (view "${view}")` : ""} was refused: ${body.error ?? `HTTP ${res.status}`}`);
|
|
129
|
+
}
|
|
130
|
+
const out = (await res.json().catch(() => ({})));
|
|
131
|
+
if (typeof out.token !== "string" || !out.token)
|
|
132
|
+
throw new Error(`the auth service's exchange returned no token - its build may be stale; restart it with \`cotal up\``);
|
|
133
|
+
return { bearer: out.token, sentinelCreds: callout.sentinelCreds };
|
|
134
|
+
},
|
|
135
|
+
/** WHO the local login is, as this space's derived owner — offline (cached session sub + the
|
|
136
|
+
* space's owner secret; no IdP round trip). The spawn paths' "whose agents are these" answer. */
|
|
137
|
+
async ownerForLogin({ dir, space }) {
|
|
138
|
+
const idp = loadPinnedIdp(dir);
|
|
139
|
+
const secret = loadOwnerSecret(dir);
|
|
140
|
+
if (!idp || !secret)
|
|
141
|
+
throw new Error(`space "${space}" has no user-auth material on this machine - spawns for a user-auth space run where \`cotal up --user-auth\` provisioned it`);
|
|
142
|
+
const session = requireIdpSession(homeCotalDir(), idp.url);
|
|
143
|
+
if (!session.sub)
|
|
144
|
+
throw new Error(`your cached login for ${idp.url} predates this build (no subject recorded) - re-run \`cotal login --idp ${idp.url}\``);
|
|
145
|
+
return deriveOwnerForIdpSubject(secret, idp.issuer, session.sub);
|
|
146
|
+
},
|
|
147
|
+
/** Offline status read: the pinned IdP, this machine's cached login, and (when the local ledger
|
|
148
|
+
* has material) the actor's grant row. No IdP round trip, no service call, no mint — `cotal
|
|
149
|
+
* status` must be able to say "not signed in" without becoming a connect. */
|
|
150
|
+
async userStatus({ dir, space, actor }) {
|
|
151
|
+
const idp = loadPinnedIdp(dir);
|
|
152
|
+
if (!idp)
|
|
153
|
+
throw new Error(`space "${space}" has no user-auth material on this machine - user-mode status reads run where \`cotal up --user-auth\` provisioned the space`);
|
|
154
|
+
const session = loadIdpSession(homeCotalDir(), idp.url);
|
|
155
|
+
if (!session?.sub)
|
|
156
|
+
return { idpUrl: idp.url };
|
|
157
|
+
const login = { sub: session.sub, expiresAt: session.expiresAt };
|
|
158
|
+
const secret = loadOwnerSecret(dir);
|
|
159
|
+
if (!secret)
|
|
160
|
+
return { idpUrl: idp.url, login };
|
|
161
|
+
const owner = deriveOwnerForIdpSubject(secret, idp.issuer, session.sub);
|
|
162
|
+
const row = findInteractiveActor(dir, owner, actor);
|
|
163
|
+
return {
|
|
164
|
+
idpUrl: idp.url,
|
|
165
|
+
login,
|
|
166
|
+
owner,
|
|
167
|
+
grant: row
|
|
168
|
+
? {
|
|
169
|
+
scope: row.scope,
|
|
170
|
+
allowSubscribe: row.allowSubscribe,
|
|
171
|
+
allowPublish: row.allowPublish,
|
|
172
|
+
...(row.role ? { role: row.role } : {}),
|
|
173
|
+
...(row.label ? { label: row.label } : {}),
|
|
174
|
+
}
|
|
175
|
+
: "not-granted",
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
/** Spawn-path grant authorship: one atomic MANAGED-AGENT row (its own row space — never
|
|
179
|
+
* IdP-exchangeable by construction) carrying the agent's ACLs + the hash of a fresh per-agent
|
|
180
|
+
* secret. Upsert semantics rotate the secret on respawn — a captured old secret dies the moment
|
|
181
|
+
* its agent is respawned. */
|
|
182
|
+
async grantAgent({ dir, space, owner, actor, scope, allowSubscribe, allowPublish, role, parent, label }) {
|
|
183
|
+
const callout = loadCalloutAuth(dir);
|
|
184
|
+
if (!callout)
|
|
185
|
+
throw new Error(`space "${space}" has no user-auth material under ${dir} - enable it with \`cotal up --user-auth --idp <url>\` before spawning user-mode agents`);
|
|
186
|
+
const { actorToken, tokenHash } = newActorToken();
|
|
187
|
+
grantManagedActor(dir, {
|
|
188
|
+
owner,
|
|
189
|
+
actor,
|
|
190
|
+
scope,
|
|
191
|
+
allowSubscribe,
|
|
192
|
+
allowPublish,
|
|
193
|
+
...(role ? { role } : {}),
|
|
194
|
+
...(parent ? { parent } : {}),
|
|
195
|
+
...(label ? { label } : {}),
|
|
196
|
+
tokenHash,
|
|
197
|
+
});
|
|
198
|
+
return { actorToken, sentinelCreds: callout.sentinelCreds };
|
|
199
|
+
},
|
|
200
|
+
async revokeAgent({ dir, owner, actor }) {
|
|
201
|
+
return revokeManagedActor(dir, owner, actor);
|
|
202
|
+
},
|
|
203
|
+
/** Fresh read across BOTH row spaces (actor names are disjoint between them, so the unified
|
|
204
|
+
* lookup is unambiguous): the manager's control authorization must see an operator's
|
|
205
|
+
* `actor grant` scope edit — or a revoke — on the very next stop/attach, hence no caching. */
|
|
206
|
+
async actorScope({ dir, owner, actor }) {
|
|
207
|
+
const row = findActorUnified(dir, owner, actor);
|
|
208
|
+
return row ? [...row.scope] : undefined;
|
|
209
|
+
},
|
|
210
|
+
agentBearerCommand: "agent-bearer",
|
|
211
|
+
};
|
|
212
|
+
registry.register(cotalAuthProvider);
|
|
213
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ParsedArgs } from "@cotal-ai/core";
|
|
2
|
+
/** JWKS max-age seconds — the cache contract's knob. Exported so a rotation tool can compute the
|
|
3
|
+
* retire floor (max-age + max bearer TTL) from it. */
|
|
4
|
+
export declare const JWKS_MAX_AGE_SEC = 300;
|
|
5
|
+
/** Run the auth service. Flags: `--space` (required), `--server` (broker URL, required), `--port`
|
|
6
|
+
* (loopback HTTP port; default ephemeral). All persisted material must already exist in the
|
|
7
|
+
* space-scoped state dir (the provider's `prepareServer` ran at `cotal up`) — a missing piece is a
|
|
8
|
+
* fail-loud config error naming the fix, never a silent partial service. */
|
|
9
|
+
export declare function runAuthService(args: ParsedArgs): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAyCA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA0B9D;uDACuD;AACvD,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAapC;;;6EAG6E;AAC7E,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA4FpE"}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AUTH SERVICE daemon — the one server-side process a user-auth space runs alongside its broker
|
|
3
|
+
* (the delivery-daemon pattern: spawned detached by `cotal up`, pid-filed per space, torn down by
|
|
4
|
+
* `cotal down`). It hosts BOTH halves of the identity plane, which share state and so belong in one
|
|
5
|
+
* isolated process rather than smeared across the manager/delivery daemons:
|
|
6
|
+
*
|
|
7
|
+
* - **Plane 2 — the NATS auth callout** ({@link startAuthCallout}): answers `$SYS.REQ.USER.AUTH`
|
|
8
|
+
* over its own callout-account connection, validating bearers offline against the LOCAL issuer
|
|
9
|
+
* key set and minting scoped data-account user JWTs.
|
|
10
|
+
* - **Plane 1 — the token exchange + JWKS, over loopback HTTP**: `POST /exchange` turns a fresh
|
|
11
|
+
* IdP JWT (from `cotal login`'s cached session) into a Cotal user bearer via the pinned
|
|
12
|
+
* {@link createIdpBridge}; `GET /jwks` publishes the issuer's public keys.
|
|
13
|
+
*
|
|
14
|
+
* Least-privilege by construction: the daemon loads ONLY provider-owned projected files from its
|
|
15
|
+
* space-scoped state dir (`.cotal/auth/<space>/`) — the data-account signing seed arrives via
|
|
16
|
+
* `service-keys.json` (minting scoped users IS this service's function); the space's operator seed
|
|
17
|
+
* and account seed never enter this process (`prepareServer` wrote the projection and kept the rest).
|
|
18
|
+
*
|
|
19
|
+
* The exchange surface is LOCAL-V1, hardened: loopback bind only; `POST /exchange` requires the
|
|
20
|
+
* per-start high-entropy capability (`Authorization: Bearer <cap>` — readable only from the 0600
|
|
21
|
+
* discovery file, so same-user file ACL is the boundary); requests carrying an `Origin` header are
|
|
22
|
+
* rejected (a browser page can reach loopback; it must not be able to drive the exchange); bodies
|
|
23
|
+
* must be `application/json`; failed exchanges are rate-limited and logged. No CORS headers, ever.
|
|
24
|
+
* Remote/cross-machine exchange is explicitly NOT this surface.
|
|
25
|
+
*
|
|
26
|
+
* Both trust boundaries authorize against the SAME actor ledger, read fresh per request — a revoke
|
|
27
|
+
* bites at the next exchange AND the next connect with no restart.
|
|
28
|
+
*
|
|
29
|
+
* JWKS cache contract (gate 4, explicit): responses carry `Cache-Control: max-age=300`. A verifier
|
|
30
|
+
* may cache the set for up to 5 minutes, so a rotated-out (retired) kid MUST stay published for at
|
|
31
|
+
* least (300s + the max bearer TTL) after rotation before `retire` — otherwise still-live bearers
|
|
32
|
+
* signed by it fail verification at a cold cache. The local callout uses `issuer.localKeySet()`
|
|
33
|
+
* (live, in-process) and is exempt.
|
|
34
|
+
*
|
|
35
|
+
* Readiness contract: the discovery file (`auth-service.json`) is written only AFTER the callout
|
|
36
|
+
* subscription is FLUSHED to the broker and the HTTP listener is bound — its existence (plus a
|
|
37
|
+
* /health probe) IS the readiness signal the provider's `ready()` polls.
|
|
38
|
+
*/
|
|
39
|
+
import { randomBytes } from "node:crypto";
|
|
40
|
+
import { createServer } from "node:http";
|
|
41
|
+
import { connect, credsAuthenticator } from "@nats-io/transport-node";
|
|
42
|
+
import { isReachable } from "@cotal-ai/core";
|
|
43
|
+
import { findCotalRoot, userAuthStateDir } from "@cotal-ai/workspace";
|
|
44
|
+
import { decodeJwt } from "jose";
|
|
45
|
+
import { startAuthCallout } from "./callout.js";
|
|
46
|
+
import { createIdpBridge } from "./idp.js";
|
|
47
|
+
import { pinnedJwksResolver } from "./issuer.js";
|
|
48
|
+
import { calloutPermissions } from "./permissions.js";
|
|
49
|
+
import { AGENT_BEARER_TTL_SEC, ledgerAclResolver, ledgerAuthorizeAgentExchange, ledgerAuthorizeConnect, ledgerAuthorizeGrant, } from "./ledger.js";
|
|
50
|
+
import { clearAuthServiceInfo, loadCalloutAuth, loadIssuer, loadOwnerSecret, loadPinnedIdp, loadServiceKeys, saveAuthServiceInfo, spaceIssuer, } from "./store.js";
|
|
51
|
+
/** JWKS max-age seconds — the cache contract's knob. Exported so a rotation tool can compute the
|
|
52
|
+
* retire floor (max-age + max bearer TTL) from it. */
|
|
53
|
+
export const JWKS_MAX_AGE_SEC = 300;
|
|
54
|
+
/** Failed-exchange rate limit: at most this many REFUSED exchanges per rolling minute; further
|
|
55
|
+
* attempts get 429 until the window drains. Successes are unthrottled (the CLI's normal path). */
|
|
56
|
+
const FAILED_EXCHANGE_PER_MIN = 30;
|
|
57
|
+
/** Invalid-capability attempts get their OWN window (same size): an unauthenticated local prober
|
|
58
|
+
* is throttled AND audited, but never consumes the refused-exchange budget of a caller holding
|
|
59
|
+
* the real capability — a cap-less process must not be able to starve legitimate exchanges. */
|
|
60
|
+
const BAD_CAP_PER_MIN = 30;
|
|
61
|
+
/** Run the auth service. Flags: `--space` (required), `--server` (broker URL, required), `--port`
|
|
62
|
+
* (loopback HTTP port; default ephemeral). All persisted material must already exist in the
|
|
63
|
+
* space-scoped state dir (the provider's `prepareServer` ran at `cotal up`) — a missing piece is a
|
|
64
|
+
* fail-loud config error naming the fix, never a silent partial service. */
|
|
65
|
+
export async function runAuthService(args) {
|
|
66
|
+
const v = args.values;
|
|
67
|
+
const space = v.space;
|
|
68
|
+
if (!space)
|
|
69
|
+
throw new Error("auth-service: --space is required");
|
|
70
|
+
const server = v.server;
|
|
71
|
+
if (!server)
|
|
72
|
+
throw new Error("auth-service: --server is required (the broker this callout serves)");
|
|
73
|
+
const port = v.port === undefined ? 0 : Number(v.port);
|
|
74
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
75
|
+
throw new Error(`auth-service: --port must be a port number, got "${v.port}"`);
|
|
76
|
+
// The provider's space-scoped state dir — every file this daemon reads lives here. The layout
|
|
77
|
+
// fact is workspace-owned (userAuthStateDir); this daemon never touches `.cotal/auth/auth.json`.
|
|
78
|
+
const dir = userAuthStateDir(findCotalRoot(), space);
|
|
79
|
+
// Scrub any stale discovery file FIRST — a dead prior daemon's entry must never satisfy a
|
|
80
|
+
// readiness poll for THIS start (the provider's ready() also pid-checks; belt and braces).
|
|
81
|
+
clearAuthServiceInfo(dir);
|
|
82
|
+
const keys = loadServiceKeys(dir);
|
|
83
|
+
const callout = loadCalloutAuth(dir);
|
|
84
|
+
const issuer = await loadIssuer(dir);
|
|
85
|
+
const ownerSecret = loadOwnerSecret(dir);
|
|
86
|
+
const idp = loadPinnedIdp(dir);
|
|
87
|
+
if (!keys || !callout || !issuer || !ownerSecret || !idp)
|
|
88
|
+
throw new Error(`auth-service: user-auth material is missing under ${dir} - enable it with \`cotal up --user-auth --idp <url>\``);
|
|
89
|
+
if (issuer.issuer !== spaceIssuer(space))
|
|
90
|
+
throw new Error(`auth-service: issuer pin ${issuer.issuer} does not match space "${space}"`);
|
|
91
|
+
if (!(await isReachable(server, { creds: callout.calloutCreds })))
|
|
92
|
+
throw new Error(`auth-service: can't reach the broker at ${server} with the callout creds - is the mesh up (with the callout account preloaded)?`);
|
|
93
|
+
// ---- Plane 2: the callout, on its own callout-account connection ----
|
|
94
|
+
const nc = await connect({
|
|
95
|
+
servers: server,
|
|
96
|
+
authenticator: credsAuthenticator(new TextEncoder().encode(callout.calloutCreds)),
|
|
97
|
+
name: `cotal:auth-service:${space}`,
|
|
98
|
+
});
|
|
99
|
+
startAuthCallout(nc, {
|
|
100
|
+
xkeySeed: callout.xkey.seed,
|
|
101
|
+
authAccount: { pub: callout.account.pub, signingSeed: callout.account.signingSeed },
|
|
102
|
+
dataAccount: { pub: keys.dataAccount.pub, signingSeed: keys.dataAccount.signingSeed },
|
|
103
|
+
space,
|
|
104
|
+
token: { key: issuer.localKeySet(), issuer: issuer.issuer },
|
|
105
|
+
authorizeActor: ledgerAuthorizeConnect(dir),
|
|
106
|
+
permissionsFor: calloutPermissions(ledgerAclResolver(dir)),
|
|
107
|
+
log: (l) => console.error(l),
|
|
108
|
+
});
|
|
109
|
+
// The subscription must be ON the broker before readiness is signaled — an `up` that recorded a
|
|
110
|
+
// usable user mesh while the SUB was still in flight would intermittently deny first connects.
|
|
111
|
+
await nc.flush();
|
|
112
|
+
// ---- Plane 1: the exchange + JWKS, loopback HTTP ----
|
|
113
|
+
const bridge = createIdpBridge({
|
|
114
|
+
idp: { issuer: idp.issuer, audience: idp.audience, key: pinnedJwksResolver(idp.jwksUri) },
|
|
115
|
+
space,
|
|
116
|
+
spaceSecret: ownerSecret,
|
|
117
|
+
issuer,
|
|
118
|
+
authorizeActor: ledgerAuthorizeGrant(dir),
|
|
119
|
+
});
|
|
120
|
+
const cap = randomBytes(32).toString("hex"); // per-start exchange capability (rotates with the daemon)
|
|
121
|
+
const failures = []; // rolling-window timestamps of REFUSED exchanges
|
|
122
|
+
const badCaps = []; // rolling-window timestamps of invalid-capability attempts
|
|
123
|
+
const http = createServer((req, res) => void handle(req, res, { issuer, bridge, cap, failures, badCaps, space, dir }));
|
|
124
|
+
await new Promise((resolvePort, reject) => {
|
|
125
|
+
http.once("error", reject);
|
|
126
|
+
http.listen(port, "127.0.0.1", () => resolvePort());
|
|
127
|
+
});
|
|
128
|
+
const addr = http.address();
|
|
129
|
+
const boundPort = typeof addr === "object" && addr ? addr.port : port;
|
|
130
|
+
const url = `http://127.0.0.1:${boundPort}`;
|
|
131
|
+
// Both planes bound — NOW write the discovery file (its existence is the readiness signal).
|
|
132
|
+
saveAuthServiceInfo(dir, { url, pid: process.pid, cap });
|
|
133
|
+
console.log(`✓ auth service up (space ${space}) - callout on ${server}, exchange/JWKS at ${url}`);
|
|
134
|
+
const stop = async () => {
|
|
135
|
+
clearAuthServiceInfo(dir); // a dead service must not satisfy the next start's readiness poll
|
|
136
|
+
http.close();
|
|
137
|
+
await nc.close().catch(() => { });
|
|
138
|
+
process.exit(0);
|
|
139
|
+
};
|
|
140
|
+
process.on("SIGINT", () => void stop());
|
|
141
|
+
process.on("SIGTERM", () => void stop());
|
|
142
|
+
// A dropped broker connection is fatal-loud, not a zombie: the supervising `up`/`down` lifecycle
|
|
143
|
+
// owns restarts, and a callout that silently stopped answering would hang every user connect.
|
|
144
|
+
await nc.closed().then((err) => {
|
|
145
|
+
clearAuthServiceInfo(dir);
|
|
146
|
+
if (err) {
|
|
147
|
+
console.error(`✗ auth-service: broker connection closed (${err.message}) - exiting`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
process.exit(0);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** Route one HTTP request. Local-only surface: /jwks (public keys, cacheable), /exchange (IdP JWT →
|
|
154
|
+
* bearer; capability-gated), /health. Anything else 404s. Errors are JSON `{ error }`. */
|
|
155
|
+
async function handle(req, res, ctx) {
|
|
156
|
+
const send = (status, body, headers = {}) => {
|
|
157
|
+
// No CORS headers, ever — a browser context must never be granted a readable response here.
|
|
158
|
+
res.writeHead(status, { "content-type": "application/json", ...headers });
|
|
159
|
+
res.end(JSON.stringify(body));
|
|
160
|
+
};
|
|
161
|
+
try {
|
|
162
|
+
if (req.url === "/health")
|
|
163
|
+
return send(200, { ok: true, issuer: ctx.issuer.issuer });
|
|
164
|
+
if (req.url === "/jwks") {
|
|
165
|
+
if (req.method !== "GET")
|
|
166
|
+
return send(405, { error: "GET only" });
|
|
167
|
+
// The explicit cache contract (see the module doc): max-age bounds how stale a verifier's set
|
|
168
|
+
// may be, which in turn floors how long a retired kid must stay published after rotation.
|
|
169
|
+
return send(200, ctx.issuer.jwks(), { "cache-control": `max-age=${JWKS_MAX_AGE_SEC}` });
|
|
170
|
+
}
|
|
171
|
+
if (req.url === "/exchange") {
|
|
172
|
+
if (req.method !== "POST")
|
|
173
|
+
return send(405, { error: "POST only" });
|
|
174
|
+
// Browser exclusion: a cross-site page CAN reach loopback, but its requests carry `Origin`
|
|
175
|
+
// (and can't strip it). The CLI never sends one. Reject before touching anything else.
|
|
176
|
+
if (req.headers.origin !== undefined)
|
|
177
|
+
return send(403, { error: "browser-origin requests are not served here" });
|
|
178
|
+
if (!/^application\/json\b/.test(req.headers["content-type"] ?? ""))
|
|
179
|
+
return send(415, { error: "content-type must be application/json" });
|
|
180
|
+
// The capability gate: same-user file ACL on the 0600 discovery file is the boundary. An
|
|
181
|
+
// invalid/missing cap is still a failed exchange attempt — audited and throttled, in its own
|
|
182
|
+
// window (see BAD_CAP_PER_MIN), before anything downstream is touched.
|
|
183
|
+
const auth = req.headers.authorization ?? "";
|
|
184
|
+
if (auth !== `Bearer ${ctx.cap}`) {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
while (ctx.badCaps.length && now - ctx.badCaps[0] > 60_000)
|
|
187
|
+
ctx.badCaps.shift();
|
|
188
|
+
ctx.badCaps.push(now);
|
|
189
|
+
console.error("auth-service: rejected an exchange with a missing/invalid capability");
|
|
190
|
+
if (ctx.badCaps.length > BAD_CAP_PER_MIN)
|
|
191
|
+
return send(429, { error: "too many invalid-capability attempts - wait a minute and retry" });
|
|
192
|
+
return send(401, { error: "missing/invalid exchange capability - read it from the space's auth-service.json" });
|
|
193
|
+
}
|
|
194
|
+
// Refused-exchange rate limit (probing protection): count only FAILURES.
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
while (ctx.failures.length && now - ctx.failures[0] > 60_000)
|
|
197
|
+
ctx.failures.shift();
|
|
198
|
+
if (ctx.failures.length >= FAILED_EXCHANGE_PER_MIN)
|
|
199
|
+
return send(429, { error: "too many refused exchanges - wait a minute and retry" });
|
|
200
|
+
const body = await readJsonBody(req);
|
|
201
|
+
const { idpToken, actor, actorToken, owner, ttlSec, view } = body;
|
|
202
|
+
if (ttlSec !== undefined && typeof ttlSec !== "number")
|
|
203
|
+
return send(400, { error: "ttlSec must be a number" });
|
|
204
|
+
if (view !== undefined && typeof view !== "string")
|
|
205
|
+
return send(400, { error: "view must be a string when present" });
|
|
206
|
+
// TWO grant types, disjoint by construction: a HUMAN exchange proves an IdP session
|
|
207
|
+
// (idpToken), an AGENT exchange proves a spawn-time ledger secret (owner + actorToken).
|
|
208
|
+
// A request presenting both is malformed — refuse rather than pick.
|
|
209
|
+
if (idpToken !== undefined && actorToken !== undefined)
|
|
210
|
+
return send(400, { error: "exchange takes idpToken (human) OR owner+actorToken (agent), never both" });
|
|
211
|
+
if (actorToken !== undefined) {
|
|
212
|
+
// Elevated views are for signed-in HUMANS only: an agent's secret exchange never mints one,
|
|
213
|
+
// whatever its ledger row carries (v1 — agents hold no god views).
|
|
214
|
+
if (view !== undefined)
|
|
215
|
+
return send(400, { error: "the managed (agent-secret) exchange never mints elevated views - views ride a signed-in human exchange" });
|
|
216
|
+
if (typeof owner !== "string" || !owner || typeof actor !== "string" || !actor || typeof actorToken !== "string" || !actorToken)
|
|
217
|
+
return send(400, { error: "agent exchange needs { owner: string, actor: string, actorToken: string, ttlSec?: number }" });
|
|
218
|
+
try {
|
|
219
|
+
const grant = ledgerAuthorizeAgentExchange(ctx.dir, owner, actor, actorToken);
|
|
220
|
+
const token = await ctx.issuer.issue({
|
|
221
|
+
owner,
|
|
222
|
+
space: ctx.space,
|
|
223
|
+
actor,
|
|
224
|
+
scope: grant.scope,
|
|
225
|
+
parent: grant.parent,
|
|
226
|
+
ttlSec: Math.min(ttlSec ?? AGENT_BEARER_TTL_SEC, AGENT_BEARER_TTL_SEC),
|
|
227
|
+
});
|
|
228
|
+
const { exp } = decodeJwt(token);
|
|
229
|
+
return send(200, { token, owner, exp });
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
ctx.failures.push(Date.now());
|
|
233
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
234
|
+
console.error(`auth-service: refused an agent exchange: ${reason}`);
|
|
235
|
+
return send(401, { error: reason });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (typeof idpToken !== "string" || !idpToken || typeof actor !== "string" || !actor)
|
|
239
|
+
return send(400, { error: "exchange needs { idpToken: string, actor: string, ttlSec?: number, view?: string }" });
|
|
240
|
+
try {
|
|
241
|
+
// The bridge validates `view` against the closed enum and the fresh ledger grant — an
|
|
242
|
+
// unknown or under-scoped view is a refused exchange (audited + throttled like any other).
|
|
243
|
+
const r = await ctx.bridge.exchange(idpToken, { actor, ttlSec, view: view });
|
|
244
|
+
return send(200, r);
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
// A refused exchange (bad IdP token, ungranted actor, expired proof) is an AUTHENTICATED
|
|
248
|
+
// denial with the reason — the client shows it to the operator verbatim.
|
|
249
|
+
ctx.failures.push(Date.now());
|
|
250
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
251
|
+
console.error(`auth-service: refused an exchange: ${reason}`);
|
|
252
|
+
return send(401, { error: reason });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return send(404, { error: "unknown path - /health, /jwks, /exchange" });
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
send(400, { error: e instanceof Error ? e.message : String(e) });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Read + parse a small JSON body, bounded — the exchange payload is an IdP JWT plus an actor name;
|
|
262
|
+
* 64 KB clears any sane JWT while keeping the loopback surface un-floodable. */
|
|
263
|
+
function readJsonBody(req) {
|
|
264
|
+
const MAX = 64 * 1024;
|
|
265
|
+
return new Promise((resolve, reject) => {
|
|
266
|
+
let size = 0;
|
|
267
|
+
const chunks = [];
|
|
268
|
+
req.on("data", (c) => {
|
|
269
|
+
size += c.length;
|
|
270
|
+
if (size > MAX) {
|
|
271
|
+
reject(new Error("request body too large"));
|
|
272
|
+
req.destroy();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
chunks.push(c);
|
|
276
|
+
});
|
|
277
|
+
req.on("end", () => {
|
|
278
|
+
try {
|
|
279
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
reject(new Error("request body is not valid JSON"));
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
req.on("error", reject);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type CalloutAuth, type CalloutProvisionInput } from "./callout.js";
|
|
2
|
+
import { type UserTokenIssuer } from "./issuer.js";
|
|
3
|
+
/** The pinned `iss` for a space's Cotal user bearers — a stable URN, deliberately NOT the auth
|
|
4
|
+
* service's URL (the service port is ephemeral; an issuer pin must never change across restarts).
|
|
5
|
+
* Both the issuer (mint) and the callout (validate) take it from here — one source, no drift. */
|
|
6
|
+
export declare function spaceIssuer(space: string): string;
|
|
7
|
+
/** Load the persisted callout account material, or undefined if this space never enabled user auth. */
|
|
8
|
+
export declare function loadCalloutAuth(dir: string): CalloutAuth | undefined;
|
|
9
|
+
/** Load-or-create the callout account: first call on a space mints it (operator seed required)
|
|
10
|
+
* and persists; every later call returns the SAME account, so the broker config preload and
|
|
11
|
+
* previously-issued sentinel creds stay valid. Idempotent. */
|
|
12
|
+
export declare function ensureCalloutAuth(dir: string, input: CalloutProvisionInput): Promise<CalloutAuth>;
|
|
13
|
+
/** Load the persisted issuer (all published kids live; the persisted active kid signs), or undefined
|
|
14
|
+
* if this space never enabled user auth. */
|
|
15
|
+
export declare function loadIssuer(dir: string): Promise<UserTokenIssuer | undefined>;
|
|
16
|
+
/** Load-or-create the space's user-bearer issuer: first call generates the Ed25519 signing key and
|
|
17
|
+
* persists it under the pinned {@link spaceIssuer} `iss`; later calls return the SAME keys, so
|
|
18
|
+
* outstanding bearers keep verifying. A persisted `iss` that disagrees with the space's pin fails
|
|
19
|
+
* loud (the material belongs to another space/layout — never sign under a mismatched issuer). */
|
|
20
|
+
export declare function ensureIssuer(dir: string, space: string): Promise<UserTokenIssuer>;
|
|
21
|
+
/** Load the owner-derivation secret, or undefined if this space never enabled user auth. */
|
|
22
|
+
export declare function loadOwnerSecret(dir: string): Uint8Array | undefined;
|
|
23
|
+
/** Load-or-create the per-space owner-derivation secret. There is deliberately NO regenerate path:
|
|
24
|
+
* a new secret re-keys every owner in the space (a migration, never an accident). */
|
|
25
|
+
export declare function ensureOwnerSecret(dir: string): Uint8Array;
|
|
26
|
+
/** The persisted pin of THE external IdP this space trusts (plan gate 4: "pin IdP JWKS/issuer/
|
|
27
|
+
* audience"). All four strings are frozen at `up --user-auth --idp <url>` time; nothing here is
|
|
28
|
+
* ever read from a presented token. Better Auth conventions seed the derivation (issuer/audience =
|
|
29
|
+
* the base URL's origin, JWKS under `<base>/jwks`) — a strict-OIDC IdP with different values plugs
|
|
30
|
+
* in by editing the pin file deliberately, not by the client guessing. */
|
|
31
|
+
export interface PinnedIdp {
|
|
32
|
+
/** The auth base URL as given (normalized; the `cotal login --idp` target). */
|
|
33
|
+
url: string;
|
|
34
|
+
/** Exact `iss` the IdP mints. */
|
|
35
|
+
issuer: string;
|
|
36
|
+
/** Exact `aud` the IdP mints. */
|
|
37
|
+
audience: string;
|
|
38
|
+
/** The pinned JWKS URL keys resolve from — the ONLY key path. */
|
|
39
|
+
jwksUri: string;
|
|
40
|
+
}
|
|
41
|
+
/** Load the pinned IdP, or undefined if this space never pinned one. */
|
|
42
|
+
export declare function loadPinnedIdp(dir: string): PinnedIdp | undefined;
|
|
43
|
+
/** Pin-or-verify the space's IdP. First call REQUIRES a URL (there is no default IdP); later calls
|
|
44
|
+
* either omit it (reuse the pin) or must MATCH it — re-pointing a space at a different IdP re-keys
|
|
45
|
+
* every derived owner (the issuer is part of the derivation input), a migration this refuses to do
|
|
46
|
+
* as a flag side-effect. */
|
|
47
|
+
export declare function ensurePinnedIdp(dir: string, idpUrl?: string): PinnedIdp;
|
|
48
|
+
/** EXACTLY what the long-lived auth service may hold of the space's signing material: the data
|
|
49
|
+
* account's pub + user-minting signing seed (minting scoped users at connect time IS the service's
|
|
50
|
+
* function) — and nothing else. Written by `prepareServer` (which may briefly see more), loaded by
|
|
51
|
+
* the daemon INSTEAD of the space's full trust bundle: the operator seed and the account seed never
|
|
52
|
+
* enter the service process. */
|
|
53
|
+
export interface ServiceKeys {
|
|
54
|
+
dataAccount: {
|
|
55
|
+
pub: string;
|
|
56
|
+
signingSeed: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export declare function loadServiceKeys(dir: string): ServiceKeys | undefined;
|
|
60
|
+
/** Write (or refresh) the service's key projection. Idempotent overwrite — the projection carries
|
|
61
|
+
* no identity of its own, it IS the (stable) data-account signing material, so rewriting from the
|
|
62
|
+
* same space bundle is a no-op in content. */
|
|
63
|
+
export declare function saveServiceKeys(dir: string, keys: ServiceKeys): void;
|
|
64
|
+
/** Where the RUNNING auth service listens — written by the daemon only after EVERY plane is bound
|
|
65
|
+
* (its existence is the readiness signal), read by user-mode connects from other directories on
|
|
66
|
+
* this machine. Runtime state, not a trust pin: the port and capability rotate per start.
|
|
67
|
+
* `cap` is the per-start exchange capability: /exchange requires `Authorization: Bearer <cap>`,
|
|
68
|
+
* so a local process can only exchange if it can read this 0600 file — same-user file ACL is the
|
|
69
|
+
* boundary, and browser/off-user processes are shut out of the loopback port. */
|
|
70
|
+
export interface AuthServiceInfo {
|
|
71
|
+
/** The local exchange/JWKS base URL, e.g. `http://127.0.0.1:53200`. */
|
|
72
|
+
url: string;
|
|
73
|
+
pid: number;
|
|
74
|
+
/** High-entropy per-start exchange capability (hex). NEVER copied into the mesh registry. */
|
|
75
|
+
cap: string;
|
|
76
|
+
}
|
|
77
|
+
export declare function loadAuthServiceInfo(dir: string): AuthServiceInfo | undefined;
|
|
78
|
+
export declare function saveAuthServiceInfo(dir: string, info: AuthServiceInfo): void;
|
|
79
|
+
/** Remove the discovery file (daemon shutdown / CLI pre-start scrub) so a stale entry can never
|
|
80
|
+
* satisfy a readiness poll for the NEXT start. */
|
|
81
|
+
export declare function clearAuthServiceInfo(dir: string): void;
|
|
82
|
+
//# sourceMappingURL=store.d.ts.map
|