@cortexkit/opencode-openai-auth 1.2.0 → 1.4.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-CjXETpTA.d.ts → codex-request-hw0XcldE.d.ts} +146 -29
- package/dist/index-1eq2s1cp.js +2 -0
- package/dist/{index-qp5hdpw8.js → index-1tdnrt68.js} +1 -1
- package/dist/index-5230x6s8.js +2 -0
- package/dist/{index-qxbchh2d.js → index-ag04nyv7.js} +7 -7
- package/dist/index-brbqqer5.js +90 -0
- package/dist/{index-xwvze96g.js → index-d98f1xdp.js} +1 -1
- package/dist/index-jcyrm58f.js +65 -0
- package/dist/{index-htvcmtcx.js → index-rsvnabbe.js} +2 -2
- package/dist/{index-ftkhhr6d.js → index-s88n2bjn.js} +1 -1
- package/dist/{index-bc89akak.js → index-t5m0dhmq.js} +1 -1
- package/dist/index-vy729a0b.js +69 -0
- package/dist/index-wgwze391.js +2 -0
- package/dist/{index-ffg0sgg5.js → index-yhw3hnsh.js} +2 -2
- package/dist/index.d.ts +11 -3
- package/dist/index.js +1 -1
- package/dist/{model-CQcw10oW.d.ts → model-DphbMQce.d.ts} +1 -1
- package/dist/{port-file-BDJnzgHH.d.ts → port-file-Cx-wwe5Q.d.ts} +1 -1
- package/dist/{protocol-C-kagE6O.d.ts → protocol-CDqdjh74.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-BfQ4mpZO.d.ts → sidebar-state-pTd9gg9W.d.ts} +71 -7
- package/dist/sidebar-state.d.ts +2 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/{token-z6qq7a1d.js → token-0z5g59p8.js} +1 -1
- package/dist/token-util-jf9g6stk.js +1 -0
- 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 +6 -6
- package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +119 -6
- package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +294 -16
- package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +178 -26
- package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
- package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +100 -43
- package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +10 -2
- 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/81dddd203f1b92a3-quota-writer.js +253 -0
- package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +31 -6
- package/src/tui-compiled/raw/shared/94f53cfd572ba4bb-availability.js +106 -0
- package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +116 -12
- package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
- package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +348 -21
- 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 +254 -0
- package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +5 -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 +119 -6
- package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +294 -16
- package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +178 -26
- package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
- package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +100 -43
- package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +10 -2
- 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/81dddd203f1b92a3-quota-writer.js +253 -0
- package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +31 -6
- package/src/tui-compiled/runtime/shared/94f53cfd572ba4bb-availability.js +106 -0
- package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +116 -12
- package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
- package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +348 -21
- 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 +254 -0
- package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +5 -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-03em8hv1.js +0 -69
- package/dist/index-6n4pnfkg.js +0 -2
- package/dist/index-ehjj89rx.js +0 -2
- package/dist/index-f21addzm.js +0 -90
- package/dist/index-r2s51ac1.js +0 -2
- package/dist/index-rwbrhkse.js +0 -65
- package/dist/token-util-cf0nyh7n.js +0 -1
|
@@ -3,8 +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 {
|
|
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, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
9
|
+
import { SAFETY_POLL_MS } from "./dc9290c2a7552ba6-watch.js";
|
|
8
10
|
/**
|
|
9
11
|
* One plugin host's view of its vault accounts. It turns vault credentials
|
|
10
12
|
* into rows for `/routing`, authorizes each physical send with the vault, and
|
|
@@ -29,9 +31,18 @@ export class ClaustrumConsumer {
|
|
|
29
31
|
#timer;
|
|
30
32
|
#started = false;
|
|
31
33
|
#insula;
|
|
34
|
+
#vaultWatch;
|
|
35
|
+
#releaseInsulaWatch;
|
|
36
|
+
#insulaSkips = new Map();
|
|
37
|
+
/** When this instance last completed a discovery, by `now`. */
|
|
38
|
+
#discoveredAt;
|
|
39
|
+
#releaseQuotaWriter;
|
|
32
40
|
constructor(options) {
|
|
33
41
|
this.#options = options;
|
|
34
42
|
this.#logger = options.logger ?? createLogger('claustrum');
|
|
43
|
+
this.#releaseQuotaWriter = retainVaultQuotaWriter(options.rosterPath, (publication) => {
|
|
44
|
+
this.#roster = applyVaultQuota(this.#roster, publication).roster;
|
|
45
|
+
});
|
|
35
46
|
}
|
|
36
47
|
#assertOpen(signal) {
|
|
37
48
|
this.#shutdown.signal.throwIfAborted();
|
|
@@ -91,6 +102,8 @@ export class ClaustrumConsumer {
|
|
|
91
102
|
requireAssertion: this.#options.requireAssertion,
|
|
92
103
|
now: this.#options.now,
|
|
93
104
|
logger: this.#logger,
|
|
105
|
+
setTimeoutImpl: this.#options.setTimeoutImpl,
|
|
106
|
+
clearTimeoutImpl: this.#options.clearTimeoutImpl,
|
|
94
107
|
});
|
|
95
108
|
if (this.#options.insula && client.insula) {
|
|
96
109
|
this.#insula = acquireInsulaQuotaSource(client.insula, this.#options.tokenPath);
|
|
@@ -122,6 +135,27 @@ export class ClaustrumConsumer {
|
|
|
122
135
|
? latest.reach
|
|
123
136
|
: undefined;
|
|
124
137
|
}
|
|
138
|
+
/** How this consumer currently learns about changes, without I/O. */
|
|
139
|
+
watchState() {
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
vault: this.#vaultWatch?.mode ?? 'polling',
|
|
142
|
+
...(this.#options.insula && {
|
|
143
|
+
insula: this.#insula?.source.watchMode() ?? 'polling',
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Close the watches with the connection they belong to. */
|
|
148
|
+
#stopWatch() {
|
|
149
|
+
this.#vaultWatch?.close();
|
|
150
|
+
this.#vaultWatch = undefined;
|
|
151
|
+
this.#releaseInsulaWatch?.();
|
|
152
|
+
this.#releaseInsulaWatch = undefined;
|
|
153
|
+
}
|
|
154
|
+
/** Whether any account is using an in-memory outage receipt, without I/O. */
|
|
155
|
+
keptReceiptState() {
|
|
156
|
+
return (this.#custody?.keptReceiptState() ??
|
|
157
|
+
Object.freeze({ active: false, since: null }));
|
|
158
|
+
}
|
|
125
159
|
/** Rows for `/routing`, from the last committed roster. */
|
|
126
160
|
routingRows() {
|
|
127
161
|
return vaultRoutingRows(this.#roster);
|
|
@@ -134,6 +168,7 @@ export class ClaustrumConsumer {
|
|
|
134
168
|
if (!(await this.#active())) {
|
|
135
169
|
this.#roster = undefined;
|
|
136
170
|
this.#grantReach = undefined;
|
|
171
|
+
this.#stopWatch();
|
|
137
172
|
this.#custody?.close();
|
|
138
173
|
this.#custody = undefined;
|
|
139
174
|
this.#insula?.release();
|
|
@@ -146,11 +181,9 @@ export class ClaustrumConsumer {
|
|
|
146
181
|
let discovered;
|
|
147
182
|
const roster = await refreshVaultRoster({
|
|
148
183
|
path: this.#options.rosterPath,
|
|
149
|
-
custody
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return discovered;
|
|
153
|
-
},
|
|
184
|
+
custody,
|
|
185
|
+
onInventory: (inventory) => {
|
|
186
|
+
discovered = inventory;
|
|
154
187
|
},
|
|
155
188
|
isActive: () => this.#active(),
|
|
156
189
|
signal: this.#shutdown.signal,
|
|
@@ -163,7 +196,8 @@ export class ClaustrumConsumer {
|
|
|
163
196
|
});
|
|
164
197
|
this.#assertOpen();
|
|
165
198
|
const changed = roster?.view !== this.#roster?.view;
|
|
166
|
-
this.#roster = roster;
|
|
199
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, roster);
|
|
200
|
+
custody.retain(roster?.rows ?? []);
|
|
167
201
|
// Each discovery replaces the previous value, including with unknown. A
|
|
168
202
|
// refresh that served a peer's roster discovered nothing and leaves it;
|
|
169
203
|
// grantReach() hides it once the roster's view no longer matches.
|
|
@@ -175,12 +209,23 @@ export class ClaustrumConsumer {
|
|
|
175
209
|
// notification, and the poll runs every few seconds.
|
|
176
210
|
if (roster && changed)
|
|
177
211
|
this.#options.onRoster?.(roster);
|
|
212
|
+
this.#discoveredAt = (this.#options.now ?? Date.now)();
|
|
178
213
|
return roster;
|
|
179
214
|
})().finally(() => {
|
|
180
215
|
this.#refreshing = undefined;
|
|
181
216
|
});
|
|
182
217
|
return this.#refreshing;
|
|
183
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Poll in the background and open the vault and insula watches. Until the
|
|
221
|
+
* vault watch is accepted, and whenever it is unavailable, every tick lists
|
|
222
|
+
* the vault exactly as before watches existed. While it is open a tick
|
|
223
|
+
* lists only when five minutes have passed since the last discovery, since
|
|
224
|
+
* the watch keeps no history; it still checks `isCustodyActive`, so a switch
|
|
225
|
+
* to local mode closes the connection as quickly as before. Insula is
|
|
226
|
+
* polled on the tick only while its own watch is not open. A host not in
|
|
227
|
+
* vault mode makes no request.
|
|
228
|
+
*/
|
|
184
229
|
start() {
|
|
185
230
|
if (this.#started)
|
|
186
231
|
return;
|
|
@@ -188,8 +233,10 @@ export class ClaustrumConsumer {
|
|
|
188
233
|
this.#started = true;
|
|
189
234
|
const tick = async () => {
|
|
190
235
|
try {
|
|
191
|
-
await this
|
|
236
|
+
if (await this.#listDue())
|
|
237
|
+
await this.refresh();
|
|
192
238
|
await this.refreshInsulaQuota();
|
|
239
|
+
this.#openWatch();
|
|
193
240
|
}
|
|
194
241
|
catch (error) {
|
|
195
242
|
if (!this.#shutdown.signal.aborted)
|
|
@@ -208,6 +255,53 @@ export class ClaustrumConsumer {
|
|
|
208
255
|
};
|
|
209
256
|
void tick();
|
|
210
257
|
}
|
|
258
|
+
/** Whether a background tick must list the vault now. */
|
|
259
|
+
async #listDue() {
|
|
260
|
+
if (this.#vaultWatch?.mode !== 'watching')
|
|
261
|
+
return true;
|
|
262
|
+
const now = (this.#options.now ?? Date.now)();
|
|
263
|
+
if (this.#discoveredAt === undefined ||
|
|
264
|
+
now - this.#discoveredAt >= SAFETY_POLL_MS)
|
|
265
|
+
return true;
|
|
266
|
+
// refresh() is what closes the connection when the host left vault mode.
|
|
267
|
+
return !(await this.#active());
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Open the vault and insula watches once a vault connection exists. Only
|
|
271
|
+
* reached in vault mode: a host in local mode never connects.
|
|
272
|
+
*/
|
|
273
|
+
#openWatch() {
|
|
274
|
+
const custody = this.#custody;
|
|
275
|
+
if (this.#shutdown.signal.aborted || !custody)
|
|
276
|
+
return;
|
|
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)
|
|
295
|
+
this.#options.onError?.(error);
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
if (this.#options.insula && this.#insula && !this.#releaseInsulaWatch)
|
|
299
|
+
this.#releaseInsulaWatch = this.#insula.source.watch({
|
|
300
|
+
provider: this.#options.insula.provider,
|
|
301
|
+
setTimeoutImpl: this.#options.setTimeoutImpl,
|
|
302
|
+
clearTimeoutImpl: this.#options.clearTimeoutImpl,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
211
305
|
/**
|
|
212
306
|
* Fetch on a background tick, at most once per minute across projects with
|
|
213
307
|
* the same daemon and enrollment token path. Admission must use the cache
|
|
@@ -258,7 +352,7 @@ export class ClaustrumConsumer {
|
|
|
258
352
|
return undefined;
|
|
259
353
|
try {
|
|
260
354
|
await readClaustrumEnrollmentToken(this.#options.tokenPath);
|
|
261
|
-
const roster = await readVaultRoster(this.#options.rosterPath);
|
|
355
|
+
const roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
262
356
|
const row = roster?.rows.find((entry) => entry.routeId === routeId);
|
|
263
357
|
return this.#normalizeInsulaQuota(row?.accountIdentity);
|
|
264
358
|
}
|
|
@@ -266,6 +360,44 @@ export class ClaustrumConsumer {
|
|
|
266
360
|
return undefined;
|
|
267
361
|
}
|
|
268
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
|
+
}
|
|
269
401
|
#normalizeInsulaQuota(accountIdentity) {
|
|
270
402
|
if (!this.#options.insula || !accountIdentity)
|
|
271
403
|
return undefined;
|
|
@@ -282,20 +414,33 @@ export class ClaustrumConsumer {
|
|
|
282
414
|
async authorize(routeId, signal) {
|
|
283
415
|
this.#assertOpen(signal);
|
|
284
416
|
const roster = this.#roster ?? (await this.#wait(this.refresh(), signal));
|
|
285
|
-
if (!roster || !(await this.#active()))
|
|
417
|
+
if (!roster || !(await this.#active())) {
|
|
418
|
+
this.#custody?.close();
|
|
419
|
+
this.#custody = undefined;
|
|
420
|
+
this.#stopWatch();
|
|
421
|
+
this.#insula?.release();
|
|
422
|
+
this.#insula = undefined;
|
|
286
423
|
throw new ClaustrumConsumerError('not-active', 'Claustrum scoped custody is not active');
|
|
287
|
-
|
|
424
|
+
}
|
|
425
|
+
const current = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
288
426
|
this.#assertOpen(signal);
|
|
289
427
|
const observed = roster.rows.find((row) => row.routeId === routeId);
|
|
290
428
|
const configured = current?.rows.find((row) => row.routeId === routeId);
|
|
291
|
-
if (configured && !configured.enabled)
|
|
429
|
+
if (configured && !configured.enabled) {
|
|
430
|
+
if (observed)
|
|
431
|
+
this.#custody?.forget(observed.credentialId);
|
|
292
432
|
throw new ClaustrumConsumerError('route-declined', 'Claustrum route is disabled');
|
|
433
|
+
}
|
|
293
434
|
if (!observed ||
|
|
294
435
|
!configured ||
|
|
295
436
|
configured.state !== 'active' ||
|
|
296
437
|
configured.credentialId !== observed.credentialId ||
|
|
297
|
-
configured.accountIdentity !== observed.accountIdentity
|
|
438
|
+
configured.accountIdentity !== observed.accountIdentity ||
|
|
439
|
+
configured.credentialType !== observed.credentialType) {
|
|
440
|
+
if (observed)
|
|
441
|
+
this.#custody?.forget(observed.credentialId);
|
|
298
442
|
throw new ClaustrumConsumerError('route-unavailable', 'Claustrum route is disabled, removed or changed');
|
|
443
|
+
}
|
|
299
444
|
const custody = await this.#wait(this.#getCustody(), signal);
|
|
300
445
|
const combined = combineAbortSignals([
|
|
301
446
|
this.#shutdown.signal,
|
|
@@ -308,7 +453,7 @@ export class ClaustrumConsumer {
|
|
|
308
453
|
...(configured.accountIdentity !== undefined && {
|
|
309
454
|
accountIdentity: configured.accountIdentity,
|
|
310
455
|
}),
|
|
311
|
-
}, combined.signal);
|
|
456
|
+
}, combined.signal, routeId);
|
|
312
457
|
}
|
|
313
458
|
finally {
|
|
314
459
|
combined.dispose();
|
|
@@ -332,6 +477,10 @@ export class ClaustrumConsumer {
|
|
|
332
477
|
const signal = options.signal;
|
|
333
478
|
let served = await this.authorize(routeId, signal);
|
|
334
479
|
let response = await dispatch(served, signal);
|
|
480
|
+
if (response.status === 401 && served.fromKeptReceipt) {
|
|
481
|
+
await this.reportFailure(served, 401, options.reporterSource ?? 'direct');
|
|
482
|
+
return response;
|
|
483
|
+
}
|
|
335
484
|
if (response.status === 401 && !signal?.aborted) {
|
|
336
485
|
let current;
|
|
337
486
|
try {
|
|
@@ -365,12 +514,13 @@ export class ClaustrumConsumer {
|
|
|
365
514
|
async decline(routeId) {
|
|
366
515
|
this.#assertOpen();
|
|
367
516
|
await declineVaultRoute(this.#options.rosterPath, routeId);
|
|
368
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
517
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
518
|
+
this.#custody?.retain(this.#roster?.rows ?? []);
|
|
369
519
|
}
|
|
370
520
|
async accept(routeId) {
|
|
371
521
|
this.#assertOpen();
|
|
372
522
|
await acceptVaultRoute(this.#options.rosterPath, routeId);
|
|
373
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
523
|
+
this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
374
524
|
}
|
|
375
525
|
/**
|
|
376
526
|
* Store a quota or profile observation for a vault route. The receipt the
|
|
@@ -380,7 +530,7 @@ export class ClaustrumConsumer {
|
|
|
380
530
|
*/
|
|
381
531
|
async recordQuota(routeId, observation, attempt) {
|
|
382
532
|
this.#assertOpen();
|
|
383
|
-
|
|
533
|
+
return recordVaultQuota(this.#options.rosterPath, {
|
|
384
534
|
routeId,
|
|
385
535
|
observation,
|
|
386
536
|
credentialId: attempt.credentialId,
|
|
@@ -392,27 +542,21 @@ export class ClaustrumConsumer {
|
|
|
392
542
|
expectedAccountIdentity: attempt.expectedAccountIdentity,
|
|
393
543
|
}),
|
|
394
544
|
});
|
|
395
|
-
if (kept)
|
|
396
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
397
|
-
return kept;
|
|
398
545
|
}
|
|
399
546
|
/**
|
|
400
547
|
* Store a background observation that belongs to an account rather than to
|
|
401
548
|
* a served credential (an insula reading from `cachedInsulaQuotaForRoute`).
|
|
402
549
|
* It lands only while the route is still bound to that known account, so no
|
|
403
550
|
* credential fetch is needed and a replaced account's reading is dropped.
|
|
404
|
-
*
|
|
551
|
+
* Memory updates immediately; durable publication is detached and coalesced.
|
|
405
552
|
*/
|
|
406
553
|
async recordAccountQuota(routeId, accountIdentity, observation) {
|
|
407
554
|
this.#assertOpen();
|
|
408
|
-
|
|
555
|
+
return recordVaultAccountQuota(this.#options.rosterPath, {
|
|
409
556
|
routeId,
|
|
410
557
|
accountIdentity,
|
|
411
558
|
observation,
|
|
412
559
|
});
|
|
413
|
-
if (kept)
|
|
414
|
-
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
415
|
-
return kept;
|
|
416
560
|
}
|
|
417
561
|
close() {
|
|
418
562
|
if (this.#shutdown.signal.aborted)
|
|
@@ -421,10 +565,18 @@ export class ClaustrumConsumer {
|
|
|
421
565
|
if (this.#timer)
|
|
422
566
|
(this.#options.clearTimeoutImpl ?? clearTimeout)(this.#timer);
|
|
423
567
|
this.#timer = undefined;
|
|
568
|
+
this.#stopWatch();
|
|
424
569
|
this.#custody?.close();
|
|
425
570
|
this.#insula?.release();
|
|
426
571
|
this.#insula = undefined;
|
|
427
572
|
this.#roster = undefined;
|
|
428
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();
|
|
429
581
|
}
|
|
430
582
|
}
|
|
@@ -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
|
});
|
|
@@ -415,55 +420,73 @@ function receiptFence(row, receipt) {
|
|
|
415
420
|
* write is fenced on the receipt the reading was taken with: it lands only
|
|
416
421
|
* while the row still holds that receipt's credential (as representative or
|
|
417
422
|
* alias) and account, so a slow read for a replaced account can never land on
|
|
418
|
-
* its successor.
|
|
423
|
+
* its successor. Resolves false when the fence refuses the reading against the
|
|
424
|
+
* roster as it stands now (on disk, plus readings this process has not
|
|
425
|
+
* written yet), and true once the reading is queued for the next write. The
|
|
426
|
+
* write applies the fence again, so a rebind between the two still refuses.
|
|
419
427
|
*/
|
|
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
|
-
};
|
|
428
|
+
export async function recordVaultQuota(path, input) {
|
|
429
|
+
const { routeId, observation, ...receipt } = input;
|
|
430
|
+
return enqueueFencedVaultQuota(path, {
|
|
431
|
+
routeId,
|
|
432
|
+
observation,
|
|
433
|
+
accountIdentity: receipt.accountIdentity,
|
|
434
|
+
receipt,
|
|
440
435
|
});
|
|
441
436
|
}
|
|
437
|
+
// The answer callers rely on (false for a replaced account) comes from the
|
|
438
|
+
// fence, so it runs here against the current roster before anything is
|
|
439
|
+
// queued. The read takes no lock: the roster file is replaced atomically.
|
|
440
|
+
async function enqueueFencedVaultQuota(path, publication) {
|
|
441
|
+
if (!isQuotaObservation(publication.observation))
|
|
442
|
+
return false;
|
|
443
|
+
const current = pendingVaultQuota(path, await readVaultRoster(path));
|
|
444
|
+
if (!applyVaultQuota(current, publication).kept)
|
|
445
|
+
return false;
|
|
446
|
+
return enqueueVaultQuota(path, publication);
|
|
447
|
+
}
|
|
442
448
|
/**
|
|
443
449
|
* Merge a quota observation that belongs to an account rather than to a
|
|
444
450
|
* served credential, such as a background reading from insula. The write
|
|
445
451
|
* lands only while the row is still bound to that exact known account, so a
|
|
446
452
|
* reading taken before a replacement never reaches the successor. Unlike
|
|
447
453
|
* `recordVaultQuota` it never binds an account: a row without a known account
|
|
448
|
-
* is refused.
|
|
454
|
+
* is refused. Resolves false when the row is not bound to that account now,
|
|
455
|
+
* and true once the reading is queued, as `recordVaultQuota` does.
|
|
449
456
|
*/
|
|
450
457
|
export function recordVaultAccountQuota(path, input) {
|
|
451
|
-
return
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
};
|
|
466
|
-
|
|
458
|
+
return enqueueFencedVaultQuota(path, input);
|
|
459
|
+
}
|
|
460
|
+
/** Apply the same identity fence to memory overlays and durable quota batches. */
|
|
461
|
+
export function applyVaultQuota(current, input) {
|
|
462
|
+
const row = current?.rows.find((entry) => entry.routeId === input.routeId);
|
|
463
|
+
const fence = !row
|
|
464
|
+
? 'refuse'
|
|
465
|
+
: input.receipt
|
|
466
|
+
? receiptFence(row, input.receipt)
|
|
467
|
+
: row.accountIdentity !== undefined &&
|
|
468
|
+
row.accountIdentity === input.accountIdentity
|
|
469
|
+
? 'hold'
|
|
470
|
+
: 'refuse';
|
|
471
|
+
if (!current || !row || fence === 'refuse')
|
|
472
|
+
return { roster: current, kept: false };
|
|
473
|
+
const quota = mergeQuotaObservation(row.quota, input.observation);
|
|
474
|
+
// Binding remembers which account this credential proved, without claiming
|
|
475
|
+
// that the vault's discovery has named that account yet.
|
|
476
|
+
const bound = fence === 'bind'
|
|
477
|
+
? { accountIdentity: input.accountIdentity, unclaimed: true }
|
|
478
|
+
: {};
|
|
479
|
+
return {
|
|
480
|
+
roster: {
|
|
481
|
+
...current,
|
|
482
|
+
rows: current.rows.map((entry) => entry === row ? { ...entry, ...bound, quota } : entry),
|
|
483
|
+
},
|
|
484
|
+
kept: true,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const pendingDiscoveries = new Map();
|
|
488
|
+
export function forgetPendingVaultDiscovery(path) {
|
|
489
|
+
pendingDiscoveries.delete(resolve(path));
|
|
467
490
|
}
|
|
468
491
|
/**
|
|
469
492
|
* Discover and commit the roster. The lease covers the list call and the
|
|
@@ -471,12 +494,16 @@ export function recordVaultAccountQuota(path, input) {
|
|
|
471
494
|
* from a newer one, so a run that lost its lease is refused at commit. A
|
|
472
495
|
* failed list, or custody switching off mid-run, never writes anything; a
|
|
473
496
|
* peer holding the lease means its last committed roster is served instead.
|
|
497
|
+
* A write-lock timeout keeps the discovered inventory for the next poll,
|
|
498
|
+
* unless a peer has superseded its base view in the meantime.
|
|
474
499
|
*/
|
|
475
500
|
export async function refreshVaultRoster(options) {
|
|
476
501
|
options.signal?.throwIfAborted();
|
|
477
502
|
const active = async () => (await options.isActive?.()) ?? true;
|
|
478
|
-
if (!(await active()))
|
|
503
|
+
if (!(await active())) {
|
|
504
|
+
forgetPendingVaultDiscovery(options.path);
|
|
479
505
|
return undefined;
|
|
506
|
+
}
|
|
480
507
|
const lease = await acquireRefreshFileLock({
|
|
481
508
|
...DISCOVERY_LEASE,
|
|
482
509
|
path: options.path,
|
|
@@ -489,8 +516,25 @@ export async function refreshVaultRoster(options) {
|
|
|
489
516
|
throw new ClaustrumConsumerError('roster-busy', 'Claustrum account discovery is already in progress');
|
|
490
517
|
return persisted;
|
|
491
518
|
}
|
|
519
|
+
const key = resolve(options.path);
|
|
520
|
+
let discovered;
|
|
492
521
|
try {
|
|
493
|
-
const
|
|
522
|
+
const previous = await readVaultRoster(options.path);
|
|
523
|
+
const pending = pendingDiscoveries.get(key);
|
|
524
|
+
// Opaque view cursors cannot be ordered. Reuse a discovery only while no
|
|
525
|
+
// peer has committed a different view since it was read.
|
|
526
|
+
discovered =
|
|
527
|
+
pending &&
|
|
528
|
+
(pending.previousView === previous?.view ||
|
|
529
|
+
pending.inventory.view === previous?.view)
|
|
530
|
+
? pending
|
|
531
|
+
: {
|
|
532
|
+
inventory: await options.custody.discover(options.signal),
|
|
533
|
+
previousView: previous?.view,
|
|
534
|
+
};
|
|
535
|
+
pendingDiscoveries.delete(key);
|
|
536
|
+
const { inventory } = discovered;
|
|
537
|
+
options.onInventory?.(inventory);
|
|
494
538
|
options.signal?.throwIfAborted();
|
|
495
539
|
return await mutateVaultRoster(options.path, async (current) => {
|
|
496
540
|
if (!(await active()))
|
|
@@ -503,8 +547,21 @@ export async function refreshVaultRoster(options) {
|
|
|
503
547
|
const next = projectVaultRoster(current, inventory, projection);
|
|
504
548
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
505
549
|
return { ...(changed && { next }), result: next };
|
|
550
|
+
}, {
|
|
551
|
+
beforePublish: async () => {
|
|
552
|
+
options.signal?.throwIfAborted();
|
|
553
|
+
await lease.assertOwned();
|
|
554
|
+
},
|
|
506
555
|
});
|
|
507
556
|
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
if (error instanceof LockContentionError &&
|
|
559
|
+
discovered &&
|
|
560
|
+
!options.signal?.aborted &&
|
|
561
|
+
(await active()))
|
|
562
|
+
pendingDiscoveries.set(key, discovered);
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
508
565
|
finally {
|
|
509
566
|
await lease.release();
|
|
510
567
|
}
|