@cortexkit/antigravity-auth-core 3.0.8 → 3.0.10
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/common-auth-embedded/auth-menu/login.d.ts +6 -10
- package/dist/common-auth-embedded/auth-menu/login.js +14 -11
- package/dist/common-auth-embedded/claustrum/consumer.d.ts +8 -2
- package/dist/common-auth-embedded/claustrum/consumer.js +64 -21
- package/dist/common-auth-embedded/claustrum/index.d.ts +2 -1
- package/dist/common-auth-embedded/claustrum/index.js +1 -0
- package/dist/common-auth-embedded/claustrum/insula.d.ts +23 -0
- package/dist/common-auth-embedded/claustrum/insula.js +64 -2
- package/dist/common-auth-embedded/claustrum/quota-writer.d.ts +18 -0
- package/dist/common-auth-embedded/claustrum/quota-writer.js +253 -0
- package/dist/common-auth-embedded/claustrum/roster.d.ts +18 -2
- package/dist/common-auth-embedded/claustrum/roster.js +100 -43
- package/dist/common-auth-embedded/fs/with-lock.d.ts +1 -0
- package/dist/common-auth-embedded/fs/with-lock.js +2 -0
- package/dist/common-auth-embedded/quota/format.d.ts +19 -0
- package/dist/common-auth-embedded/quota/format.js +50 -5
- package/dist/common-auth-embedded/quota/index.d.ts +3 -3
- package/dist/common-auth-embedded/quota/index.js +2 -2
- package/dist/common-auth-embedded/quota/map.d.ts +24 -1
- package/dist/common-auth-embedded/quota/map.js +31 -6
- package/dist/common-auth-embedded/quota/merge.d.ts +6 -1
- package/dist/common-auth-embedded/quota/merge.js +17 -3
- package/dist/common-auth-embedded/quota/projection.d.ts +3 -1
- package/dist/common-auth-embedded/quota/projection.js +3 -1
- package/dist/common-auth-embedded/source-output.json +89 -73
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -1
- package/dist/pre-response-deadline.d.ts.map +1 -1
- package/dist/pre-response-deadline.js +5 -1
- package/dist/pre-response-deadline.js.map +1 -1
- package/dist/vault-account-source.d.ts +14 -2
- package/dist/vault-account-source.d.ts.map +1 -1
- package/dist/vault-account-source.js +38 -0
- package/dist/vault-account-source.js.map +1 -1
- package/package.json +1 -1
|
@@ -26,28 +26,24 @@ export interface MenuLogin {
|
|
|
26
26
|
signal?: AbortSignal;
|
|
27
27
|
}): Promise<LoginFlow>;
|
|
28
28
|
/**
|
|
29
|
-
* Opens the URL in a browser; false or a throw means
|
|
30
|
-
*
|
|
29
|
+
* Opens the URL in a browser; false or a throw means its opener could not be
|
|
30
|
+
* started. Defaults to `openBrowserForMenu`.
|
|
31
31
|
*/
|
|
32
32
|
openBrowser?(url: string): boolean | undefined | Promise<boolean | undefined>;
|
|
33
33
|
}
|
|
34
|
-
type BrowserExec = (file: string, args: string[], options: {
|
|
35
|
-
stdio: 'ignore';
|
|
36
|
-
timeout: number;
|
|
37
|
-
}) => unknown;
|
|
38
34
|
type BrowserSpawn = (file: string, args: string[], options: {
|
|
39
35
|
stdio: 'ignore';
|
|
40
|
-
windowsHide
|
|
36
|
+
windowsHide?: true;
|
|
41
37
|
}) => {
|
|
42
38
|
pid?: number;
|
|
43
39
|
unref(): void;
|
|
44
40
|
on(event: 'error', listener: () => void): unknown;
|
|
45
41
|
};
|
|
46
|
-
/** Opens a URL with the platform's opener; false when
|
|
47
|
-
export declare function openBrowserForMenu(url: string, platform?: NodeJS.Platform,
|
|
42
|
+
/** Opens a URL with the platform's opener; false when it could not start. */
|
|
43
|
+
export declare function openBrowserForMenu(url: string, platform?: NodeJS.Platform, spawn?: BrowserSpawn): boolean;
|
|
48
44
|
/**
|
|
49
45
|
* Runs a login for the menu: a browser login first and, when no browser can
|
|
50
|
-
* be
|
|
46
|
+
* be started, a device-code login instead, so a headless machine can still
|
|
51
47
|
* add an account. The URL is always printed, so an operator can open it by
|
|
52
48
|
* hand when the opener reports success but nothing appears.
|
|
53
49
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/** Opens a URL with the platform's opener; false when
|
|
3
|
-
export function openBrowserForMenu(url, platform = process.platform,
|
|
1
|
+
import { spawn as defaultSpawn } from 'node:child_process';
|
|
2
|
+
/** Opens a URL with the platform's opener; false when it could not start. */
|
|
3
|
+
export function openBrowserForMenu(url, platform = process.platform, spawn = defaultSpawn) {
|
|
4
4
|
try {
|
|
5
5
|
if (platform === 'win32') {
|
|
6
6
|
// Start-Process opens the URL with its associated app. Base64 UTF-16LE
|
|
@@ -28,15 +28,18 @@ export function openBrowserForMenu(url, platform = process.platform, execFileSyn
|
|
|
28
28
|
// Node leaves pid undefined when the process could not be started.
|
|
29
29
|
return child.pid !== undefined;
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
31
|
+
const child = spawn(platform === 'darwin' ? 'open' : 'xdg-open', [url], {
|
|
32
|
+
stdio: 'ignore',
|
|
33
|
+
});
|
|
34
|
+
// A failed launch reports asynchronously; without a listener it would
|
|
35
|
+
// become an uncaught exception after this function has returned.
|
|
36
|
+
child.on('error', () => { });
|
|
37
|
+
child.unref();
|
|
38
|
+
// The URL handler's process may outlive the caller; a pid means launch began.
|
|
39
|
+
return child.pid !== undefined;
|
|
38
40
|
}
|
|
39
41
|
catch {
|
|
42
|
+
// Browser launch is best effort; the printed URL remains the fallback.
|
|
40
43
|
return false;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
@@ -53,7 +56,7 @@ function printFlow(context, flow) {
|
|
|
53
56
|
}
|
|
54
57
|
/**
|
|
55
58
|
* Runs a login for the menu: a browser login first and, when no browser can
|
|
56
|
-
* be
|
|
59
|
+
* be started, a device-code login instead, so a headless machine can still
|
|
57
60
|
* add an account. The URL is always printed, so an operator can open it by
|
|
58
61
|
* hand when the opener reports success but nothing appears.
|
|
59
62
|
*/
|
|
@@ -3,7 +3,7 @@ import type { QuotaObservation } from '../quota/index.js';
|
|
|
3
3
|
import type { RoutingRow } from '../routing/index.js';
|
|
4
4
|
import { type ClaustrumFamily, type ClaustrumScopedAttempt, type ClaustrumScopedClient, type IdentityParser, type KeptReceiptState, type VaultGrantReach } from './custody.js';
|
|
5
5
|
import { type ClaustrumLogger } from './errors.js';
|
|
6
|
-
import { type InsulaQuotaOptions } from './insula.js';
|
|
6
|
+
import { type InsulaQuotaOptions, type InsulaQuotaResult } from './insula.js';
|
|
7
7
|
import { type AccountMapper, type QuotaReceipt, type VaultRosterFile } from './roster.js';
|
|
8
8
|
import { type WatchMode } from './watch.js';
|
|
9
9
|
export interface ClaustrumConsumerOptions<Q = QuotaObservation> {
|
|
@@ -134,6 +134,10 @@ export declare class ClaustrumConsumer<Q = QuotaObservation> {
|
|
|
134
134
|
* A route without a known provider account cannot join an insula entry.
|
|
135
135
|
*/
|
|
136
136
|
cachedInsulaQuotaForRoute(routeId: string): Promise<Q | undefined>;
|
|
137
|
+
/** Cache-only background lookup, preserving the reason a route has no reading. */
|
|
138
|
+
lookupInsulaQuotaForRoute(routeId: string): Promise<InsulaQuotaResult<Q>>;
|
|
139
|
+
/** Skip the fallback poll while insula is cold or its upstream fetch failed. */
|
|
140
|
+
pollInsulaQuotaForRoute(routeId: string, fallback: () => Promise<Q | undefined>): Promise<Q | undefined>;
|
|
137
141
|
/**
|
|
138
142
|
* Authorize one physical send on a vault route. The roster file is re-read
|
|
139
143
|
* first, so a decline committed by another process applies before the next
|
|
@@ -165,8 +169,10 @@ export declare class ClaustrumConsumer<Q = QuotaObservation> {
|
|
|
165
169
|
* a served credential (an insula reading from `cachedInsulaQuotaForRoute`).
|
|
166
170
|
* It lands only while the route is still bound to that known account, so no
|
|
167
171
|
* credential fetch is needed and a replaced account's reading is dropped.
|
|
168
|
-
*
|
|
172
|
+
* Memory updates immediately; durable publication is detached and coalesced.
|
|
169
173
|
*/
|
|
170
174
|
recordAccountQuota(routeId: string, accountIdentity: string, observation: QuotaObservation): Promise<boolean>;
|
|
171
175
|
close(): void;
|
|
176
|
+
/** Close immediately; the last consumer also waits for the writer's bounded drain. */
|
|
177
|
+
dispose(): Promise<void>;
|
|
172
178
|
}
|
|
@@ -3,8 +3,9 @@ import { combineAbortSignals } from '../util/any-signal.js';
|
|
|
3
3
|
import { ClaustrumScopedCustody, decideScopedRetryAfter401, } from './custody.js';
|
|
4
4
|
import { readClaustrumEnrollmentToken } from './enrollment.js';
|
|
5
5
|
import { ClaustrumConsumerError } from './errors.js';
|
|
6
|
-
import { acquireInsulaQuotaSource } from './insula.js';
|
|
7
|
-
import {
|
|
6
|
+
import { acquireInsulaQuotaSource, } from './insula.js';
|
|
7
|
+
import { pendingVaultQuota, retainVaultQuotaWriter } from './quota-writer.js';
|
|
8
|
+
import { acceptVaultRoute, applyVaultQuota, declineVaultRoute, forgetPendingVaultDiscovery, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, vaultRoutingRows, } from './roster.js';
|
|
8
9
|
import { SAFETY_POLL_MS } from './watch.js';
|
|
9
10
|
/**
|
|
10
11
|
* One plugin host's view of its vault accounts. It turns vault credentials
|
|
@@ -32,11 +33,16 @@ export class ClaustrumConsumer {
|
|
|
32
33
|
#insula;
|
|
33
34
|
#vaultWatch;
|
|
34
35
|
#releaseInsulaWatch;
|
|
36
|
+
#insulaSkips = new Map();
|
|
35
37
|
/** When this instance last completed a discovery, by `now`. */
|
|
36
38
|
#discoveredAt;
|
|
39
|
+
#releaseQuotaWriter;
|
|
37
40
|
constructor(options) {
|
|
38
41
|
this.#options = options;
|
|
39
42
|
this.#logger = options.logger ?? createLogger('claustrum');
|
|
43
|
+
this.#releaseQuotaWriter = retainVaultQuotaWriter(options.rosterPath, (publication) => {
|
|
44
|
+
this.#roster = applyVaultQuota(this.#roster, publication).roster;
|
|
45
|
+
});
|
|
40
46
|
}
|
|
41
47
|
#assertOpen(signal) {
|
|
42
48
|
this.#shutdown.signal.throwIfAborted();
|
|
@@ -175,11 +181,9 @@ export class ClaustrumConsumer {
|
|
|
175
181
|
let discovered;
|
|
176
182
|
const roster = await refreshVaultRoster({
|
|
177
183
|
path: this.#options.rosterPath,
|
|
178
|
-
custody
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return discovered;
|
|
182
|
-
},
|
|
184
|
+
custody,
|
|
185
|
+
onInventory: (inventory) => {
|
|
186
|
+
discovered = inventory;
|
|
183
187
|
},
|
|
184
188
|
isActive: () => this.#active(),
|
|
185
189
|
signal: this.#shutdown.signal,
|
|
@@ -192,7 +196,7 @@ export class ClaustrumConsumer {
|
|
|
192
196
|
});
|
|
193
197
|
this.#assertOpen();
|
|
194
198
|
const changed = roster?.view !== this.#roster?.view;
|
|
195
|
-
this.#roster = roster;
|
|
199
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, roster);
|
|
196
200
|
custody.retain(roster?.rows ?? []);
|
|
197
201
|
// Each discovery replaces the previous value, including with unknown. A
|
|
198
202
|
// refresh that served a peer's roster discovered nothing and leaves it;
|
|
@@ -348,7 +352,7 @@ export class ClaustrumConsumer {
|
|
|
348
352
|
return undefined;
|
|
349
353
|
try {
|
|
350
354
|
await readClaustrumEnrollmentToken(this.#options.tokenPath);
|
|
351
|
-
const roster = await readVaultRoster(this.#options.rosterPath);
|
|
355
|
+
const roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
352
356
|
const row = roster?.rows.find((entry) => entry.routeId === routeId);
|
|
353
357
|
return this.#normalizeInsulaQuota(row?.accountIdentity);
|
|
354
358
|
}
|
|
@@ -356,6 +360,44 @@ export class ClaustrumConsumer {
|
|
|
356
360
|
return undefined;
|
|
357
361
|
}
|
|
358
362
|
}
|
|
363
|
+
/** Cache-only background lookup, preserving the reason a route has no reading. */
|
|
364
|
+
async lookupInsulaQuotaForRoute(routeId) {
|
|
365
|
+
const options = this.#options.insula;
|
|
366
|
+
if (!options || this.#shutdown.signal.aborted || !(await this.#active()))
|
|
367
|
+
return { miss: 'unreachable' };
|
|
368
|
+
try {
|
|
369
|
+
await readClaustrumEnrollmentToken(this.#options.tokenPath);
|
|
370
|
+
const roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
371
|
+
const account = roster?.rows.find((row) => row.routeId === routeId)?.accountIdentity;
|
|
372
|
+
if (!account || !this.#insula)
|
|
373
|
+
return { miss: 'unreachable' };
|
|
374
|
+
const result = this.#insula.source.lookup(options.provider, account, (this.#options.now ?? Date.now)());
|
|
375
|
+
const reading = result.reading &&
|
|
376
|
+
options.normalize(result.reading.entry, result.reading.context);
|
|
377
|
+
if (result.miss)
|
|
378
|
+
return { miss: result.miss, ...(reading !== undefined && { reading }) };
|
|
379
|
+
// Refusing an echo or unsupported window leaves the plugin's poll in charge.
|
|
380
|
+
return reading === undefined ? { miss: 'unreachable' } : { reading };
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return { miss: 'unreachable' };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** Skip the fallback poll while insula is cold or its upstream fetch failed. */
|
|
387
|
+
async pollInsulaQuotaForRoute(routeId, fallback) {
|
|
388
|
+
const result = await this.lookupInsulaQuotaForRoute(routeId);
|
|
389
|
+
if (result.miss === 'cold' || result.miss === 'upstream-failed') {
|
|
390
|
+
if (this.#insulaSkips.get(routeId) !== result.miss) {
|
|
391
|
+
this.#insulaSkips.set(routeId, result.miss);
|
|
392
|
+
this.#logger.warn('skipped insula provider poll', {
|
|
393
|
+
reason: result.miss,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
this.#insulaSkips.delete(routeId);
|
|
399
|
+
return result.miss ? fallback() : result.reading;
|
|
400
|
+
}
|
|
359
401
|
#normalizeInsulaQuota(accountIdentity) {
|
|
360
402
|
if (!this.#options.insula || !accountIdentity)
|
|
361
403
|
return undefined;
|
|
@@ -380,7 +422,7 @@ export class ClaustrumConsumer {
|
|
|
380
422
|
this.#insula = undefined;
|
|
381
423
|
throw new ClaustrumConsumerError('not-active', 'Claustrum scoped custody is not active');
|
|
382
424
|
}
|
|
383
|
-
const current = await readVaultRoster(this.#options.rosterPath);
|
|
425
|
+
const current = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
384
426
|
this.#assertOpen(signal);
|
|
385
427
|
const observed = roster.rows.find((row) => row.routeId === routeId);
|
|
386
428
|
const configured = current?.rows.find((row) => row.routeId === routeId);
|
|
@@ -472,13 +514,13 @@ export class ClaustrumConsumer {
|
|
|
472
514
|
async decline(routeId) {
|
|
473
515
|
this.#assertOpen();
|
|
474
516
|
await declineVaultRoute(this.#options.rosterPath, routeId);
|
|
475
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
517
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
476
518
|
this.#custody?.retain(this.#roster?.rows ?? []);
|
|
477
519
|
}
|
|
478
520
|
async accept(routeId) {
|
|
479
521
|
this.#assertOpen();
|
|
480
522
|
await acceptVaultRoute(this.#options.rosterPath, routeId);
|
|
481
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
523
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
482
524
|
}
|
|
483
525
|
/**
|
|
484
526
|
* Store a quota or profile observation for a vault route. The receipt the
|
|
@@ -488,7 +530,7 @@ export class ClaustrumConsumer {
|
|
|
488
530
|
*/
|
|
489
531
|
async recordQuota(routeId, observation, attempt) {
|
|
490
532
|
this.#assertOpen();
|
|
491
|
-
|
|
533
|
+
return recordVaultQuota(this.#options.rosterPath, {
|
|
492
534
|
routeId,
|
|
493
535
|
observation,
|
|
494
536
|
credentialId: attempt.credentialId,
|
|
@@ -500,27 +542,21 @@ export class ClaustrumConsumer {
|
|
|
500
542
|
expectedAccountIdentity: attempt.expectedAccountIdentity,
|
|
501
543
|
}),
|
|
502
544
|
});
|
|
503
|
-
if (kept)
|
|
504
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
505
|
-
return kept;
|
|
506
545
|
}
|
|
507
546
|
/**
|
|
508
547
|
* Store a background observation that belongs to an account rather than to
|
|
509
548
|
* a served credential (an insula reading from `cachedInsulaQuotaForRoute`).
|
|
510
549
|
* It lands only while the route is still bound to that known account, so no
|
|
511
550
|
* credential fetch is needed and a replaced account's reading is dropped.
|
|
512
|
-
*
|
|
551
|
+
* Memory updates immediately; durable publication is detached and coalesced.
|
|
513
552
|
*/
|
|
514
553
|
async recordAccountQuota(routeId, accountIdentity, observation) {
|
|
515
554
|
this.#assertOpen();
|
|
516
|
-
|
|
555
|
+
return recordVaultAccountQuota(this.#options.rosterPath, {
|
|
517
556
|
routeId,
|
|
518
557
|
accountIdentity,
|
|
519
558
|
observation,
|
|
520
559
|
});
|
|
521
|
-
if (kept)
|
|
522
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
523
|
-
return kept;
|
|
524
560
|
}
|
|
525
561
|
close() {
|
|
526
562
|
if (this.#shutdown.signal.aborted)
|
|
@@ -535,5 +571,12 @@ export class ClaustrumConsumer {
|
|
|
535
571
|
this.#insula = undefined;
|
|
536
572
|
this.#roster = undefined;
|
|
537
573
|
this.#grantReach = undefined;
|
|
574
|
+
void this.#releaseQuotaWriter();
|
|
575
|
+
forgetPendingVaultDiscovery(this.#options.rosterPath);
|
|
576
|
+
}
|
|
577
|
+
/** Close immediately; the last consumer also waits for the writer's bounded drain. */
|
|
578
|
+
async dispose() {
|
|
579
|
+
this.close();
|
|
580
|
+
await this.#releaseQuotaWriter();
|
|
538
581
|
}
|
|
539
582
|
}
|
|
@@ -7,8 +7,9 @@ export { type AccountIdentitySource, type ClaustrumFamily, type ClaustrumRosterW
|
|
|
7
7
|
export { type ClaustrumEnrollmentClient, type ClaustrumEnrollmentConnection, type ClaustrumEnrollmentDisconnectResult, ClaustrumEnrollmentManager, type ClaustrumEnrollmentPaths, type ClaustrumEnrollmentResetResult, type ClaustrumEnrollmentStatus, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, type EnrollmentDisposition, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from './enrollment.js';
|
|
8
8
|
export { ClaustrumConsumerError, type ClaustrumConsumerFailureKind, type ClaustrumLogger, } from './errors.js';
|
|
9
9
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, type HostSlotContent, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
|
|
10
|
-
export { acquireInsulaQuotaSource, type InsulaQuotaContext, type InsulaQuotaOptions, type InsulaQuotaSource, type InsulaUsageEntry, type InsulaUsageTransport, type InsulaUsageWatch, type InsulaUsageWindow, type InsulaWatchOptions, } from './insula.js';
|
|
10
|
+
export { acquireInsulaQuotaSource, type InsulaQuotaContext, type InsulaQuotaLookup, type InsulaQuotaMiss, type InsulaQuotaOptions, type InsulaQuotaReading, type InsulaQuotaResult, type InsulaQuotaSource, type InsulaUsageEntry, type InsulaUsageTransport, type InsulaUsageWatch, type InsulaUsageWindow, type InsulaWatchOptions, } from './insula.js';
|
|
11
11
|
export { acceptAccount, type DeclinedAccount, declineAccount, isDeclined, } from './interlock.js';
|
|
12
|
+
export { drainVaultQuota } from './quota-writer.js';
|
|
12
13
|
export { type AccountMapper, acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, type MutateVaultRosterOptions, mutateVaultRoster, type ProjectionOptions, projectVaultRoster, type QuotaReceipt, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, type VaultPrimary, type VaultPrimaryBinding, type VaultPrimaryUnavailableReason, type VaultRosterFile, type VaultRosterLockContext, type VaultRosterRow, vaultRoutingRows, } from './roster.js';
|
|
13
14
|
export { type ChangeNotice, ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, type ScopedRosterWatchOptions, type ScopedWatchEnd, type ScopedWatchHandle, type ScopedWatchNoticeShape, type ScopedWatchSource, WATCH_RETRY_MS, type WatchMode, } from './watch.js';
|
|
14
15
|
/**
|
|
@@ -14,6 +14,7 @@ export { ClaustrumConsumerError, } from './errors.js';
|
|
|
14
14
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
|
|
15
15
|
export { acquireInsulaQuotaSource, } from './insula.js';
|
|
16
16
|
export { acceptAccount, declineAccount, isDeclined, } from './interlock.js';
|
|
17
|
+
export { drainVaultQuota } from './quota-writer.js';
|
|
17
18
|
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from './roster.js';
|
|
18
19
|
export { ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, WATCH_RETRY_MS, } from './watch.js';
|
|
19
20
|
/**
|
|
@@ -34,6 +34,27 @@ export interface InsulaQuotaContext {
|
|
|
34
34
|
fetchedAt: number;
|
|
35
35
|
source?: string;
|
|
36
36
|
}
|
|
37
|
+
export type InsulaQuotaMiss = 'cold' | 'stale' | 'absent' | 'upstream-failed' | 'credential' | 'unreachable';
|
|
38
|
+
export interface InsulaQuotaReading {
|
|
39
|
+
entry: InsulaUsageEntry;
|
|
40
|
+
context: InsulaQuotaContext;
|
|
41
|
+
}
|
|
42
|
+
/** A miss may retain a dated reading; it is not a current quota observation. */
|
|
43
|
+
export type InsulaQuotaLookup = {
|
|
44
|
+
reading: InsulaQuotaReading;
|
|
45
|
+
miss?: never;
|
|
46
|
+
} | {
|
|
47
|
+
miss: InsulaQuotaMiss;
|
|
48
|
+
reading?: InsulaQuotaReading;
|
|
49
|
+
};
|
|
50
|
+
/** The normalized equivalent of a source lookup, for a plugin's quota type. */
|
|
51
|
+
export type InsulaQuotaResult<Q> = {
|
|
52
|
+
reading: Q;
|
|
53
|
+
miss?: never;
|
|
54
|
+
} | {
|
|
55
|
+
miss: InsulaQuotaMiss;
|
|
56
|
+
reading?: Q;
|
|
57
|
+
};
|
|
37
58
|
export interface InsulaQuotaOptions<Q> {
|
|
38
59
|
provider: 'codex' | 'claude' | 'antigravity';
|
|
39
60
|
/**
|
|
@@ -110,6 +131,8 @@ export declare class InsulaQuotaSource {
|
|
|
110
131
|
entry: InsulaUsageEntry;
|
|
111
132
|
context: InsulaQuotaContext;
|
|
112
133
|
} | undefined;
|
|
134
|
+
/** Cache-only background lookup; current() retains its original miss behavior. */
|
|
135
|
+
lookup(provider: string, accountId: string, now: number): InsulaQuotaLookup;
|
|
113
136
|
}
|
|
114
137
|
export declare function acquireInsulaQuotaSource(transport: InsulaUsageTransport, tokenPath: string): {
|
|
115
138
|
source: InsulaQuotaSource;
|
|
@@ -128,6 +128,9 @@ export function insulaUsageTransport(options) {
|
|
|
128
128
|
}
|
|
129
129
|
const MINUTE = 60_000;
|
|
130
130
|
const MAX_AGE = 4 * MINUTE;
|
|
131
|
+
// consumer-contract.md:331 refetches a slot after 60 s. Two such intervals
|
|
132
|
+
// bound our willingness to wait, not a claim that insula completed its sweep.
|
|
133
|
+
const COLD_WAIT = 2 * MINUTE;
|
|
131
134
|
const sources = new Map();
|
|
132
135
|
// Keep only the minute budget after the last owner closes, so project churn
|
|
133
136
|
// cannot reset the bound. Expired budgets are pruned on the next tick.
|
|
@@ -170,6 +173,9 @@ export class InsulaQuotaSource {
|
|
|
170
173
|
transport;
|
|
171
174
|
key;
|
|
172
175
|
#entries = [];
|
|
176
|
+
#available = false;
|
|
177
|
+
#completeProviders = new Set();
|
|
178
|
+
#coldSince = new Map();
|
|
173
179
|
/** Every usage.get, polled or watch-driven, shares this one slot. */
|
|
174
180
|
#reads = new CoalescedRead(async () => {
|
|
175
181
|
try {
|
|
@@ -178,13 +184,25 @@ export class InsulaQuotaSource {
|
|
|
178
184
|
!Array.isArray(reply.result) ||
|
|
179
185
|
reply.error !== undefined)
|
|
180
186
|
throw new Error('unavailable insula usage');
|
|
181
|
-
// Each response is a snapshot, not a delta.
|
|
182
|
-
// neither pending nor the entry count
|
|
187
|
+
// Each response is a snapshot, not a delta. Only completeProviders
|
|
188
|
+
// asserts completeness; neither pending nor the entry count does.
|
|
183
189
|
this.#entries = reply.result.filter((entry) => record(entry) && typeof entry.provider === 'string');
|
|
190
|
+
this.#available =
|
|
191
|
+
this.#entries.length === reply.result.length &&
|
|
192
|
+
(reply.completeProviders === undefined ||
|
|
193
|
+
(Array.isArray(reply.completeProviders) &&
|
|
194
|
+
reply.completeProviders.every((value) => typeof value === 'string')));
|
|
195
|
+
this.#completeProviders = new Set(this.#available && Array.isArray(reply.completeProviders)
|
|
196
|
+
? reply.completeProviders
|
|
197
|
+
: []);
|
|
198
|
+
for (const provider of this.#completeProviders)
|
|
199
|
+
this.#coldSince.delete(provider);
|
|
184
200
|
}
|
|
185
201
|
catch {
|
|
186
202
|
// Refusal is absence, even if the previous reading was still young.
|
|
187
203
|
this.#entries = [];
|
|
204
|
+
this.#available = false;
|
|
205
|
+
this.#completeProviders.clear();
|
|
188
206
|
}
|
|
189
207
|
}, () => { });
|
|
190
208
|
#reader = new ChangeNoticeReader();
|
|
@@ -391,6 +409,50 @@ export class InsulaQuotaSource {
|
|
|
391
409
|
}
|
|
392
410
|
return undefined;
|
|
393
411
|
}
|
|
412
|
+
/** Cache-only background lookup; current() retains its original miss behavior. */
|
|
413
|
+
lookup(provider, accountId, now) {
|
|
414
|
+
if (!this.#available)
|
|
415
|
+
return { miss: 'unreachable' };
|
|
416
|
+
const entry = this.#entries.find((value) => value.provider === provider &&
|
|
417
|
+
typeof value.account === 'string' &&
|
|
418
|
+
value.account.trim() === accountId);
|
|
419
|
+
// An unlabelled error describes the provider's credential path, not an
|
|
420
|
+
// unrelated labelled account. Never borrow another account's verdict.
|
|
421
|
+
const errorClass = (entry ??
|
|
422
|
+
this.#entries.find((value) => value.provider === provider &&
|
|
423
|
+
value.account === undefined &&
|
|
424
|
+
value.errorClass !== undefined))?.errorClass;
|
|
425
|
+
if (errorClass === 'upstream_failed')
|
|
426
|
+
return { miss: 'upstream-failed' };
|
|
427
|
+
if (errorClass === 'credential_absent' ||
|
|
428
|
+
errorClass === 'credential_unusable' ||
|
|
429
|
+
errorClass === 'credential_rejected')
|
|
430
|
+
return { miss: 'credential' };
|
|
431
|
+
if (errorClass !== undefined)
|
|
432
|
+
return { miss: 'unreachable' };
|
|
433
|
+
if (entry) {
|
|
434
|
+
const fetchedAt = typeof entry.fetchedAt === 'string'
|
|
435
|
+
? Date.parse(entry.fetchedAt)
|
|
436
|
+
: Number.NaN;
|
|
437
|
+
if (!record(entry.usage) || !Number.isFinite(fetchedAt))
|
|
438
|
+
return { miss: 'unreachable' };
|
|
439
|
+
const reading = {
|
|
440
|
+
entry,
|
|
441
|
+
context: {
|
|
442
|
+
fetchedAt,
|
|
443
|
+
...(typeof entry.source === 'string' && { source: entry.source }),
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
return now >= fetchedAt + MAX_AGE || entry.stale !== undefined
|
|
447
|
+
? { miss: 'stale', reading }
|
|
448
|
+
: { reading };
|
|
449
|
+
}
|
|
450
|
+
if (this.#completeProviders.has(provider))
|
|
451
|
+
return { miss: 'absent' };
|
|
452
|
+
const since = this.#coldSince.get(provider) ?? now;
|
|
453
|
+
this.#coldSince.set(provider, since);
|
|
454
|
+
return { miss: now - since >= COLD_WAIT ? 'unreachable' : 'cold' };
|
|
455
|
+
}
|
|
394
456
|
}
|
|
395
457
|
export function acquireInsulaQuotaSource(transport, tokenPath) {
|
|
396
458
|
const key = JSON.stringify([
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type QuotaObservation } from '../quota/index.js';
|
|
2
|
+
import { type QuotaReceipt, type VaultRosterFile } from './roster.js';
|
|
3
|
+
export type VaultQuotaPublication = {
|
|
4
|
+
routeId: string;
|
|
5
|
+
accountIdentity?: string;
|
|
6
|
+
receipt?: QuotaReceipt;
|
|
7
|
+
observation: QuotaObservation;
|
|
8
|
+
};
|
|
9
|
+
type Listener = (publication: VaultQuotaPublication) => void;
|
|
10
|
+
/** Retain the shared writer; the last consumer's close drains it. */
|
|
11
|
+
export declare function retainVaultQuotaWriter(path: string, listener: Listener): () => Promise<void>;
|
|
12
|
+
/** Enqueue without filesystem work; each quota field retains its own receipt fence. */
|
|
13
|
+
export declare function enqueueVaultQuota(path: string, publication: VaultQuotaPublication): Promise<boolean>;
|
|
14
|
+
/** Overlay uncommitted observations when a consumer reloads the durable roster. */
|
|
15
|
+
export declare function pendingVaultQuota(path: string, roster: VaultRosterFile | undefined): VaultRosterFile | undefined;
|
|
16
|
+
/** Wait at most timeoutMs for detached publication; shutdown loss is acceptable. */
|
|
17
|
+
export declare function drainVaultQuota(path: string, timeoutMs?: number): Promise<void>;
|
|
18
|
+
export {};
|