@cortexkit/common-auth 0.2.6 → 0.2.8
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/claustrum/consumer.d.ts +93 -0
- package/dist/claustrum/consumer.js +276 -0
- package/dist/claustrum/custody.d.ts +128 -0
- package/dist/claustrum/custody.js +321 -0
- package/dist/claustrum/enrollment.d.ts +121 -0
- package/dist/claustrum/enrollment.js +579 -0
- package/dist/claustrum/errors.d.ts +16 -0
- package/dist/claustrum/errors.js +8 -0
- package/dist/claustrum/host-slot.d.ts +39 -0
- package/dist/claustrum/host-slot.js +72 -0
- package/dist/claustrum/index.d.ts +18 -1
- package/dist/claustrum/index.js +22 -2
- package/dist/claustrum/interlock.d.ts +29 -0
- package/dist/claustrum/interlock.js +36 -0
- package/dist/claustrum/roster.d.ts +103 -0
- package/dist/claustrum/roster.js +334 -0
- package/dist/opencode2/errors.d.ts +23 -0
- package/dist/opencode2/errors.js +15 -0
- package/dist/opencode2/index.d.ts +8 -1
- package/dist/opencode2/index.js +4 -2
- package/dist/opencode2/install.d.ts +19 -0
- package/dist/opencode2/install.js +415 -0
- package/dist/opencode2/integration.d.ts +78 -0
- package/dist/opencode2/integration.js +94 -0
- package/dist/opencode2/sse.d.ts +12 -0
- package/dist/opencode2/sse.js +60 -0
- package/dist/opencode2/types.d.ts +202 -0
- package/dist/opencode2/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { ClaustrumCredentialError, } from '@cortexkit/claustrum-client';
|
|
2
|
+
import { createLogger } from '../logger/index.js';
|
|
3
|
+
import { readClaustrumEnrollmentToken } from './enrollment.js';
|
|
4
|
+
import { ClaustrumConsumerError } from './errors.js';
|
|
5
|
+
import { CUSTODY_PLACEHOLDER_PREFIX } from './host-slot.js';
|
|
6
|
+
/** Only a new version of the same account may replace an in-flight 401. */
|
|
7
|
+
export function isScopedCredentialRotation(served, current) {
|
|
8
|
+
return (current !== undefined &&
|
|
9
|
+
current.credentialId === served.credentialId &&
|
|
10
|
+
current.accountIdentity === served.accountIdentity &&
|
|
11
|
+
current.recordVersion !== served.recordVersion);
|
|
12
|
+
}
|
|
13
|
+
function scopedRetryReason(served, current) {
|
|
14
|
+
if (current === undefined)
|
|
15
|
+
return 'reauthorize-failed';
|
|
16
|
+
if (current.credentialId !== served.credentialId)
|
|
17
|
+
return 'credential-changed';
|
|
18
|
+
if (current.accountIdentity !== served.accountIdentity)
|
|
19
|
+
return 'account-changed';
|
|
20
|
+
if (current.recordVersion === served.recordVersion)
|
|
21
|
+
return 'version-unchanged';
|
|
22
|
+
return 'rotated';
|
|
23
|
+
}
|
|
24
|
+
const defaultLogger = createLogger('claustrum');
|
|
25
|
+
/**
|
|
26
|
+
* Decide whether a request that got a 401 should retry with the freshly
|
|
27
|
+
* re-authorized receipt, and log the decision. The vault can refresh a
|
|
28
|
+
* credential between the send and the 401; the log line lets that refresh be
|
|
29
|
+
* matched against the consumer's retry. `site` names the kind of request.
|
|
30
|
+
*/
|
|
31
|
+
export function decideScopedRetryAfter401(site, served, current, logger = defaultLogger) {
|
|
32
|
+
const retry = isScopedCredentialRotation(served, current);
|
|
33
|
+
logger.debug('scoped 401 re-authorized', {
|
|
34
|
+
site,
|
|
35
|
+
credentialId: served.credentialId,
|
|
36
|
+
servedVersion: served.recordVersion,
|
|
37
|
+
currentVersion: current?.recordVersion ?? null,
|
|
38
|
+
retry,
|
|
39
|
+
reason: scopedRetryReason(served, current),
|
|
40
|
+
});
|
|
41
|
+
return retry;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* How long a served OAuth token must stay valid after the vault hands it out,
|
|
45
|
+
* so it cannot expire while a long request is still using it.
|
|
46
|
+
*/
|
|
47
|
+
export const SERVING_MARGIN_MS = 300_000;
|
|
48
|
+
function invalidMaterial() {
|
|
49
|
+
return new ClaustrumConsumerError('invalid-material', 'Claustrum returned invalid credential material');
|
|
50
|
+
}
|
|
51
|
+
function accessTokenFromMaterial(material) {
|
|
52
|
+
let access = material.trim();
|
|
53
|
+
if (access.startsWith('{')) {
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(access);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw invalidMaterial();
|
|
60
|
+
}
|
|
61
|
+
if (!parsed || typeof parsed !== 'object')
|
|
62
|
+
throw invalidMaterial();
|
|
63
|
+
const record = parsed;
|
|
64
|
+
const value = record.access_token ?? record.access;
|
|
65
|
+
access = typeof value === 'string' ? value : '';
|
|
66
|
+
}
|
|
67
|
+
// A header-safe token only: control characters could split the request,
|
|
68
|
+
// and the custody placeholder must never be sent as a credential.
|
|
69
|
+
if (!/^[\x21-\x7e]+$/.test(access) ||
|
|
70
|
+
access.startsWith(CUSTODY_PLACEHOLDER_PREFIX)) {
|
|
71
|
+
throw invalidMaterial();
|
|
72
|
+
}
|
|
73
|
+
return access;
|
|
74
|
+
}
|
|
75
|
+
function credentialTypeOf(row, family) {
|
|
76
|
+
if (!row.operations.includes('read'))
|
|
77
|
+
return undefined;
|
|
78
|
+
if (!row.categories.includes(family.category))
|
|
79
|
+
return undefined;
|
|
80
|
+
if (row.credentialType === 'oauth')
|
|
81
|
+
return row.refreshAdapter === family.refreshAdapter ? 'oauth' : undefined;
|
|
82
|
+
if (row.credentialType === 'api_key' && family.apiKeys)
|
|
83
|
+
return row.refreshAdapter === undefined ? 'api_key' : undefined;
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reads this consumer's vault credentials (list, fetch, 401 report), with the
|
|
88
|
+
* enrollment token as authorization. Used by every host of a plugin. There is
|
|
89
|
+
* deliberately no credential cache and no single-flight of credential reads:
|
|
90
|
+
* each physical send is authorized by the vault, so a revoked enrollment or a
|
|
91
|
+
* changed record takes effect on the next send. The enrollment token is
|
|
92
|
+
* re-read per operation so an operator's reissue on disk is picked up.
|
|
93
|
+
*/
|
|
94
|
+
export class ClaustrumScopedCustody {
|
|
95
|
+
#client;
|
|
96
|
+
#readToken;
|
|
97
|
+
#now;
|
|
98
|
+
#family;
|
|
99
|
+
#parseIdentity;
|
|
100
|
+
#logger;
|
|
101
|
+
#provenance = new WeakMap();
|
|
102
|
+
#reports = new WeakMap();
|
|
103
|
+
#closed = false;
|
|
104
|
+
constructor(options) {
|
|
105
|
+
const tokenPath = options.tokenPath;
|
|
106
|
+
if (options.readToken) {
|
|
107
|
+
this.#readToken = options.readToken;
|
|
108
|
+
}
|
|
109
|
+
else if (tokenPath) {
|
|
110
|
+
this.#readToken = () => readClaustrumEnrollmentToken(tokenPath);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
throw new ClaustrumConsumerError('not-enrolled', 'Claustrum enrollment token path is required');
|
|
114
|
+
}
|
|
115
|
+
this.#client = options.client;
|
|
116
|
+
this.#family = options.family;
|
|
117
|
+
this.#parseIdentity = options.parseIdentity;
|
|
118
|
+
this.#now = options.now ?? Date.now;
|
|
119
|
+
this.#logger = options.logger ?? defaultLogger;
|
|
120
|
+
}
|
|
121
|
+
#check(signal) {
|
|
122
|
+
if (this.#closed)
|
|
123
|
+
throw new ClaustrumConsumerError('closed', 'Claustrum scoped custody is closed');
|
|
124
|
+
signal?.throwIfAborted();
|
|
125
|
+
}
|
|
126
|
+
async #token(signal) {
|
|
127
|
+
this.#check(signal);
|
|
128
|
+
const value = await this.#readToken();
|
|
129
|
+
this.#check(signal);
|
|
130
|
+
if (!/^[0-9a-f]{64}$/.test(value.token) ||
|
|
131
|
+
!Number.isSafeInteger(value.token_generation) ||
|
|
132
|
+
value.token_generation < 1) {
|
|
133
|
+
throw new ClaustrumConsumerError('invalid-token', 'Invalid Claustrum enrollment token');
|
|
134
|
+
}
|
|
135
|
+
return value.token;
|
|
136
|
+
}
|
|
137
|
+
async #call(operation, signal) {
|
|
138
|
+
this.#check(signal);
|
|
139
|
+
let result;
|
|
140
|
+
let removeAbortListener;
|
|
141
|
+
try {
|
|
142
|
+
const pending = operation();
|
|
143
|
+
result = signal
|
|
144
|
+
? await Promise.race([
|
|
145
|
+
pending,
|
|
146
|
+
new Promise((_resolve, reject) => {
|
|
147
|
+
const abort = () => reject(signal.reason);
|
|
148
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
149
|
+
removeAbortListener = () => signal.removeEventListener('abort', abort);
|
|
150
|
+
if (signal.aborted)
|
|
151
|
+
abort();
|
|
152
|
+
}),
|
|
153
|
+
])
|
|
154
|
+
: await pending;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
this.#check(signal);
|
|
158
|
+
// Keep the vault's own refusals (ClaustrumCredentialError, carrying
|
|
159
|
+
// code, class and action); replace any other error, whose text may echo
|
|
160
|
+
// request params that include the enrollment token.
|
|
161
|
+
if (error instanceof ClaustrumCredentialError)
|
|
162
|
+
throw error;
|
|
163
|
+
throw new ClaustrumConsumerError('unavailable', 'Claustrum scoped operation unavailable');
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
removeAbortListener?.();
|
|
167
|
+
}
|
|
168
|
+
this.#check(signal);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* List this consumer's credentials. A record that cannot be used is skipped
|
|
173
|
+
* and warned about rather than failing the whole list, so one bad record
|
|
174
|
+
* never hides every other account. Records outside the family are not
|
|
175
|
+
* skipped records: they simply are not this consumer's.
|
|
176
|
+
*/
|
|
177
|
+
async discover(signal) {
|
|
178
|
+
const token = await this.#token(signal);
|
|
179
|
+
const inventory = await this.#call(() => this.#client.listScoped(token), signal);
|
|
180
|
+
const skipped = [];
|
|
181
|
+
const candidates = [];
|
|
182
|
+
const counts = new Map();
|
|
183
|
+
for (const row of inventory.rows) {
|
|
184
|
+
const type = credentialTypeOf(row, this.#family);
|
|
185
|
+
if (!type)
|
|
186
|
+
continue;
|
|
187
|
+
candidates.push({ row, type });
|
|
188
|
+
counts.set(row.id, (counts.get(row.id) ?? 0) + 1);
|
|
189
|
+
}
|
|
190
|
+
const credentials = [];
|
|
191
|
+
for (const { row, type } of candidates) {
|
|
192
|
+
let reason;
|
|
193
|
+
if (!row.id.trim())
|
|
194
|
+
reason = 'empty credential id';
|
|
195
|
+
else if ((counts.get(row.id) ?? 0) > 1)
|
|
196
|
+
reason = 'duplicate credential id';
|
|
197
|
+
else if (row.accountId !== undefined && !row.accountId.trim())
|
|
198
|
+
reason = 'blank account identity';
|
|
199
|
+
else if (!row.state.trim())
|
|
200
|
+
reason = 'empty state';
|
|
201
|
+
if (reason) {
|
|
202
|
+
skipped.push({ ...(row.id.trim() && { credentialId: row.id }), reason });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
credentials.push(Object.freeze({
|
|
206
|
+
credentialId: row.id,
|
|
207
|
+
credentialType: type,
|
|
208
|
+
...(row.accountId !== undefined && {
|
|
209
|
+
accountIdentity: row.accountId,
|
|
210
|
+
}),
|
|
211
|
+
state: row.state,
|
|
212
|
+
...(row.email !== undefined && { email: row.email }),
|
|
213
|
+
...(row.orgName !== undefined && { orgName: row.orgName }),
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
for (const record of skipped)
|
|
217
|
+
this.#logger.warn('skipped malformed vault record', record);
|
|
218
|
+
return {
|
|
219
|
+
view: inventory.view,
|
|
220
|
+
credentials: Object.freeze(credentials),
|
|
221
|
+
skipped: Object.freeze(skipped),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Fetch the credential for one physical send and wrap it in a fresh receipt.
|
|
226
|
+
* Identity is checked only where both sides assert one: the vault's served
|
|
227
|
+
* identity (or, without one, the plugin's parse of the token) must equal the
|
|
228
|
+
* roster's when both are present; absence on either side proves nothing and
|
|
229
|
+
* does not refuse.
|
|
230
|
+
*/
|
|
231
|
+
async authorize(identity, signal) {
|
|
232
|
+
if (!identity.credentialId)
|
|
233
|
+
throw new ClaustrumConsumerError('route-unavailable', 'Claustrum dispatch requires a credential id');
|
|
234
|
+
// Capture the caller's fields before yielding so a later mutation cannot move the fence.
|
|
235
|
+
const { credentialId, credentialType, accountIdentity } = identity;
|
|
236
|
+
const token = await this.#token(signal);
|
|
237
|
+
const served = await this.#call(() => this.#client.getScoped({
|
|
238
|
+
credentialId,
|
|
239
|
+
enrollmentToken: token,
|
|
240
|
+
...(credentialType === 'oauth' && { minTtlMs: SERVING_MARGIN_MS }),
|
|
241
|
+
}), signal);
|
|
242
|
+
if (served.credentialId !== undefined &&
|
|
243
|
+
served.credentialId !== credentialId) {
|
|
244
|
+
throw new ClaustrumConsumerError('identity-changed', 'Claustrum served credential identity changed');
|
|
245
|
+
}
|
|
246
|
+
const expiresAtMs = served.expiresAtMs;
|
|
247
|
+
if (!Number.isSafeInteger(served.recordVersion) ||
|
|
248
|
+
served.recordVersion < 0 ||
|
|
249
|
+
(expiresAtMs === null && credentialType === 'oauth') ||
|
|
250
|
+
(expiresAtMs !== null &&
|
|
251
|
+
(!Number.isFinite(expiresAtMs) ||
|
|
252
|
+
expiresAtMs - this.#now() < SERVING_MARGIN_MS))) {
|
|
253
|
+
throw new ClaustrumConsumerError('insufficient-validity', 'Claustrum served credential has insufficient validity');
|
|
254
|
+
}
|
|
255
|
+
const accessToken = accessTokenFromMaterial(served.material);
|
|
256
|
+
const servedIdentity = served.accountId?.trim()
|
|
257
|
+
? served.accountId
|
|
258
|
+
: this.#parseIdentity?.(accessToken);
|
|
259
|
+
if (accountIdentity !== undefined &&
|
|
260
|
+
servedIdentity !== undefined &&
|
|
261
|
+
servedIdentity !== accountIdentity) {
|
|
262
|
+
throw new ClaustrumConsumerError('identity-changed', 'Claustrum served credential identity changed');
|
|
263
|
+
}
|
|
264
|
+
const resolvedIdentity = accountIdentity ?? servedIdentity;
|
|
265
|
+
const attempt = Object.freeze(Object.defineProperty({
|
|
266
|
+
credentialId,
|
|
267
|
+
credentialType,
|
|
268
|
+
...(resolvedIdentity !== undefined && {
|
|
269
|
+
accountIdentity: resolvedIdentity,
|
|
270
|
+
}),
|
|
271
|
+
recordVersion: served.recordVersion,
|
|
272
|
+
expiresAtMs,
|
|
273
|
+
}, 'accessToken', { value: accessToken, enumerable: false }));
|
|
274
|
+
this.#provenance.set(attempt, token);
|
|
275
|
+
return attempt;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Report that a send this consumer actually made was rejected. Only a 401
|
|
279
|
+
* is reported (the vault refuses 429, 402 and 5xx reports), addressed by
|
|
280
|
+
* credential id with the exact record version that send was served, and
|
|
281
|
+
* signed with the enrollment token that authorized it. A receipt this
|
|
282
|
+
* custody did not issue is refused rather than reported.
|
|
283
|
+
*/
|
|
284
|
+
async reportFailure(attempt, status, reporterSource) {
|
|
285
|
+
if (status !== 401)
|
|
286
|
+
return;
|
|
287
|
+
this.#check();
|
|
288
|
+
const token = this.#provenance.get(attempt);
|
|
289
|
+
if (!token)
|
|
290
|
+
throw new ClaustrumConsumerError('no-receipt', 'Claustrum failure report requires an original dispatch receipt');
|
|
291
|
+
const pending = this.#reports.get(attempt);
|
|
292
|
+
if (pending)
|
|
293
|
+
return pending;
|
|
294
|
+
const report = this.#call(() => this.#client.reportAuthFailureScoped({
|
|
295
|
+
credentialId: attempt.credentialId,
|
|
296
|
+
enrollmentToken: token,
|
|
297
|
+
providerStatus: 401,
|
|
298
|
+
recordVersion: attempt.recordVersion,
|
|
299
|
+
reporterSource,
|
|
300
|
+
}));
|
|
301
|
+
this.#reports.set(attempt, report);
|
|
302
|
+
try {
|
|
303
|
+
await report;
|
|
304
|
+
this.#logger.debug('scoped 401 reported', {
|
|
305
|
+
credentialId: attempt.credentialId,
|
|
306
|
+
recordVersion: attempt.recordVersion,
|
|
307
|
+
reporterSource,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
this.#reports.delete(attempt);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
close() {
|
|
316
|
+
if (this.#closed)
|
|
317
|
+
return;
|
|
318
|
+
this.#closed = true;
|
|
319
|
+
this.#client.close();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { type ClaustrumClientOptions as ClaustrumEnrollmentClientOptions, type EnrollmentPollOutcome, type EnrollmentTokenFile, writeEnrollmentTokenFile } from '@cortexkit/claustrum-client';
|
|
2
|
+
/**
|
|
3
|
+
* Codes the vault's closed enrollment-refusal vocabulary marks permanent. The
|
|
4
|
+
* client labels some module error frames transient/retry regardless, so the
|
|
5
|
+
* producer's code takes precedence over the client's action for these.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TERMINAL_ENROLLMENT_CODES: ReadonlySet<string>;
|
|
8
|
+
/** Codes that are always worth another ceremony tick, whatever the action says. */
|
|
9
|
+
export declare const RETRYABLE_ENROLLMENT_CODES: ReadonlySet<string>;
|
|
10
|
+
export type EnrollmentDisposition = 'terminal' | 'retry';
|
|
11
|
+
/**
|
|
12
|
+
* Classify an enrollment transport refusal as `(code, disposition)`. The
|
|
13
|
+
* vault's own code wins: a terminal code is terminal even when the client
|
|
14
|
+
* says retry, queue saturation is retryable even when it says gone, and any
|
|
15
|
+
* other code follows the client's action. Errors that are not producer
|
|
16
|
+
* refusals return undefined and are the caller's to rethrow.
|
|
17
|
+
*/
|
|
18
|
+
export declare function classifyEnrollmentError(error: unknown): {
|
|
19
|
+
code: string;
|
|
20
|
+
disposition: EnrollmentDisposition;
|
|
21
|
+
} | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The name a plugin proposes for one host, for example `openai-auth-opencode`.
|
|
24
|
+
* Each host enrolls separately so the operator can revoke one without the other.
|
|
25
|
+
*/
|
|
26
|
+
export declare function enrollmentName(plugin: string, host: string): string;
|
|
27
|
+
export interface ClaustrumEnrollmentClient {
|
|
28
|
+
enrollPropose(input: {
|
|
29
|
+
name: string;
|
|
30
|
+
requestSecretHash: string;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
requestId: string;
|
|
33
|
+
}>;
|
|
34
|
+
enrollPoll(input: {
|
|
35
|
+
requestId: string;
|
|
36
|
+
requestSecret: string;
|
|
37
|
+
}): Promise<EnrollmentPollOutcome>;
|
|
38
|
+
}
|
|
39
|
+
export interface ClaustrumEnrollmentConnection extends ClaustrumEnrollmentClient {
|
|
40
|
+
close(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Connect the enrollment ceremony. Setup calls this; the request path never
|
|
44
|
+
* does. `connectionFile` is required because this library reads no
|
|
45
|
+
* environment and knows no host paths.
|
|
46
|
+
*/
|
|
47
|
+
export declare function connectClaustrumEnrollmentClient(options: ClaustrumEnrollmentClientOptions & {
|
|
48
|
+
connectionFile: string;
|
|
49
|
+
}): Promise<ClaustrumEnrollmentConnection>;
|
|
50
|
+
export type ClaustrumEnrollmentStatus = {
|
|
51
|
+
state: 'idle';
|
|
52
|
+
} | {
|
|
53
|
+
state: 'pending';
|
|
54
|
+
proposedName: string;
|
|
55
|
+
requestId?: string;
|
|
56
|
+
retryCode?: string;
|
|
57
|
+
} | {
|
|
58
|
+
state: 'approved';
|
|
59
|
+
proposedName: string;
|
|
60
|
+
approvedName?: string;
|
|
61
|
+
tokenGeneration: number;
|
|
62
|
+
} | {
|
|
63
|
+
state: 'denied';
|
|
64
|
+
proposedName: string;
|
|
65
|
+
} | {
|
|
66
|
+
state: 'blocked';
|
|
67
|
+
proposedName: string;
|
|
68
|
+
code: string;
|
|
69
|
+
} | {
|
|
70
|
+
state: 'unavailable';
|
|
71
|
+
proposedName: string;
|
|
72
|
+
code: string;
|
|
73
|
+
} | {
|
|
74
|
+
state: 'busy';
|
|
75
|
+
};
|
|
76
|
+
export interface ClaustrumEnrollmentPaths {
|
|
77
|
+
statePath: string;
|
|
78
|
+
tokenPath: string;
|
|
79
|
+
}
|
|
80
|
+
/** The ceremony state file sits next to the token: `x.json` pairs with `x-state.json`. */
|
|
81
|
+
export declare function getClaustrumEnrollmentPaths(tokenPath: string): ClaustrumEnrollmentPaths;
|
|
82
|
+
/**
|
|
83
|
+
* One host's token and state paths. Every host gets its own pair under the
|
|
84
|
+
* plugin's state directory, so an OpenCode enrollment and a Pi enrollment can
|
|
85
|
+
* be approved and revoked independently. A plugin-resolved override replaces
|
|
86
|
+
* the default token path; a relative override resolves against `cwd`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function hostEnrollmentPaths(input: {
|
|
89
|
+
stateDir: string;
|
|
90
|
+
host: string;
|
|
91
|
+
override?: string;
|
|
92
|
+
cwd?: string;
|
|
93
|
+
}): ClaustrumEnrollmentPaths;
|
|
94
|
+
export declare function readClaustrumEnrollmentStatus(paths: ClaustrumEnrollmentPaths, proposedName: string): Promise<ClaustrumEnrollmentStatus>;
|
|
95
|
+
/**
|
|
96
|
+
* Read fresh bearer material for one scoped operation; never publish it.
|
|
97
|
+
* Re-reading per operation is what lets an operator reissue a token on disk.
|
|
98
|
+
*/
|
|
99
|
+
export declare function readClaustrumEnrollmentToken(tokenPath: string): Promise<EnrollmentTokenFile>;
|
|
100
|
+
/**
|
|
101
|
+
* The enrollment ceremony for one host. Run it from setup only: it proposes,
|
|
102
|
+
* polls and persists, and every step can wait on an operator. The request
|
|
103
|
+
* path reads the resulting token and never calls into this class.
|
|
104
|
+
*/
|
|
105
|
+
export declare class ClaustrumEnrollmentManager {
|
|
106
|
+
#private;
|
|
107
|
+
constructor(options: {
|
|
108
|
+
client: ClaustrumEnrollmentClient;
|
|
109
|
+
paths: ClaustrumEnrollmentPaths;
|
|
110
|
+
proposedName: string;
|
|
111
|
+
now?: () => number;
|
|
112
|
+
mintSecret?: () => string;
|
|
113
|
+
writeTokenFile?: typeof writeEnrollmentTokenFile;
|
|
114
|
+
});
|
|
115
|
+
status(): Promise<ClaustrumEnrollmentStatus>;
|
|
116
|
+
resetTerminal(): Promise<ClaustrumEnrollmentResetResult>;
|
|
117
|
+
reconcile(): Promise<ClaustrumEnrollmentStatus>;
|
|
118
|
+
}
|
|
119
|
+
export type ClaustrumEnrollmentResetResult = 'reset' | 'idle' | 'refused-pending' | 'refused-approved' | 'busy';
|
|
120
|
+
/** Reset local terminal metadata without connecting to the credential daemon. */
|
|
121
|
+
export declare function resetClaustrumEnrollmentState(paths: ClaustrumEnrollmentPaths, proposedName: string): Promise<ClaustrumEnrollmentResetResult>;
|