@cortexkit/antigravity-auth-core 3.0.11 → 3.0.13
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/account-selector.d.ts +9 -1
- package/dist/account-selector.d.ts.map +1 -1
- package/dist/account-selector.js +23 -8
- package/dist/account-selector.js.map +1 -1
- package/dist/agy-transport.d.ts.map +1 -1
- package/dist/agy-transport.js +4 -0
- package/dist/agy-transport.js.map +1 -1
- package/dist/common-auth-embedded/claustrum/consumer.d.ts +83 -2
- package/dist/common-auth-embedded/claustrum/consumer.js +337 -37
- package/dist/common-auth-embedded/claustrum/custody.d.ts +36 -1
- package/dist/common-auth-embedded/claustrum/custody.js +95 -11
- package/dist/common-auth-embedded/claustrum/enrollment.js +37 -12
- package/dist/common-auth-embedded/claustrum/errors.d.ts +1 -1
- package/dist/common-auth-embedded/claustrum/hub.d.ts +195 -0
- package/dist/common-auth-embedded/claustrum/hub.js +988 -0
- package/dist/common-auth-embedded/claustrum/index.d.ts +13 -3
- package/dist/common-auth-embedded/claustrum/index.js +37 -4
- package/dist/common-auth-embedded/claustrum/quota-writer.d.ts +8 -1
- package/dist/common-auth-embedded/claustrum/quota-writer.js +38 -66
- package/dist/common-auth-embedded/claustrum/roster.d.ts +80 -1
- package/dist/common-auth-embedded/claustrum/roster.js +259 -12
- package/dist/common-auth-embedded/claustrum/shutdown.js +42 -0
- package/dist/common-auth-embedded/commands/builtins.d.ts +37 -0
- package/dist/common-auth-embedded/commands/builtins.js +95 -13
- package/dist/common-auth-embedded/commands/index.d.ts +1 -1
- package/dist/common-auth-embedded/fs/file-read-cache.js +57 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +20 -11
- package/dist/common-auth-embedded/quota/commit-interval.d.ts +9 -0
- package/dist/common-auth-embedded/quota/commit-interval.js +18 -0
- package/dist/common-auth-embedded/quota/pending.js +48 -0
- package/dist/common-auth-embedded/source-output.json +152 -88
- package/dist/common-auth-embedded/store/attribution.d.ts +5 -0
- package/dist/common-auth-embedded/store/attribution.js +39 -25
- package/dist/common-auth-embedded/store/mutate.d.ts +2 -0
- package/dist/common-auth-embedded/store/mutate.js +4 -0
- package/dist/common-auth-embedded/store/pool.d.ts +22 -0
- package/dist/common-auth-embedded/store/pool.js +18 -3
- package/dist/common-auth-embedded/store/pull.js +3 -1
- package/dist/common-auth-embedded/store/quota-writer.js +312 -0
- package/dist/common-auth-embedded/store/runtime.d.ts +2 -0
- package/dist/common-auth-embedded/store/settings.d.ts +1 -1
- package/dist/common-auth-embedded/store/settings.js +16 -6
- package/dist/model-registry.d.ts +2 -2
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +6 -4
- package/dist/model-registry.js.map +1 -1
- package/dist/pre-response-deadline.d.ts.map +1 -1
- package/dist/pre-response-deadline.js +6 -5
- package/dist/pre-response-deadline.js.map +1 -1
- package/dist/quota-manager.d.ts +1 -1
- package/dist/quota-manager.d.ts.map +1 -1
- package/dist/quota-manager.js +12 -4
- package/dist/quota-manager.js.map +1 -1
- package/dist/quota-presentation.d.ts.map +1 -1
- package/dist/quota-presentation.js +8 -0
- package/dist/quota-presentation.js.map +1 -1
- package/dist/quota-types.d.ts +1 -1
- package/dist/quota-types.d.ts.map +1 -1
- package/dist/quota-types.js +5 -0
- package/dist/quota-types.js.map +1 -1
- package/dist/vault-account-source.d.ts +6 -1
- package/dist/vault-account-source.d.ts.map +1 -1
- package/dist/vault-account-source.js +9 -0
- package/dist/vault-account-source.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { ClaustrumUnreachableError, classifyVaultFailure, } from './availability
|
|
|
4
4
|
import { readClaustrumEnrollmentToken } from './enrollment.js';
|
|
5
5
|
import { ClaustrumConsumerError } from './errors.js';
|
|
6
6
|
import { CUSTODY_PLACEHOLDER_PREFIX } from './host-slot.js';
|
|
7
|
+
import { drainShutdown, shutdownWarning } from './shutdown.js';
|
|
7
8
|
import { ScopedRosterWatch, } from './watch.js';
|
|
8
9
|
/** Only a new version of the same account may replace an in-flight 401. */
|
|
9
10
|
export function isScopedCredentialRotation(served, current) {
|
|
@@ -79,15 +80,56 @@ function accessTokenFromMaterial(material) {
|
|
|
79
80
|
return access;
|
|
80
81
|
}
|
|
81
82
|
function credentialTypeOf(row, family) {
|
|
83
|
+
const notMine = { kind: 'not-mine' };
|
|
82
84
|
if (!row.operations.includes('read'))
|
|
83
|
-
return
|
|
85
|
+
return notMine;
|
|
84
86
|
if (!row.categories.includes(family.category))
|
|
85
|
-
return
|
|
87
|
+
return notMine;
|
|
88
|
+
if (row.credentialType !== 'oauth' && row.credentialType !== 'api_key')
|
|
89
|
+
return notMine;
|
|
90
|
+
if ('classify' in family) {
|
|
91
|
+
// Older clients omit these facts entirely; do not call a policy that needs them.
|
|
92
|
+
const facts = {
|
|
93
|
+
credentialId: row.id,
|
|
94
|
+
credentialType: row.credentialType,
|
|
95
|
+
categories: row.categories,
|
|
96
|
+
refreshAdapter: row.refreshAdapter,
|
|
97
|
+
...rowFactsOf(row),
|
|
98
|
+
};
|
|
99
|
+
if (facts.providerIds === undefined)
|
|
100
|
+
return { kind: 'refused', code: 'unmapped' };
|
|
101
|
+
try {
|
|
102
|
+
const result = family.classify(facts);
|
|
103
|
+
if (result.kind === 'not-mine')
|
|
104
|
+
return notMine;
|
|
105
|
+
if (result.kind === 'mine' && typeof result.member === 'string')
|
|
106
|
+
return { kind: 'mine', type: row.credentialType, member: result.member };
|
|
107
|
+
if (result.kind === 'refused' &&
|
|
108
|
+
typeof result.code === 'string' &&
|
|
109
|
+
/^[a-z][a-z0-9-]{0,31}$/.test(result.code) &&
|
|
110
|
+
!result.code.includes('\n') &&
|
|
111
|
+
family.refusalCodes.includes(result.code))
|
|
112
|
+
return { kind: 'refused', code: result.code };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// A classifier error may contain vault data; expose only a fixed code.
|
|
116
|
+
}
|
|
117
|
+
return { kind: 'refused', code: 'unclassified' };
|
|
118
|
+
}
|
|
86
119
|
if (row.credentialType === 'oauth')
|
|
87
|
-
return row.refreshAdapter === family.refreshAdapter
|
|
120
|
+
return row.refreshAdapter === family.refreshAdapter
|
|
121
|
+
? { kind: 'mine', type: 'oauth' }
|
|
122
|
+
: notMine;
|
|
88
123
|
if (row.credentialType === 'api_key' && family.apiKeys)
|
|
89
|
-
return row.refreshAdapter === undefined
|
|
90
|
-
|
|
124
|
+
return row.refreshAdapter === undefined
|
|
125
|
+
? { kind: 'mine', type: 'api_key' }
|
|
126
|
+
: notMine;
|
|
127
|
+
return notMine;
|
|
128
|
+
}
|
|
129
|
+
/** Structural access keeps the public facts independent of client declarations. */
|
|
130
|
+
function rowFactsOf(row) {
|
|
131
|
+
const metadata = row;
|
|
132
|
+
return { providerIds: metadata.providerIds, authMethod: metadata.authMethod };
|
|
91
133
|
}
|
|
92
134
|
/**
|
|
93
135
|
* The grants named in the vault's list reply, or undefined when it names none.
|
|
@@ -136,6 +178,9 @@ export class ClaustrumScopedCustody {
|
|
|
136
178
|
/** The view of the last successful `discover()`, for `watchRoster`. */
|
|
137
179
|
#discoveredView;
|
|
138
180
|
#watches = new Set();
|
|
181
|
+
#tokenReads = new Set();
|
|
182
|
+
#warnShutdown;
|
|
183
|
+
#disposing;
|
|
139
184
|
constructor(options) {
|
|
140
185
|
const tokenPath = options.tokenPath;
|
|
141
186
|
if (options.readToken) {
|
|
@@ -153,6 +198,7 @@ export class ClaustrumScopedCustody {
|
|
|
153
198
|
this.#requireAssertion = options.requireAssertion ?? false;
|
|
154
199
|
this.#now = options.now ?? Date.now;
|
|
155
200
|
this.#logger = options.logger ?? defaultLogger;
|
|
201
|
+
this.#warnShutdown = shutdownWarning(this.#logger);
|
|
156
202
|
this.#setTimeout = options.setTimeoutImpl ?? setTimeout;
|
|
157
203
|
this.#clearTimeout = options.clearTimeoutImpl ?? clearTimeout;
|
|
158
204
|
}
|
|
@@ -220,7 +266,15 @@ export class ClaustrumScopedCustody {
|
|
|
220
266
|
}
|
|
221
267
|
async #token(signal) {
|
|
222
268
|
this.#check(signal);
|
|
223
|
-
const
|
|
269
|
+
const pending = this.#readToken();
|
|
270
|
+
this.#tokenReads.add(pending);
|
|
271
|
+
let value;
|
|
272
|
+
try {
|
|
273
|
+
value = await pending;
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
this.#tokenReads.delete(pending);
|
|
277
|
+
}
|
|
224
278
|
this.#check(signal);
|
|
225
279
|
if (!/^[0-9a-f]{64}$/.test(value.token) ||
|
|
226
280
|
!Number.isSafeInteger(value.token_generation) ||
|
|
@@ -299,17 +353,32 @@ export class ClaustrumScopedCustody {
|
|
|
299
353
|
throw error;
|
|
300
354
|
}
|
|
301
355
|
const skipped = [];
|
|
356
|
+
const refused = [];
|
|
357
|
+
let notMine = 0;
|
|
302
358
|
const candidates = [];
|
|
303
359
|
const counts = new Map();
|
|
304
360
|
for (const row of inventory.rows) {
|
|
305
|
-
const
|
|
306
|
-
if (
|
|
361
|
+
const classified = credentialTypeOf(row, this.#family);
|
|
362
|
+
if (classified.kind === 'not-mine') {
|
|
363
|
+
notMine++;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (classified.kind === 'refused') {
|
|
367
|
+
refused.push({
|
|
368
|
+
...(row.id.trim() && { credentialId: row.id }),
|
|
369
|
+
code: classified.code,
|
|
370
|
+
});
|
|
307
371
|
continue;
|
|
308
|
-
|
|
372
|
+
}
|
|
373
|
+
candidates.push({
|
|
374
|
+
row,
|
|
375
|
+
type: classified.type,
|
|
376
|
+
...(classified.member !== undefined && { member: classified.member }),
|
|
377
|
+
});
|
|
309
378
|
counts.set(row.id, (counts.get(row.id) ?? 0) + 1);
|
|
310
379
|
}
|
|
311
380
|
const credentials = [];
|
|
312
|
-
for (const { row, type } of candidates) {
|
|
381
|
+
for (const { row, type, member } of candidates) {
|
|
313
382
|
let reason;
|
|
314
383
|
if (!row.id.trim())
|
|
315
384
|
reason = 'empty credential id';
|
|
@@ -320,12 +389,17 @@ export class ClaustrumScopedCustody {
|
|
|
320
389
|
else if (!row.state.trim())
|
|
321
390
|
reason = 'empty state';
|
|
322
391
|
if (reason) {
|
|
323
|
-
skipped.push({
|
|
392
|
+
skipped.push({
|
|
393
|
+
...(row.id.trim() && { credentialId: row.id }),
|
|
394
|
+
...(member !== undefined && { member }),
|
|
395
|
+
reason,
|
|
396
|
+
});
|
|
324
397
|
continue;
|
|
325
398
|
}
|
|
326
399
|
credentials.push(Object.freeze({
|
|
327
400
|
credentialId: row.id,
|
|
328
401
|
credentialType: type,
|
|
402
|
+
...(member !== undefined && { member }),
|
|
329
403
|
...(row.accountId !== undefined && {
|
|
330
404
|
accountIdentity: row.accountId,
|
|
331
405
|
}),
|
|
@@ -341,6 +415,10 @@ export class ClaustrumScopedCustody {
|
|
|
341
415
|
view: inventory.view,
|
|
342
416
|
credentials: Object.freeze(credentials),
|
|
343
417
|
skipped: Object.freeze(skipped),
|
|
418
|
+
...('classify' in this.#family && {
|
|
419
|
+
refused: Object.freeze(refused),
|
|
420
|
+
notMine,
|
|
421
|
+
}),
|
|
344
422
|
...(grantReach && { grantReach }),
|
|
345
423
|
};
|
|
346
424
|
}
|
|
@@ -646,4 +724,10 @@ export class ClaustrumScopedCustody {
|
|
|
646
724
|
this.#kept.clear();
|
|
647
725
|
this.#client.close();
|
|
648
726
|
}
|
|
727
|
+
/** Stop immediately; allow token reads 2 s and share an owner's timeout warning. */
|
|
728
|
+
async dispose(onTimeout = this.#warnShutdown) {
|
|
729
|
+
this.close();
|
|
730
|
+
this.#disposing ??= drainShutdown(this.#tokenReads, onTimeout);
|
|
731
|
+
await this.#disposing;
|
|
732
|
+
}
|
|
649
733
|
}
|
|
@@ -3,6 +3,7 @@ import { constants as fsConstants } from 'node:fs';
|
|
|
3
3
|
import { chmod, mkdir, open, readFile, realpath, rename, stat, unlink, } from 'node:fs/promises';
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, resolve, } from 'node:path';
|
|
5
5
|
import { ClaustrumCredentialError, ClaustrumClient as ClaustrumWireClient, writeEnrollmentTokenFile, } from '@cortexkit/claustrum-client';
|
|
6
|
+
import { FileReadCache } from '../fs/file-read-cache.js';
|
|
6
7
|
import { acquireRefreshFileLock } from '../fs/index.js';
|
|
7
8
|
import { ensurePrivateDirectory as ensureWindowsDirectory, PrivateFileError, readPrivateText, verifySafeDirectory, windowsPrivateFiles, writePrivateAtomic, } from '../fs/private-files.js';
|
|
8
9
|
import { ClaustrumConsumerError } from './errors.js';
|
|
@@ -402,32 +403,56 @@ async function writeStateAtomic(path, state, ancestorOptions = {}) {
|
|
|
402
403
|
await unlink(temporary).catch(() => { });
|
|
403
404
|
}
|
|
404
405
|
}
|
|
406
|
+
const enrollmentStatusCache = new FileReadCache();
|
|
405
407
|
export async function readClaustrumEnrollmentStatus(paths, proposedName) {
|
|
408
|
+
// Windows ACL, owner and ancestry checks are not represented by stat identity.
|
|
409
|
+
// Keep the native private-file validation and full read on every call there.
|
|
410
|
+
const result = windowsPrivateFiles
|
|
411
|
+
? await readEnrollmentStatus(paths, proposedName)
|
|
412
|
+
: await enrollmentStatusCache.read([paths.tokenPath, paths.statePath], async ([tokenPath = paths.tokenPath, statePath = paths.statePath]) => readEnrollmentStatus({ tokenPath, statePath }, proposedName), (identities) => {
|
|
413
|
+
// Recheck the current effective user as well as mode and regular-file kind.
|
|
414
|
+
// lstat probes also prevent a link to the old inode from becoming a hit.
|
|
415
|
+
for (const metadata of identities)
|
|
416
|
+
if (metadata !== 'absent')
|
|
417
|
+
validateReadableSecretFile(metadata);
|
|
418
|
+
});
|
|
419
|
+
const status = { ...result.status };
|
|
420
|
+
if (result.usesProposedName && status.state === 'approved')
|
|
421
|
+
status.proposedName = proposedName;
|
|
422
|
+
return status;
|
|
423
|
+
}
|
|
424
|
+
async function readEnrollmentStatus(paths, proposedName) {
|
|
406
425
|
const tokenValue = await readBoundedJson(paths.tokenPath);
|
|
407
426
|
const token = tokenValue === undefined ? undefined : decodeTokenFile(tokenValue);
|
|
408
427
|
const stateValue = await readBoundedJson(paths.statePath);
|
|
409
428
|
const state = stateValue === undefined ? undefined : decodeEnrollmentState(stateValue);
|
|
410
429
|
if (token) {
|
|
411
430
|
return {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
state
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
431
|
+
usesProposedName: state === undefined,
|
|
432
|
+
status: {
|
|
433
|
+
state: 'approved',
|
|
434
|
+
proposedName: state?.proposedName ?? proposedName,
|
|
435
|
+
...(state?.phase === 'approved' &&
|
|
436
|
+
state.approvedName !== undefined && {
|
|
437
|
+
approvedName: state.approvedName,
|
|
438
|
+
}),
|
|
439
|
+
tokenGeneration: token.token_generation,
|
|
440
|
+
},
|
|
419
441
|
};
|
|
420
442
|
}
|
|
421
443
|
if (!state)
|
|
422
|
-
return { state: 'idle' };
|
|
444
|
+
return { status: { state: 'idle' }, usesProposedName: false };
|
|
423
445
|
if (state.phase === 'approved') {
|
|
424
446
|
return {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
447
|
+
usesProposedName: false,
|
|
448
|
+
status: {
|
|
449
|
+
state: 'blocked',
|
|
450
|
+
proposedName: state.proposedName,
|
|
451
|
+
code: 'missing_token',
|
|
452
|
+
},
|
|
428
453
|
};
|
|
429
454
|
}
|
|
430
|
-
return statusFromState(state);
|
|
455
|
+
return { status: statusFromState(state), usesProposedName: false };
|
|
431
456
|
}
|
|
432
457
|
/**
|
|
433
458
|
* Resolve the host's account mode from its enrollment files. It never changes
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `code`, a `class` and an `action`), so callers can tell the vault's verdict
|
|
5
5
|
* apart from a check made on this side.
|
|
6
6
|
*/
|
|
7
|
-
export type ClaustrumConsumerFailureKind = 'closed' | 'not-enrolled' | 'invalid-token' | 'unavailable' | 'identity-changed' | 'identity-unasserted' | 'insufficient-validity' | 'invalid-material' | 'not-active' | 'route-unavailable' | 'route-declined' | 'no-receipt' | 'unsafe-file' | 'invalid-state' | 'wrong-consumer' | 'roster-busy' | 'host-slot-login' | 'host-slot-placeholder' | 'placeholder-refresh';
|
|
7
|
+
export type ClaustrumConsumerFailureKind = 'closed' | 'not-enrolled' | 'invalid-token' | 'unavailable' | 'identity-changed' | 'identity-unasserted' | 'insufficient-validity' | 'invalid-material' | 'not-active' | 'route-unavailable' | 'route-declined' | 'no-receipt' | 'unsafe-file' | 'invalid-state' | 'wrong-consumer' | 'roster-busy' | 'host-slot-login' | 'host-slot-placeholder' | 'placeholder-refresh' | 'invalid-order';
|
|
8
8
|
export declare class ClaustrumConsumerError extends Error {
|
|
9
9
|
readonly kind: ClaustrumConsumerFailureKind;
|
|
10
10
|
constructor(kind: ClaustrumConsumerFailureKind, message: string);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { QuotaObservation } from '../quota/index.js';
|
|
2
|
+
import { type ClaustrumConsumer } from './consumer.js';
|
|
3
|
+
import { type ClassifyingClaustrumFamily, type ClaustrumScopedClient, ClaustrumScopedCustody, type IdentityParser, type KeptReceiptState, type VaultGrantReach } from './custody.js';
|
|
4
|
+
import { type ClaustrumLogger } from './errors.js';
|
|
5
|
+
import { type AccountMapper, type VaultRosterFile } from './roster.js';
|
|
6
|
+
import { type WatchMode } from './watch.js';
|
|
7
|
+
export interface ClaustrumDiscoveryHubOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The plugin's owner-only state directory. Member rosters live under
|
|
10
|
+
* `<stateDir>/rosters/<hostGeneration>/<fingerprint>/<provider>.json`.
|
|
11
|
+
*/
|
|
12
|
+
stateDir: string;
|
|
13
|
+
/**
|
|
14
|
+
* Which host generation this process runs (for example `opencode1`,
|
|
15
|
+
* `opencode2` or `pi`). Hosts that classify rows differently keep separate
|
|
16
|
+
* roster directories.
|
|
17
|
+
*/
|
|
18
|
+
hostGeneration: string;
|
|
19
|
+
/**
|
|
20
|
+
* Everything the plugin's classifier decides with, as plain JSON: the
|
|
21
|
+
* exact object it hands the classifier (its version, the supported-table
|
|
22
|
+
* entries, the reserved provider set and every host fact it reads). The
|
|
23
|
+
* hub hashes it, with the member set, `routePrefix` and `mapAccount`, into
|
|
24
|
+
* the roster key, so processes whose classifiers or projections could
|
|
25
|
+
* disagree never share a roster file. Configuration that `mapAccount`
|
|
26
|
+
* reads, beyond its own source text, belongs here too.
|
|
27
|
+
*/
|
|
28
|
+
classifierInput: unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Every provider the plugin serves on this host, fixed for the hub's life.
|
|
31
|
+
* Each is the `member` a classified row names and a roster file name, so
|
|
32
|
+
* it is lowercase ASCII: letters, digits, `.`, `_` and `-`.
|
|
33
|
+
*/
|
|
34
|
+
members: readonly string[];
|
|
35
|
+
/** Prefix of every member's vault route ids; part of the roster key. */
|
|
36
|
+
routePrefix?: string;
|
|
37
|
+
/**
|
|
38
|
+
* How every member presents a vault credential (the credential names its
|
|
39
|
+
* member). Part of the roster key by its source text.
|
|
40
|
+
*/
|
|
41
|
+
mapAccount?: AccountMapper;
|
|
42
|
+
/** This host's enrollment token, written by setup. */
|
|
43
|
+
tokenPath: string;
|
|
44
|
+
/**
|
|
45
|
+
* The plugin's classifying family. A legacy `ClaustrumFamily` names no
|
|
46
|
+
* member for any row and is refused.
|
|
47
|
+
*/
|
|
48
|
+
family: ClassifyingClaustrumFamily<string>;
|
|
49
|
+
connect: () => Promise<ClaustrumScopedClient>;
|
|
50
|
+
/**
|
|
51
|
+
* How long one `connect()` attempt may take before the hub treats the vault
|
|
52
|
+
* as unreachable (default 10 s, as `ClaustrumConsumerOptions`). The hub's
|
|
53
|
+
* connect is shared by every member waiting for a lease, so an attempt that
|
|
54
|
+
* never settles would hold every one of them; after the limit they get an
|
|
55
|
+
* unreachable error and the next acquire connects again.
|
|
56
|
+
*/
|
|
57
|
+
connectTimeoutMs?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the plugin takes its accounts from the vault. Read from local
|
|
60
|
+
* state only, never from the vault: on every hub tick, and when a member
|
|
61
|
+
* refreshes or authorizes. The first false read revokes every lease,
|
|
62
|
+
* closes the connection and clears every member.
|
|
63
|
+
*/
|
|
64
|
+
isCustodyActive?: () => boolean | Promise<boolean>;
|
|
65
|
+
parseIdentity?: IdentityParser;
|
|
66
|
+
/** As `ClaustrumConsumerOptions.requireAssertion`, for every member. */
|
|
67
|
+
requireAssertion?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* How often the hub ticks (default 5 s). A tick reads authority, installs
|
|
70
|
+
* member roster files another writer changed under the same view, and
|
|
71
|
+
* lists when a list is due: on every tick while no vault watch is open,
|
|
72
|
+
* when a pending member's retry is due (a member the last list did not
|
|
73
|
+
* commit, retried with a backoff), otherwise every five minutes as a
|
|
74
|
+
* safety net.
|
|
75
|
+
*/
|
|
76
|
+
pollIntervalMs?: number;
|
|
77
|
+
setTimeoutImpl?: typeof setTimeout;
|
|
78
|
+
clearTimeoutImpl?: typeof clearTimeout;
|
|
79
|
+
now?: () => number;
|
|
80
|
+
logger?: ClaustrumLogger;
|
|
81
|
+
/**
|
|
82
|
+
* Errors of the hub's own tick, list and watch. A failed list and a vault
|
|
83
|
+
* refusal of the watch also reach every member's `onError`.
|
|
84
|
+
*/
|
|
85
|
+
onError?: (error: unknown) => void;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What one hub member takes: only what is per provider. The hub owns the
|
|
89
|
+
* roster path, the enrollment token, the family, the connection, the
|
|
90
|
+
* authority check, the poll timer and the projection inputs that are part of
|
|
91
|
+
* the roster key (`routePrefix`, `mapAccount`), so none of them can be given
|
|
92
|
+
* here. The type parameter matches the consumer `member` returns; members
|
|
93
|
+
* have no insula source.
|
|
94
|
+
*/
|
|
95
|
+
export interface ClaustrumHubMemberOptions<_Q = QuotaObservation> {
|
|
96
|
+
/**
|
|
97
|
+
* Ids of the plugin's local pool rows; no vault route id may equal one.
|
|
98
|
+
* Read on every projection and install, so it may change at any time.
|
|
99
|
+
*/
|
|
100
|
+
reservedRouteIds?: () => Iterable<string>;
|
|
101
|
+
/** As `ClaustrumConsumerOptions.onRoster`: once per change of view. */
|
|
102
|
+
onRoster?: (roster: VaultRosterFile) => void;
|
|
103
|
+
/** This member's own failures, such as a roster commit that failed. */
|
|
104
|
+
onError?: (error: unknown) => void;
|
|
105
|
+
logger?: ClaustrumLogger;
|
|
106
|
+
}
|
|
107
|
+
/** What a lease may do with the shared custody. */
|
|
108
|
+
export type ClaustrumLeasedCustody = Pick<ClaustrumScopedCustody, 'authorize' | 'reportFailure' | 'forget' | 'keptReceiptState'>;
|
|
109
|
+
/**
|
|
110
|
+
* One acquisition of the hub's shared custody. `custody` throws `closed`
|
|
111
|
+
* after `release()`, and once the connection it belongs to ended for any
|
|
112
|
+
* reason, vault mode going off included. `release()` is idempotent.
|
|
113
|
+
*/
|
|
114
|
+
export interface CustodyLease {
|
|
115
|
+
readonly custody: ClaustrumLeasedCustody;
|
|
116
|
+
/** The connection this lease belongs to; each new connection counts up. */
|
|
117
|
+
readonly generation: number;
|
|
118
|
+
release(): void;
|
|
119
|
+
}
|
|
120
|
+
/** The rows the hub's last complete discovery refused or left to others. */
|
|
121
|
+
export interface ClaustrumHubRefusals {
|
|
122
|
+
readonly view: string;
|
|
123
|
+
readonly refused: readonly {
|
|
124
|
+
credentialId?: string;
|
|
125
|
+
code: string;
|
|
126
|
+
}[];
|
|
127
|
+
readonly notMine: number;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One discovery hub per host process: the only vault connection and the only
|
|
131
|
+
* `ClaustrumScopedCustody`, one vault watch and one list per refresh, fanned
|
|
132
|
+
* out to one roster per provider. Members (`member()`) are ordinary
|
|
133
|
+
* `ClaustrumConsumer`s that never connect, list or watch themselves; each
|
|
134
|
+
* holds a custody lease. The connection stays open while vault mode is on,
|
|
135
|
+
* because the hub's own discovery holds a lease of its own, and closes when
|
|
136
|
+
* the last lease of a connection is released, when vault mode goes off, or
|
|
137
|
+
* at `close()`.
|
|
138
|
+
*/
|
|
139
|
+
export declare class ClaustrumDiscoveryHub {
|
|
140
|
+
#private;
|
|
141
|
+
constructor(options: ClaustrumDiscoveryHubOptions);
|
|
142
|
+
/** The roster file member `provider` reads and the hub commits. */
|
|
143
|
+
rosterPath(provider: string): string;
|
|
144
|
+
/**
|
|
145
|
+
* Build the consumer for `provider`, once. It is an ordinary
|
|
146
|
+
* `ClaustrumConsumer` whose roster path is `rosterPath(provider)`; it
|
|
147
|
+
* connects, lists and watches only through this hub.
|
|
148
|
+
*/
|
|
149
|
+
member<Q = QuotaObservation>(provider: string, options: ClaustrumHubMemberOptions<Q>): ClaustrumConsumer<Q>;
|
|
150
|
+
/**
|
|
151
|
+
* Take a lease on the current connection, connecting first when there is
|
|
152
|
+
* none. The acquirer's count is taken before anything is awaited, so a
|
|
153
|
+
* release elsewhere meanwhile cannot end the connection under it. An abort
|
|
154
|
+
* or a failed connect gives that count back; an acquirer whose connection
|
|
155
|
+
* ended while it waited tries once more on the current one. Rejects with
|
|
156
|
+
* `not-active` while vault mode is off.
|
|
157
|
+
*/
|
|
158
|
+
acquire(signal?: AbortSignal): Promise<CustodyLease>;
|
|
159
|
+
/** The current connection and its lease count, for status and tests. */
|
|
160
|
+
leaseState(): {
|
|
161
|
+
readonly generation?: number;
|
|
162
|
+
readonly count: number;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Start the hub's tick, which runs until `close()`. While vault mode is
|
|
166
|
+
* off a tick only reads authority and makes no vault request.
|
|
167
|
+
*/
|
|
168
|
+
start(): void;
|
|
169
|
+
/**
|
|
170
|
+
* List once and commit every member, as a tick does. Resolves once every
|
|
171
|
+
* member's memory holds its new roster. Rejects when the list fails; a
|
|
172
|
+
* member's own commit error goes to that member's `onError`.
|
|
173
|
+
*/
|
|
174
|
+
refresh(): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Rows the last discovery every member committed refused, with their fixed
|
|
177
|
+
* codes, and how many rows it left to other plugins. Undefined while
|
|
178
|
+
* unknown: before a complete discovery, and while vault mode is off.
|
|
179
|
+
*/
|
|
180
|
+
refusals(): ClaustrumHubRefusals | undefined;
|
|
181
|
+
/** The grant reach of the same discovery as `refusals()`. */
|
|
182
|
+
grantReach(): VaultGrantReach | undefined;
|
|
183
|
+
/** How the hub learns about changes; `polling` until a watch is open. */
|
|
184
|
+
watchState(): WatchMode;
|
|
185
|
+
/** The shared custody's kept receipts, host-wide. */
|
|
186
|
+
keptReceiptState(): KeptReceiptState;
|
|
187
|
+
/**
|
|
188
|
+
* Stop the tick and the watch and close the connection. Members are not
|
|
189
|
+
* closed; they fail `closed` from then on. Use `dispose()` to also allow
|
|
190
|
+
* in-flight discovery up to two seconds to release its file handles.
|
|
191
|
+
*/
|
|
192
|
+
close(): void;
|
|
193
|
+
/** Close immediately, then join background work for at most two seconds. */
|
|
194
|
+
dispose(): Promise<void>;
|
|
195
|
+
}
|