@cortexkit/opencode-openai-auth 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +8 -8
- package/dist/{codex-request-B9D0e97Q.d.ts → codex-request-BjGT9yWg.d.ts} +36 -28
- package/dist/index-2p3gcjsa.js +69 -0
- package/dist/{index-m3ctrabv.js → index-2rw81dtm.js} +1 -1
- package/dist/index-2yszc75z.js +2 -0
- package/dist/{index-gn2b097f.js → index-3y453wg4.js} +7 -7
- package/dist/{index-bc89akak.js → index-4w0nsq5s.js} +1 -1
- package/dist/index-6fdmqk6p.js +90 -0
- package/dist/{index-xwvze96g.js → index-7vkn2e46.js} +1 -1
- package/dist/{index-2pgkrebq.js → index-djnfq652.js} +2 -2
- package/dist/{index-tddr876f.js → index-e416r885.js} +2 -2
- package/dist/index-fztcacjr.js +65 -0
- package/dist/index-gmmakhbt.js +2 -0
- package/dist/{index-ftkhhr6d.js → index-krcqvyqf.js} +1 -1
- package/dist/index-mhfja8yr.js +2 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +1 -1
- package/dist/{model-BdITTmeb.d.ts → model-BMq5SCUB.d.ts} +1 -1
- package/dist/{port-file-DbHIVC9a.d.ts → port-file-CjMFUWcX.d.ts} +1 -1
- package/dist/{protocol-MysYPAho.d.ts → protocol-BC3ndToM.d.ts} +1 -1
- package/dist/rpc/port-file.d.ts +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +1 -1
- package/dist/rpc/protocol.js +1 -1
- package/dist/rpc/rpc-client.d.ts +3 -3
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/{sidebar-state-CVSok7zv.d.ts → sidebar-state-DfYyqiOH.d.ts} +55 -7
- package/dist/sidebar-state.d.ts +2 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/token-util-ed4az5gg.js +1 -0
- package/dist/{token-z6qq7a1d.js → token-ycw7dyev.js} +1 -1
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +30 -3
- package/dist/v2/server.d.ts +2 -2
- package/dist/v2/server.js +2 -2
- package/package.json +5 -5
- package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +50 -5
- package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +70 -1
- package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +104 -62
- package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
- package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +172 -43
- package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +49 -3
- package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +3 -1
- package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +7 -9
- package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +2 -0
- package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +17 -3
- package/src/tui-compiled/raw/shared/754d6017f8e419cf-builtins.js +95 -13
- package/src/tui-compiled/raw/shared/81dddd203f1b92a3-quota-writer.js +253 -0
- package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +31 -6
- package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +116 -12
- package/src/tui-compiled/raw/shared/b9588ad4725c1d10-enrollment.js +27 -0
- package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
- package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +64 -2
- package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +2 -2
- package/src/tui-compiled/raw/shared/dbc61cd405941828-accounts.js +5 -4
- package/src/tui-compiled/raw/shared/dc9290c2a7552ba6-watch.js +2 -1
- package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +2 -1
- package/src/tui-compiled/raw/sidebar-state.js +32 -3
- package/src/tui-compiled/raw/tui/v2-dialog.js +2 -1
- package/src/tui-compiled/raw/tui.tsx +170 -26
- package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +50 -5
- package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +70 -1
- package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +104 -62
- package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
- package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +172 -43
- package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +49 -3
- package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +3 -1
- package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +7 -9
- package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +2 -0
- package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +17 -3
- package/src/tui-compiled/runtime/shared/754d6017f8e419cf-builtins.js +95 -13
- package/src/tui-compiled/runtime/shared/81dddd203f1b92a3-quota-writer.js +253 -0
- package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +31 -6
- package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +116 -12
- package/src/tui-compiled/runtime/shared/b9588ad4725c1d10-enrollment.js +27 -0
- package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
- package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +64 -2
- package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +2 -2
- package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +5 -4
- package/src/tui-compiled/runtime/shared/dc9290c2a7552ba6-watch.js +2 -1
- package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +2 -1
- package/src/tui-compiled/runtime/sidebar-state.js +32 -3
- package/src/tui-compiled/runtime/tui/v2-dialog.js +2 -1
- package/src/tui-compiled/runtime/tui.js +350 -219
- package/dist/index-1mbgsxsr.js +0 -2
- package/dist/index-4v2fs42x.js +0 -65
- package/dist/index-8x1aj7q3.js +0 -2
- package/dist/index-ex6nx3mq.js +0 -2
- package/dist/index-psjyexj3.js +0 -69
- package/dist/index-tn8x5tz8.js +0 -90
- package/dist/token-util-cf0nyh7n.js +0 -1
|
@@ -3,9 +3,10 @@ import { combineAbortSignals } from "./6371b24a0573d79e-any-signal.js";
|
|
|
3
3
|
import { ClaustrumScopedCustody, decideScopedRetryAfter401, } from "./0f77ebc3c914c9c6-custody.js";
|
|
4
4
|
import { readClaustrumEnrollmentToken } from "./b9588ad4725c1d10-enrollment.js";
|
|
5
5
|
import { ClaustrumConsumerError } from "./ca0fccaba1d87af4-errors.js";
|
|
6
|
-
import { acquireInsulaQuotaSource } from "./d0f0b6be200311af-insula.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { acquireInsulaQuotaSource, } from "./d0f0b6be200311af-insula.js";
|
|
7
|
+
import { pendingVaultQuota, retainVaultQuotaWriter } from "./81dddd203f1b92a3-quota-writer.js";
|
|
8
|
+
import { acceptVaultRoute, applyVaultQuota, declineVaultRoute, forgetPendingVaultDiscovery, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, reorderVaultRoster, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
9
|
+
import { SAFETY_POLL_MS } from "./dc9290c2a7552ba6-watch.js";
|
|
9
10
|
/**
|
|
10
11
|
* One plugin host's view of its vault accounts. It turns vault credentials
|
|
11
12
|
* into rows for `/routing`, authorizes each physical send with the vault, and
|
|
@@ -19,8 +20,6 @@ export class ClaustrumConsumer {
|
|
|
19
20
|
#shutdown = new AbortController();
|
|
20
21
|
#logger;
|
|
21
22
|
#custody;
|
|
22
|
-
/** The custody's client, kept so `start()` can open its vault watch. */
|
|
23
|
-
#client;
|
|
24
23
|
#connecting;
|
|
25
24
|
#refreshing;
|
|
26
25
|
#roster;
|
|
@@ -34,11 +33,16 @@ export class ClaustrumConsumer {
|
|
|
34
33
|
#insula;
|
|
35
34
|
#vaultWatch;
|
|
36
35
|
#releaseInsulaWatch;
|
|
36
|
+
#insulaSkips = new Map();
|
|
37
37
|
/** When this instance last completed a discovery, by `now`. */
|
|
38
38
|
#discoveredAt;
|
|
39
|
+
#releaseQuotaWriter;
|
|
39
40
|
constructor(options) {
|
|
40
41
|
this.#options = options;
|
|
41
42
|
this.#logger = options.logger ?? createLogger('claustrum');
|
|
43
|
+
this.#releaseQuotaWriter = retainVaultQuotaWriter(options.rosterPath, (publication) => {
|
|
44
|
+
this.#roster = applyVaultQuota(this.#roster, publication).roster;
|
|
45
|
+
});
|
|
42
46
|
}
|
|
43
47
|
#assertOpen(signal) {
|
|
44
48
|
this.#shutdown.signal.throwIfAborted();
|
|
@@ -90,7 +94,6 @@ export class ClaustrumConsumer {
|
|
|
90
94
|
client.close();
|
|
91
95
|
this.#assertOpen();
|
|
92
96
|
}
|
|
93
|
-
this.#client = client;
|
|
94
97
|
this.#custody = new ClaustrumScopedCustody({
|
|
95
98
|
client,
|
|
96
99
|
family: this.#options.family,
|
|
@@ -143,9 +146,8 @@ export class ClaustrumConsumer {
|
|
|
143
146
|
}
|
|
144
147
|
/** Close the watches with the connection they belong to. */
|
|
145
148
|
#stopWatch() {
|
|
146
|
-
this.#vaultWatch?.
|
|
149
|
+
this.#vaultWatch?.close();
|
|
147
150
|
this.#vaultWatch = undefined;
|
|
148
|
-
this.#client = undefined;
|
|
149
151
|
this.#releaseInsulaWatch?.();
|
|
150
152
|
this.#releaseInsulaWatch = undefined;
|
|
151
153
|
}
|
|
@@ -179,11 +181,9 @@ export class ClaustrumConsumer {
|
|
|
179
181
|
let discovered;
|
|
180
182
|
const roster = await refreshVaultRoster({
|
|
181
183
|
path: this.#options.rosterPath,
|
|
182
|
-
custody
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return discovered;
|
|
186
|
-
},
|
|
184
|
+
custody,
|
|
185
|
+
onInventory: (inventory) => {
|
|
186
|
+
discovered = inventory;
|
|
187
187
|
},
|
|
188
188
|
isActive: () => this.#active(),
|
|
189
189
|
signal: this.#shutdown.signal,
|
|
@@ -196,7 +196,7 @@ export class ClaustrumConsumer {
|
|
|
196
196
|
});
|
|
197
197
|
this.#assertOpen();
|
|
198
198
|
const changed = roster?.view !== this.#roster?.view;
|
|
199
|
-
this.#roster = roster;
|
|
199
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, roster);
|
|
200
200
|
custody.retain(roster?.rows ?? []);
|
|
201
201
|
// Each discovery replaces the previous value, including with unknown. A
|
|
202
202
|
// refresh that served a peer's roster discovered nothing and leaves it;
|
|
@@ -271,42 +271,30 @@ export class ClaustrumConsumer {
|
|
|
271
271
|
* reached in vault mode: a host in local mode never connects.
|
|
272
272
|
*/
|
|
273
273
|
#openWatch() {
|
|
274
|
-
const
|
|
275
|
-
if (this.#shutdown.signal.aborted || !
|
|
274
|
+
const custody = this.#custody;
|
|
275
|
+
if (this.#shutdown.signal.aborted || !custody)
|
|
276
276
|
return;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
277
|
+
// The same entry point a host without a consumer uses, so both apply
|
|
278
|
+
// notices, refusals and outages alike.
|
|
279
|
+
this.#vaultWatch ??= custody.watchRoster({
|
|
280
|
+
view: () => this.#roster?.view,
|
|
281
|
+
// The existing discovery, so a watched change commits the roster and
|
|
282
|
+
// re-applies it to kept receipts exactly as a poll does. A refresh
|
|
283
|
+
// already in flight may have listed before the change was announced,
|
|
284
|
+
// so wait for it and list again.
|
|
285
|
+
onView: async () => {
|
|
286
|
+
while (this.#refreshing)
|
|
287
|
+
await this.#refreshing.catch(() => { });
|
|
288
|
+
await this.refresh();
|
|
289
|
+
},
|
|
290
|
+
// Custody has dropped kept receipts, as a poll meeting the same
|
|
291
|
+
// refusal from list_scoped would.
|
|
292
|
+
onRefused: (error) => this.#options.onError?.(error),
|
|
293
|
+
onError: (error) => {
|
|
294
|
+
if (!this.#shutdown.signal.aborted)
|
|
294
295
|
this.#options.onError?.(error);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
onReachable: () => this.#custody?.reachable(),
|
|
298
|
-
onError: (error) => {
|
|
299
|
-
if (!this.#shutdown.signal.aborted)
|
|
300
|
-
this.#options.onError?.(error);
|
|
301
|
-
},
|
|
302
|
-
logger: this.#logger,
|
|
303
|
-
now: this.#options.now,
|
|
304
|
-
setTimeoutImpl: this.#options.setTimeoutImpl,
|
|
305
|
-
clearTimeoutImpl: this.#options.clearTimeoutImpl,
|
|
306
|
-
});
|
|
307
|
-
this.#vaultWatch = watch;
|
|
308
|
-
watch.start();
|
|
309
|
-
}
|
|
296
|
+
},
|
|
297
|
+
});
|
|
310
298
|
if (this.#options.insula && this.#insula && !this.#releaseInsulaWatch)
|
|
311
299
|
this.#releaseInsulaWatch = this.#insula.source.watch({
|
|
312
300
|
provider: this.#options.insula.provider,
|
|
@@ -364,7 +352,7 @@ export class ClaustrumConsumer {
|
|
|
364
352
|
return undefined;
|
|
365
353
|
try {
|
|
366
354
|
await readClaustrumEnrollmentToken(this.#options.tokenPath);
|
|
367
|
-
const roster = await readVaultRoster(this.#options.rosterPath);
|
|
355
|
+
const roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
368
356
|
const row = roster?.rows.find((entry) => entry.routeId === routeId);
|
|
369
357
|
return this.#normalizeInsulaQuota(row?.accountIdentity);
|
|
370
358
|
}
|
|
@@ -372,6 +360,44 @@ export class ClaustrumConsumer {
|
|
|
372
360
|
return undefined;
|
|
373
361
|
}
|
|
374
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
|
+
}
|
|
375
401
|
#normalizeInsulaQuota(accountIdentity) {
|
|
376
402
|
if (!this.#options.insula || !accountIdentity)
|
|
377
403
|
return undefined;
|
|
@@ -396,7 +422,7 @@ export class ClaustrumConsumer {
|
|
|
396
422
|
this.#insula = undefined;
|
|
397
423
|
throw new ClaustrumConsumerError('not-active', 'Claustrum scoped custody is not active');
|
|
398
424
|
}
|
|
399
|
-
const current = await readVaultRoster(this.#options.rosterPath);
|
|
425
|
+
const current = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
400
426
|
this.#assertOpen(signal);
|
|
401
427
|
const observed = roster.rows.find((row) => row.routeId === routeId);
|
|
402
428
|
const configured = current?.rows.find((row) => row.routeId === routeId);
|
|
@@ -459,8 +485,12 @@ export class ClaustrumConsumer {
|
|
|
459
485
|
let current;
|
|
460
486
|
try {
|
|
461
487
|
current = await this.authorize(routeId, signal);
|
|
488
|
+
// Cancellation can land after authorize settles, before this continuation.
|
|
489
|
+
signal?.throwIfAborted();
|
|
462
490
|
}
|
|
463
|
-
catch {
|
|
491
|
+
catch (error) {
|
|
492
|
+
if (signal?.aborted)
|
|
493
|
+
throw error;
|
|
464
494
|
// Re-authorization failed: keep this 401 and report it against the
|
|
465
495
|
// receipt (served record version) this request was sent with.
|
|
466
496
|
}
|
|
@@ -488,13 +518,24 @@ export class ClaustrumConsumer {
|
|
|
488
518
|
async decline(routeId) {
|
|
489
519
|
this.#assertOpen();
|
|
490
520
|
await declineVaultRoute(this.#options.rosterPath, routeId);
|
|
491
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
521
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
492
522
|
this.#custody?.retain(this.#roster?.rows ?? []);
|
|
493
523
|
}
|
|
494
524
|
async accept(routeId) {
|
|
495
525
|
this.#assertOpen();
|
|
496
526
|
await acceptVaultRoute(this.#options.rosterPath, routeId);
|
|
497
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
527
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Set the order vault accounts are tried in (`reorderVaultRoster`), then
|
|
531
|
+
* re-read the roster, so the next routing decision in this process uses
|
|
532
|
+
* the new order without waiting for a discovery. Never contacts the vault.
|
|
533
|
+
*/
|
|
534
|
+
async reorder(routeIds) {
|
|
535
|
+
this.#assertOpen();
|
|
536
|
+
const result = await reorderVaultRoster(this.#options.rosterPath, routeIds);
|
|
537
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
538
|
+
return result;
|
|
498
539
|
}
|
|
499
540
|
/**
|
|
500
541
|
* Store a quota or profile observation for a vault route. The receipt the
|
|
@@ -504,7 +545,7 @@ export class ClaustrumConsumer {
|
|
|
504
545
|
*/
|
|
505
546
|
async recordQuota(routeId, observation, attempt) {
|
|
506
547
|
this.#assertOpen();
|
|
507
|
-
|
|
548
|
+
return recordVaultQuota(this.#options.rosterPath, {
|
|
508
549
|
routeId,
|
|
509
550
|
observation,
|
|
510
551
|
credentialId: attempt.credentialId,
|
|
@@ -516,27 +557,21 @@ export class ClaustrumConsumer {
|
|
|
516
557
|
expectedAccountIdentity: attempt.expectedAccountIdentity,
|
|
517
558
|
}),
|
|
518
559
|
});
|
|
519
|
-
if (kept)
|
|
520
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
521
|
-
return kept;
|
|
522
560
|
}
|
|
523
561
|
/**
|
|
524
562
|
* Store a background observation that belongs to an account rather than to
|
|
525
563
|
* a served credential (an insula reading from `cachedInsulaQuotaForRoute`).
|
|
526
564
|
* It lands only while the route is still bound to that known account, so no
|
|
527
565
|
* credential fetch is needed and a replaced account's reading is dropped.
|
|
528
|
-
*
|
|
566
|
+
* Memory updates immediately; durable publication is detached and coalesced.
|
|
529
567
|
*/
|
|
530
568
|
async recordAccountQuota(routeId, accountIdentity, observation) {
|
|
531
569
|
this.#assertOpen();
|
|
532
|
-
|
|
570
|
+
return recordVaultAccountQuota(this.#options.rosterPath, {
|
|
533
571
|
routeId,
|
|
534
572
|
accountIdentity,
|
|
535
573
|
observation,
|
|
536
574
|
});
|
|
537
|
-
if (kept)
|
|
538
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
539
|
-
return kept;
|
|
540
575
|
}
|
|
541
576
|
close() {
|
|
542
577
|
if (this.#shutdown.signal.aborted)
|
|
@@ -551,5 +586,12 @@ export class ClaustrumConsumer {
|
|
|
551
586
|
this.#insula = undefined;
|
|
552
587
|
this.#roster = undefined;
|
|
553
588
|
this.#grantReach = undefined;
|
|
589
|
+
void this.#releaseQuotaWriter();
|
|
590
|
+
forgetPendingVaultDiscovery(this.#options.rosterPath);
|
|
591
|
+
}
|
|
592
|
+
/** Close immediately; the last consumer also waits for the writer's bounded drain. */
|
|
593
|
+
async dispose() {
|
|
594
|
+
this.close();
|
|
595
|
+
await this.#releaseQuotaWriter();
|
|
554
596
|
}
|
|
555
597
|
}
|
|
@@ -56,8 +56,33 @@ export function normalizeQuotaHeaders(h) {
|
|
|
56
56
|
const secondary = windowFromHeader(h, "x-codex-secondary-used-percent");
|
|
57
57
|
if (secondary)
|
|
58
58
|
snapshot.secondary = secondary;
|
|
59
|
+
const credits = creditsFromHeaders(h);
|
|
60
|
+
if (credits)
|
|
61
|
+
snapshot.credits = credits;
|
|
59
62
|
return snapshot;
|
|
60
63
|
}
|
|
64
|
+
function headerBool(h, name) {
|
|
65
|
+
const raw = h.get(name)?.trim();
|
|
66
|
+
if (raw === void 0)
|
|
67
|
+
return;
|
|
68
|
+
if (raw.toLowerCase() === "true" || raw === "1")
|
|
69
|
+
return !0;
|
|
70
|
+
if (raw.toLowerCase() === "false" || raw === "0")
|
|
71
|
+
return !1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
function creditsFromHeaders(h) {
|
|
75
|
+
const hasCredits = headerBool(h, "x-codex-credits-has-credits"), unlimited = headerBool(h, "x-codex-credits-unlimited");
|
|
76
|
+
if (hasCredits === void 0 || unlimited === void 0)
|
|
77
|
+
return;
|
|
78
|
+
const balance = nonNegativeNumberish(h.get("x-codex-credits-balance")?.trim());
|
|
79
|
+
return {
|
|
80
|
+
hasCredits,
|
|
81
|
+
unlimited,
|
|
82
|
+
...balance !== void 0 ? { balance } : {},
|
|
83
|
+
checkedAt: Date.now()
|
|
84
|
+
};
|
|
85
|
+
}
|
|
61
86
|
export function isCompleteQuotaHeaderFrame(h) {
|
|
62
87
|
const states = QUOTA_USED_PERCENT_HEADERS.map((name) => usedPercentHeaderState(h, name));
|
|
63
88
|
return states.includes("valid") && !states.includes("invalid");
|
|
@@ -85,8 +110,26 @@ export function normalizeWsFrame(event) {
|
|
|
85
110
|
const secondary = windowFromWs(event.rate_limits?.secondary);
|
|
86
111
|
if (secondary)
|
|
87
112
|
snapshot.secondary = secondary;
|
|
113
|
+
const credits = creditsFromWs(event.credits);
|
|
114
|
+
if (credits)
|
|
115
|
+
snapshot.credits = credits;
|
|
88
116
|
return snapshot;
|
|
89
117
|
}
|
|
118
|
+
function creditsFromWs(value) {
|
|
119
|
+
if (value === null || typeof value !== "object")
|
|
120
|
+
return;
|
|
121
|
+
const credits = value;
|
|
122
|
+
if (typeof credits.has_credits !== "boolean" || typeof credits.unlimited !== "boolean")
|
|
123
|
+
return;
|
|
124
|
+
const balance = nonNegativeNumberish(typeof credits.balance === "string" ? credits.balance.trim() : credits.balance);
|
|
125
|
+
return {
|
|
126
|
+
hasCredits: credits.has_credits,
|
|
127
|
+
unlimited: credits.unlimited,
|
|
128
|
+
...balance !== void 0 ? { balance } : {},
|
|
129
|
+
...typeof credits.overage_limit_reached === "boolean" ? { overageLimitReached: credits.overage_limit_reached } : {},
|
|
130
|
+
checkedAt: Date.now()
|
|
131
|
+
};
|
|
132
|
+
}
|
|
90
133
|
function nonNegativeNumberish(value) {
|
|
91
134
|
if (value == null || value === "")
|
|
92
135
|
return;
|
|
@@ -147,7 +190,8 @@ export function normalizeWham(json) {
|
|
|
147
190
|
hasCredits: credits.has_credits,
|
|
148
191
|
unlimited: credits.unlimited,
|
|
149
192
|
overageLimitReached: credits.overage_limit_reached,
|
|
150
|
-
...balance !== void 0 ? { balance } : {}
|
|
193
|
+
...balance !== void 0 ? { balance } : {},
|
|
194
|
+
checkedAt: Date.now()
|
|
151
195
|
};
|
|
152
196
|
}
|
|
153
197
|
return snapshot;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { acquireRefreshFileLock, LockContentionError, withLock, writeJsonAtomic, } from "./ec971142548df90e-index.js";
|
|
3
4
|
import { readPrivateText, windowsPrivateFiles } from "./375d9cca2def037d-private-files.js";
|
|
4
|
-
import { decodeQuotaMap, mergeQuotaObservation, } from "./d5a8f1061ee2f114-index.js";
|
|
5
|
+
import { decodeQuotaMap, isQuotaObservation, mergeQuotaObservation, } from "./d5a8f1061ee2f114-index.js";
|
|
5
6
|
import { ClaustrumConsumerError } from "./ca0fccaba1d87af4-errors.js";
|
|
6
7
|
import { acceptAccount, declineAccount, isDeclined, } from "./7c140680fc81be92-interlock.js";
|
|
8
|
+
import { enqueueVaultQuota, pendingVaultQuota, } from "./81dddd203f1b92a3-quota-writer.js";
|
|
9
|
+
export { drainVaultQuota } from "./81dddd203f1b92a3-quota-writer.js";
|
|
7
10
|
export const DEFAULT_ROUTE_PREFIX = 'vault:';
|
|
8
11
|
function hash(value) {
|
|
9
12
|
return createHash('sha256').update(value).digest('hex');
|
|
@@ -313,6 +316,7 @@ const WRITE_LOCK = {
|
|
|
313
316
|
name: 'claustrum-roster-write',
|
|
314
317
|
ttlMs: 10_000,
|
|
315
318
|
timeoutMs: 5_000,
|
|
319
|
+
renew: true,
|
|
316
320
|
};
|
|
317
321
|
const DISCOVERY_LEASE = { name: 'claustrum-roster', ttlMs: 30_000 };
|
|
318
322
|
/**
|
|
@@ -321,7 +325,7 @@ const DISCOVERY_LEASE = { name: 'claustrum-roster', ttlMs: 30_000 };
|
|
|
321
325
|
* before any rename performed inside `change`; if it throws, do not rename.
|
|
322
326
|
*/
|
|
323
327
|
export async function mutateVaultRoster(path, change, options = {}) {
|
|
324
|
-
return withLock(path, WRITE_LOCK, async (lock) => {
|
|
328
|
+
return withLock(path, { ...WRITE_LOCK, signal: options.signal }, async (lock) => {
|
|
325
329
|
const current = await readVaultRoster(path);
|
|
326
330
|
const { next, result } = await change(current, {
|
|
327
331
|
assertOwned: () => lock.assertOwned(),
|
|
@@ -333,6 +337,7 @@ export async function mutateVaultRoster(path, change, options = {}) {
|
|
|
333
337
|
await writeJsonAtomic(path, next, {
|
|
334
338
|
beforeRename: async () => {
|
|
335
339
|
await options.beforePublish?.();
|
|
340
|
+
options.signal?.throwIfAborted();
|
|
336
341
|
await lock.assertOwned();
|
|
337
342
|
},
|
|
338
343
|
});
|
|
@@ -373,6 +378,78 @@ export function acceptVaultRoute(path, routeId) {
|
|
|
373
378
|
};
|
|
374
379
|
});
|
|
375
380
|
}
|
|
381
|
+
function invalidOrder(message) {
|
|
382
|
+
return new ClaustrumConsumerError('invalid-order', message);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Why `routeIds` is not an order of these rows: it must name every distinct
|
|
386
|
+
* route id exactly once and nothing else. The message quotes only ids the
|
|
387
|
+
* caller supplied; route ids it left out are counted, not named.
|
|
388
|
+
*/
|
|
389
|
+
function vaultOrderProblem(rows, routeIds) {
|
|
390
|
+
if (!Array.isArray(routeIds))
|
|
391
|
+
return 'route ids must be an array of route ids';
|
|
392
|
+
const rosterIds = new Set(rows.map((row) => row.routeId));
|
|
393
|
+
const given = new Set();
|
|
394
|
+
for (const id of routeIds) {
|
|
395
|
+
if (typeof id !== 'string')
|
|
396
|
+
return 'route ids must be an array of route ids';
|
|
397
|
+
if (given.has(id))
|
|
398
|
+
return `route id ${id} appears more than once`;
|
|
399
|
+
if (!rosterIds.has(id))
|
|
400
|
+
return `route id ${id} is not in the roster`;
|
|
401
|
+
given.add(id);
|
|
402
|
+
}
|
|
403
|
+
const missing = [...rosterIds].filter((id) => !given.has(id)).length;
|
|
404
|
+
if (missing > 0)
|
|
405
|
+
return `the order leaves out ${missing} roster route id${missing === 1 ? '' : 's'}`;
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Set the order vault accounts are tried in. Routing reads that order from
|
|
410
|
+
* the roster's row order (`vaultRoutingRows`), and a later discovery keeps
|
|
411
|
+
* each account at its position, so the order lasts until the next reorder.
|
|
412
|
+
* The order is this host's routing preference: the vault has no order of its
|
|
413
|
+
* own, and this never contacts it.
|
|
414
|
+
*
|
|
415
|
+
* `routeIds` must name every roster route id exactly once; anything else
|
|
416
|
+
* (an unknown, missing or repeated id) refuses with `invalid-order` and
|
|
417
|
+
* writes nothing. It runs under the roster's write lock, like every other
|
|
418
|
+
* roster write, so a quota reading or decline committed meanwhile is kept.
|
|
419
|
+
* Each row is kept as the same object, so only the positions change: every
|
|
420
|
+
* row's fields and the rest of the file stay as they were. An order equal to
|
|
421
|
+
* the current one writes nothing. A host with no roster file yet has no
|
|
422
|
+
* routes, so only the empty order is accepted.
|
|
423
|
+
*
|
|
424
|
+
* A `ClaustrumConsumer` in the same process does not see the new order until
|
|
425
|
+
* it re-reads the file: call `consumer.reorder` instead to update both.
|
|
426
|
+
*/
|
|
427
|
+
export function reorderVaultRoster(path, routeIds, options = {}) {
|
|
428
|
+
return mutateVaultRoster(path, (current) => {
|
|
429
|
+
const rows = current?.rows ?? [];
|
|
430
|
+
const problem = vaultOrderProblem(rows, routeIds);
|
|
431
|
+
if (problem)
|
|
432
|
+
throw invalidOrder(problem);
|
|
433
|
+
// Group by route id so a hand-edited file that repeats one keeps every
|
|
434
|
+
// copy: the later copies travel right after the first.
|
|
435
|
+
const byId = new Map();
|
|
436
|
+
for (const row of rows) {
|
|
437
|
+
const group = byId.get(row.routeId);
|
|
438
|
+
if (group)
|
|
439
|
+
group.push(row);
|
|
440
|
+
else
|
|
441
|
+
byId.set(row.routeId, [row]);
|
|
442
|
+
}
|
|
443
|
+
const ids = [...routeIds];
|
|
444
|
+
const next = ids.flatMap((id) => byId.get(id) ?? []);
|
|
445
|
+
if (!current || next.every((row, index) => row === rows[index]))
|
|
446
|
+
return { result: { ids, outcome: 'unchanged' } };
|
|
447
|
+
return {
|
|
448
|
+
next: { ...current, rows: next },
|
|
449
|
+
result: { ids, outcome: 'reordered' },
|
|
450
|
+
};
|
|
451
|
+
}, { ...(options.signal && { signal: options.signal }) });
|
|
452
|
+
}
|
|
376
453
|
/**
|
|
377
454
|
* What a receipt may do to a row's quota: `hold` (the row holds the receipt's
|
|
378
455
|
* credential and account), `bind` (the row has never had a known account and
|
|
@@ -415,55 +492,73 @@ function receiptFence(row, receipt) {
|
|
|
415
492
|
* write is fenced on the receipt the reading was taken with: it lands only
|
|
416
493
|
* while the row still holds that receipt's credential (as representative or
|
|
417
494
|
* alias) and account, so a slow read for a replaced account can never land on
|
|
418
|
-
* its successor.
|
|
495
|
+
* its successor. Resolves false when the fence refuses the reading against the
|
|
496
|
+
* roster as it stands now (on disk, plus readings this process has not
|
|
497
|
+
* written yet), and true once the reading is queued for the next write. The
|
|
498
|
+
* write applies the fence again, so a rebind between the two still refuses.
|
|
419
499
|
*/
|
|
420
|
-
export function recordVaultQuota(path, input) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// A bound account is `unclaimed`: the vault's list has not named it, and
|
|
428
|
-
// the next projection keeps it as the account this credential last
|
|
429
|
-
// logged into until the vault names one.
|
|
430
|
-
const bound = fence === 'bind'
|
|
431
|
-
? { accountIdentity: input.accountIdentity, unclaimed: true }
|
|
432
|
-
: {};
|
|
433
|
-
return {
|
|
434
|
-
next: {
|
|
435
|
-
...current,
|
|
436
|
-
rows: current.rows.map((entry) => entry === row ? { ...entry, ...bound, quota } : entry),
|
|
437
|
-
},
|
|
438
|
-
result: true,
|
|
439
|
-
};
|
|
500
|
+
export async function recordVaultQuota(path, input) {
|
|
501
|
+
const { routeId, observation, ...receipt } = input;
|
|
502
|
+
return enqueueFencedVaultQuota(path, {
|
|
503
|
+
routeId,
|
|
504
|
+
observation,
|
|
505
|
+
accountIdentity: receipt.accountIdentity,
|
|
506
|
+
receipt,
|
|
440
507
|
});
|
|
441
508
|
}
|
|
509
|
+
// The answer callers rely on (false for a replaced account) comes from the
|
|
510
|
+
// fence, so it runs here against the current roster before anything is
|
|
511
|
+
// queued. The read takes no lock: the roster file is replaced atomically.
|
|
512
|
+
async function enqueueFencedVaultQuota(path, publication) {
|
|
513
|
+
if (!isQuotaObservation(publication.observation))
|
|
514
|
+
return false;
|
|
515
|
+
const current = pendingVaultQuota(path, await readVaultRoster(path));
|
|
516
|
+
if (!applyVaultQuota(current, publication).kept)
|
|
517
|
+
return false;
|
|
518
|
+
return enqueueVaultQuota(path, publication);
|
|
519
|
+
}
|
|
442
520
|
/**
|
|
443
521
|
* Merge a quota observation that belongs to an account rather than to a
|
|
444
522
|
* served credential, such as a background reading from insula. The write
|
|
445
523
|
* lands only while the row is still bound to that exact known account, so a
|
|
446
524
|
* reading taken before a replacement never reaches the successor. Unlike
|
|
447
525
|
* `recordVaultQuota` it never binds an account: a row without a known account
|
|
448
|
-
* is refused.
|
|
526
|
+
* is refused. Resolves false when the row is not bound to that account now,
|
|
527
|
+
* and true once the reading is queued, as `recordVaultQuota` does.
|
|
449
528
|
*/
|
|
450
529
|
export function recordVaultAccountQuota(path, input) {
|
|
451
|
-
return
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
};
|
|
466
|
-
|
|
530
|
+
return enqueueFencedVaultQuota(path, input);
|
|
531
|
+
}
|
|
532
|
+
/** Apply the same identity fence to memory overlays and durable quota batches. */
|
|
533
|
+
export function applyVaultQuota(current, input) {
|
|
534
|
+
const row = current?.rows.find((entry) => entry.routeId === input.routeId);
|
|
535
|
+
const fence = !row
|
|
536
|
+
? 'refuse'
|
|
537
|
+
: input.receipt
|
|
538
|
+
? receiptFence(row, input.receipt)
|
|
539
|
+
: row.accountIdentity !== undefined &&
|
|
540
|
+
row.accountIdentity === input.accountIdentity
|
|
541
|
+
? 'hold'
|
|
542
|
+
: 'refuse';
|
|
543
|
+
if (!current || !row || fence === 'refuse')
|
|
544
|
+
return { roster: current, kept: false };
|
|
545
|
+
const quota = mergeQuotaObservation(row.quota, input.observation);
|
|
546
|
+
// Binding remembers which account this credential proved, without claiming
|
|
547
|
+
// that the vault's discovery has named that account yet.
|
|
548
|
+
const bound = fence === 'bind'
|
|
549
|
+
? { accountIdentity: input.accountIdentity, unclaimed: true }
|
|
550
|
+
: {};
|
|
551
|
+
return {
|
|
552
|
+
roster: {
|
|
553
|
+
...current,
|
|
554
|
+
rows: current.rows.map((entry) => entry === row ? { ...entry, ...bound, quota } : entry),
|
|
555
|
+
},
|
|
556
|
+
kept: true,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const pendingDiscoveries = new Map();
|
|
560
|
+
export function forgetPendingVaultDiscovery(path) {
|
|
561
|
+
pendingDiscoveries.delete(resolve(path));
|
|
467
562
|
}
|
|
468
563
|
/**
|
|
469
564
|
* Discover and commit the roster. The lease covers the list call and the
|
|
@@ -471,12 +566,16 @@ export function recordVaultAccountQuota(path, input) {
|
|
|
471
566
|
* from a newer one, so a run that lost its lease is refused at commit. A
|
|
472
567
|
* failed list, or custody switching off mid-run, never writes anything; a
|
|
473
568
|
* peer holding the lease means its last committed roster is served instead.
|
|
569
|
+
* A write-lock timeout keeps the discovered inventory for the next poll,
|
|
570
|
+
* unless a peer has superseded its base view in the meantime.
|
|
474
571
|
*/
|
|
475
572
|
export async function refreshVaultRoster(options) {
|
|
476
573
|
options.signal?.throwIfAborted();
|
|
477
574
|
const active = async () => (await options.isActive?.()) ?? true;
|
|
478
|
-
if (!(await active()))
|
|
575
|
+
if (!(await active())) {
|
|
576
|
+
forgetPendingVaultDiscovery(options.path);
|
|
479
577
|
return undefined;
|
|
578
|
+
}
|
|
480
579
|
const lease = await acquireRefreshFileLock({
|
|
481
580
|
...DISCOVERY_LEASE,
|
|
482
581
|
path: options.path,
|
|
@@ -489,8 +588,25 @@ export async function refreshVaultRoster(options) {
|
|
|
489
588
|
throw new ClaustrumConsumerError('roster-busy', 'Claustrum account discovery is already in progress');
|
|
490
589
|
return persisted;
|
|
491
590
|
}
|
|
591
|
+
const key = resolve(options.path);
|
|
592
|
+
let discovered;
|
|
492
593
|
try {
|
|
493
|
-
const
|
|
594
|
+
const previous = await readVaultRoster(options.path);
|
|
595
|
+
const pending = pendingDiscoveries.get(key);
|
|
596
|
+
// Opaque view cursors cannot be ordered. Reuse a discovery only while no
|
|
597
|
+
// peer has committed a different view since it was read.
|
|
598
|
+
discovered =
|
|
599
|
+
pending &&
|
|
600
|
+
(pending.previousView === previous?.view ||
|
|
601
|
+
pending.inventory.view === previous?.view)
|
|
602
|
+
? pending
|
|
603
|
+
: {
|
|
604
|
+
inventory: await options.custody.discover(options.signal),
|
|
605
|
+
previousView: previous?.view,
|
|
606
|
+
};
|
|
607
|
+
pendingDiscoveries.delete(key);
|
|
608
|
+
const { inventory } = discovered;
|
|
609
|
+
options.onInventory?.(inventory);
|
|
494
610
|
options.signal?.throwIfAborted();
|
|
495
611
|
return await mutateVaultRoster(options.path, async (current) => {
|
|
496
612
|
if (!(await active()))
|
|
@@ -503,8 +619,21 @@ export async function refreshVaultRoster(options) {
|
|
|
503
619
|
const next = projectVaultRoster(current, inventory, projection);
|
|
504
620
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
505
621
|
return { ...(changed && { next }), result: next };
|
|
622
|
+
}, {
|
|
623
|
+
beforePublish: async () => {
|
|
624
|
+
options.signal?.throwIfAborted();
|
|
625
|
+
await lease.assertOwned();
|
|
626
|
+
},
|
|
506
627
|
});
|
|
507
628
|
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
if (error instanceof LockContentionError &&
|
|
631
|
+
discovered &&
|
|
632
|
+
!options.signal?.aborted &&
|
|
633
|
+
(await active()))
|
|
634
|
+
pendingDiscoveries.set(key, discovered);
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
508
637
|
finally {
|
|
509
638
|
await lease.release();
|
|
510
639
|
}
|