@cortexkit/opencode-openai-auth 1.4.0 → 1.6.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-hw0XcldE.d.ts → codex-request-D_In8uaR.d.ts} +52 -27
- package/dist/{index-1eq2s1cp.js → index-1qxz4s8a.js} +2 -2
- package/dist/{index-1tdnrt68.js → index-79y7w9bz.js} +1 -1
- package/dist/index-7nmdsmpp.js +2 -0
- package/dist/{index-ag04nyv7.js → index-8xq099qf.js} +2 -2
- package/dist/index-agaatt62.js +65 -0
- package/dist/{index-wgwze391.js → index-b0ty9qam.js} +2 -2
- package/dist/index-ge60xry3.js +90 -0
- package/dist/{index-s88n2bjn.js → index-jkpg9d1p.js} +1 -1
- package/dist/index-k5anexvs.js +69 -0
- package/dist/{index-yhw3hnsh.js → index-rf0ht3gc.js} +2 -2
- package/dist/{index-t5m0dhmq.js → index-x9nffaye.js} +1 -1
- package/dist/{index-d98f1xdp.js → index-ya9c5e4m.js} +1 -1
- package/dist/{index-rsvnabbe.js → index-z8bptbrk.js} +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{model-DphbMQce.d.ts → model-D22uj0OB.d.ts} +1 -1
- package/dist/{port-file-Cx-wwe5Q.d.ts → port-file-8ElfwDcq.d.ts} +1 -1
- package/dist/{protocol-CDqdjh74.d.ts → protocol-CK9guiFy.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-pTd9gg9W.d.ts → sidebar-state-Bb9L9oPq.d.ts} +4 -4
- package/dist/sidebar-state.d.ts +1 -1
- package/dist/sidebar-state.js +1 -1
- package/dist/token-util-nyvn9866.js +1 -0
- package/dist/{token-0z5g59p8.js → token-xtq3457d.js} +1 -1
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +2 -2
- package/dist/v2/server.d.ts +2 -2
- package/dist/v2/server.js +2 -2
- package/package.json +3 -3
- package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +17 -2
- package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +72 -0
- package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +49 -3
- package/src/tui-compiled/raw/shared/754d6017f8e419cf-builtins.js +95 -13
- package/src/tui-compiled/raw/shared/81dddd203f1b92a3-quota-writer.js +4 -54
- package/src/tui-compiled/raw/shared/a62a27a204266acd-attribution.js +39 -25
- package/src/tui-compiled/raw/shared/a910277ac43a8626-pull.js +3 -1
- package/src/tui-compiled/raw/shared/b9588ad4725c1d10-enrollment.js +27 -0
- package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +18 -3
- package/src/tui-compiled/raw/shared/c911dbb64c9d85c7-quota-writer.js +298 -0
- package/src/tui-compiled/raw/shared/e1c5e56df0ba4053-pending.js +48 -0
- package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +1 -1
- package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +17 -2
- package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +72 -0
- package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +49 -3
- package/src/tui-compiled/runtime/shared/754d6017f8e419cf-builtins.js +95 -13
- package/src/tui-compiled/runtime/shared/81dddd203f1b92a3-quota-writer.js +4 -54
- package/src/tui-compiled/runtime/shared/a62a27a204266acd-attribution.js +39 -25
- package/src/tui-compiled/runtime/shared/a910277ac43a8626-pull.js +3 -1
- package/src/tui-compiled/runtime/shared/b9588ad4725c1d10-enrollment.js +27 -0
- package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +18 -3
- package/src/tui-compiled/runtime/shared/c911dbb64c9d85c7-quota-writer.js +298 -0
- package/src/tui-compiled/runtime/shared/e1c5e56df0ba4053-pending.js +48 -0
- package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +1 -1
- package/dist/index-5230x6s8.js +0 -2
- package/dist/index-brbqqer5.js +0 -90
- package/dist/index-jcyrm58f.js +0 -65
- package/dist/index-vy729a0b.js +0 -69
- package/dist/token-util-jf9g6stk.js +0 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// the same locks as any other writer of those files.
|
|
5
5
|
import { formatQuota, isQuotaMap, projectQuota, quotaWindowName, } from "./d5a8f1061ee2f114-index.js";
|
|
6
6
|
import { DEFAULT_FORMER_MAIN_ID, orderForPlacement, resolveRoutingMode, } from "./3e7d45a77c6ad4d5-index.js";
|
|
7
|
-
import { projectFailure, } from "./94820e324f2b4c09-seam.js";
|
|
7
|
+
import { CommandError, projectFailure, } from "./94820e324f2b4c09-seam.js";
|
|
8
8
|
/** `disabledReason` recorded when the user disables an account from the menu. */
|
|
9
9
|
export const MENU_DISABLED_REASON = 'disabled from the command menu';
|
|
10
10
|
function isRecord(value) {
|
|
@@ -65,13 +65,16 @@ function addedText(name, result) {
|
|
|
65
65
|
function failureMessage(error) {
|
|
66
66
|
return projectFailure(error).text;
|
|
67
67
|
}
|
|
68
|
-
async function snapshot(
|
|
68
|
+
async function snapshot(options) {
|
|
69
|
+
const { store } = options;
|
|
69
70
|
const load = await store.read();
|
|
70
71
|
const read = await store.readSettings();
|
|
72
|
+
const vault = await options.routing?.vaultOrder?.();
|
|
71
73
|
return {
|
|
72
74
|
load,
|
|
73
75
|
rows: distinctRows(load),
|
|
74
76
|
settings: read.status === 'error' ? {} : read.settings,
|
|
77
|
+
...(vault ? { vault } : {}),
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
function accountView(row, options) {
|
|
@@ -323,6 +326,73 @@ function quotaSection(options, snap) {
|
|
|
323
326
|
},
|
|
324
327
|
};
|
|
325
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Sets the vault order. The roster's own refusal of the list (`invalid-order`:
|
|
331
|
+
* an account left out, which it counts without naming) is shown with its own
|
|
332
|
+
* message; any other failure is projected as usual. The kind is read from
|
|
333
|
+
* the error object so the menu does not import the vault client.
|
|
334
|
+
*/
|
|
335
|
+
async function reorderVault(vault, order) {
|
|
336
|
+
try {
|
|
337
|
+
return await vault.reorder(order);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
if (error instanceof Error &&
|
|
341
|
+
error.name === 'ClaustrumConsumerError' &&
|
|
342
|
+
error.kind === 'invalid-order')
|
|
343
|
+
throw new CommandError('vault-invalid-order', error.message, {
|
|
344
|
+
cause: error,
|
|
345
|
+
});
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function invalidVaultOrder(message) {
|
|
350
|
+
return new CommandError('vault-invalid-order', message);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* How the Routing section names vault accounts: by label, since route ids
|
|
354
|
+
* are long and opaque. A typed entry may be a label or a route id, and must
|
|
355
|
+
* name exactly one account. Refusals quote only the entry the user typed.
|
|
356
|
+
*/
|
|
357
|
+
function vaultNames(vault) {
|
|
358
|
+
const rows = vault.rows.map((row) => ({
|
|
359
|
+
id: row.id,
|
|
360
|
+
label: row.label.trim() || row.id,
|
|
361
|
+
}));
|
|
362
|
+
const matches = (entry) => rows.filter((row) => row.id === entry || row.label === entry);
|
|
363
|
+
return {
|
|
364
|
+
ids: rows.map((row) => row.id),
|
|
365
|
+
nameOf: (id) => rows.find((row) => row.id === id)?.label ?? id,
|
|
366
|
+
/**
|
|
367
|
+
* What the order input starts with for an account: its label when
|
|
368
|
+
* typing that label back names this account alone, else its route id
|
|
369
|
+
* (two accounts sharing a label, or a label with a comma or line break
|
|
370
|
+
* in it, either of which would split it into two entries).
|
|
371
|
+
*/
|
|
372
|
+
entryOf: (id) => {
|
|
373
|
+
const row = rows.find((entry) => entry.id === id);
|
|
374
|
+
return row && !/[,\n]/.test(row.label) && matches(row.label).length === 1
|
|
375
|
+
? row.label
|
|
376
|
+
: id;
|
|
377
|
+
},
|
|
378
|
+
/** The route ids the typed entries name, in order; refuses before any write. */
|
|
379
|
+
resolve: (entries) => {
|
|
380
|
+
const ids = [];
|
|
381
|
+
for (const entry of entries) {
|
|
382
|
+
const found = matches(entry);
|
|
383
|
+
if (found.length === 0)
|
|
384
|
+
throw invalidVaultOrder(`"${entry}" is not a vault account.`);
|
|
385
|
+
if (found.length > 1)
|
|
386
|
+
throw invalidVaultOrder(`"${entry}" matches more than one vault account; enter those accounts by route id.`);
|
|
387
|
+
const id = found[0].id;
|
|
388
|
+
if (ids.includes(id))
|
|
389
|
+
throw invalidVaultOrder(`"${entry}" names an account already in the order.`);
|
|
390
|
+
ids.push(id);
|
|
391
|
+
}
|
|
392
|
+
return ids;
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|
|
326
396
|
function routingSection(options, snap) {
|
|
327
397
|
const { store, extraLocks } = options;
|
|
328
398
|
const locks = extraLocks ? { extraLocks } : {};
|
|
@@ -336,14 +406,18 @@ function routingSection(options, snap) {
|
|
|
336
406
|
? raw
|
|
337
407
|
: 'ordered';
|
|
338
408
|
const resolved = resolveRoutingMode(current);
|
|
339
|
-
|
|
409
|
+
// In vault mode routing tries the vault accounts, so their order is the
|
|
410
|
+
// one shown and set here; the pool roster's order is not used.
|
|
411
|
+
const vault = snap.vault ? vaultNames(snap.vault) : undefined;
|
|
412
|
+
const ids = vault ? vault.ids : snap.rows.map((row) => row.id);
|
|
413
|
+
const nameOf = (id) => (vault ? vault.nameOf(id) : id);
|
|
340
414
|
const tried = orderForPlacement(ids, resolved.placement, options.routing?.formerMainId ?? DEFAULT_FORMER_MAIN_ID);
|
|
341
415
|
const label = choices.find((choice) => choice.value === current)?.label ?? current;
|
|
342
416
|
const lines = [`Mode: ${label}`];
|
|
343
417
|
if (ids.length > 0)
|
|
344
|
-
lines.push(
|
|
418
|
+
lines.push(`${vault ? 'Vault account order' : 'Roster order'}: ${ids.map(nameOf).join(', ')}`);
|
|
345
419
|
if (resolved.mode === 'ordered' && tried.join() !== ids.join())
|
|
346
|
-
lines.push(`Tried in order: ${tried.join(', ')}`);
|
|
420
|
+
lines.push(`Tried in order: ${tried.map(nameOf).join(', ')}`);
|
|
347
421
|
const actions = [
|
|
348
422
|
{
|
|
349
423
|
id: 'mode',
|
|
@@ -371,19 +445,27 @@ function routingSection(options, snap) {
|
|
|
371
445
|
{
|
|
372
446
|
kind: 'text',
|
|
373
447
|
id: 'order',
|
|
374
|
-
label:
|
|
375
|
-
|
|
448
|
+
label: vault
|
|
449
|
+
? 'Vault accounts, first to last, separated by commas'
|
|
450
|
+
: 'Account ids, first to last',
|
|
451
|
+
value: vault ? ids.map(vault.entryOf).join(', ') : ids.join(', '),
|
|
376
452
|
required: true,
|
|
377
453
|
},
|
|
378
454
|
],
|
|
379
455
|
run: async ({ values }) => {
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
456
|
+
const text = String(values.order);
|
|
457
|
+
// A vault label may contain spaces, so vault entries are separated by
|
|
458
|
+
// commas or line breaks, not spaces; pool ids never contain spaces.
|
|
459
|
+
const entries = text
|
|
460
|
+
.split(vault ? /[,\n]+/ : /[\s,]+/)
|
|
461
|
+
.map((entry) => entry.trim())
|
|
462
|
+
.filter((entry) => entry.length > 0);
|
|
463
|
+
const result = snap.vault && vault
|
|
464
|
+
? await reorderVault(snap.vault, vault.resolve(entries))
|
|
465
|
+
: await store.reorder(entries, locks);
|
|
384
466
|
return result.outcome === 'unchanged'
|
|
385
467
|
? 'The order is unchanged.'
|
|
386
|
-
: `Order set to ${result.ids.join(', ')}.`;
|
|
468
|
+
: `Order set to ${result.ids.map(nameOf).join(', ')}.`;
|
|
387
469
|
},
|
|
388
470
|
});
|
|
389
471
|
return {
|
|
@@ -524,7 +606,7 @@ function limitsSection(options, snap) {
|
|
|
524
606
|
}
|
|
525
607
|
/** The four built-in sections, in their fixed order, from one read of the store. */
|
|
526
608
|
export async function builtinSections(options) {
|
|
527
|
-
const snap = await snapshot(options
|
|
609
|
+
const snap = await snapshot(options);
|
|
528
610
|
return [
|
|
529
611
|
accountsSection(options, snap),
|
|
530
612
|
quotaSection(options, snap),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { createLogger } from "./cc9e0702a752cea7-index.js";
|
|
3
3
|
import { isQuotaObservation } from "./d5a8f1061ee2f114-index.js";
|
|
4
|
+
import { quotaFields } from "./e1c5e56df0ba4053-pending.js";
|
|
4
5
|
import { applyVaultQuota, mutateVaultRoster, } from "./30f5bc480f31f51c-roster.js";
|
|
5
6
|
// Every consumer of the same file shares one mailbox and one lock waiter.
|
|
6
7
|
const writers = new Map();
|
|
@@ -34,65 +35,14 @@ export function retainVaultQuotaWriter(path, listener) {
|
|
|
34
35
|
return writer.listeners.size ? Promise.resolve() : drainVaultQuota(path);
|
|
35
36
|
})());
|
|
36
37
|
}
|
|
37
|
-
//
|
|
38
|
-
// window (scope and label), each covered-but-unread window, the budget, each
|
|
39
|
-
// counter and the credits record. The mailbox keeps the newest of each part on
|
|
40
|
-
// its own, so a later observation that carries only some parts can't erase an
|
|
41
|
-
// older but still newest reading of another part, which is how
|
|
42
|
-
// mergeQuotaObservation treats them on disk. Each key also carries the
|
|
43
|
-
// receipt's identity, so the identity fence still applies to every part.
|
|
38
|
+
// Each field retains the receipt fence as well as its independent timestamp.
|
|
44
39
|
function fields(publication) {
|
|
45
|
-
|
|
46
|
-
const { observation } = publication;
|
|
47
|
-
const witness = JSON.stringify([
|
|
40
|
+
return quotaFields(publication, JSON.stringify([
|
|
48
41
|
publication.routeId,
|
|
49
42
|
publication.receipt?.credentialId,
|
|
50
43
|
publication.receipt?.accountIdentitySource,
|
|
51
44
|
publication.receipt?.expectedAccountIdentity,
|
|
52
|
-
]);
|
|
53
|
-
const add = (key, at, part) => result.set(`${witness}:${key}`, {
|
|
54
|
-
at,
|
|
55
|
-
order: 0,
|
|
56
|
-
publication: { ...publication, observation: part },
|
|
57
|
-
});
|
|
58
|
-
const keyFor = (scope, label) => JSON.stringify([scope ?? 'all', label]);
|
|
59
|
-
const read = new Set();
|
|
60
|
-
for (const reading of observation.readings ?? []) {
|
|
61
|
-
const key = keyFor(reading.scope, reading.label);
|
|
62
|
-
read.add(key);
|
|
63
|
-
add(key, observation.checkedAt, {
|
|
64
|
-
checkedAt: observation.checkedAt,
|
|
65
|
-
readings: [{ ...reading }],
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
for (const pair of observation.coverage ?? []) {
|
|
69
|
-
const key = keyFor(pair.scope, pair.label);
|
|
70
|
-
if (!read.has(key))
|
|
71
|
-
add(key, observation.checkedAt, {
|
|
72
|
-
checkedAt: observation.checkedAt,
|
|
73
|
-
coverage: [{ ...pair }],
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
if (observation.budget)
|
|
77
|
-
add('budget', observation.checkedAt, {
|
|
78
|
-
checkedAt: observation.checkedAt,
|
|
79
|
-
budget: { ...observation.budget },
|
|
80
|
-
});
|
|
81
|
-
for (const [name, counter] of Object.entries(observation.counters ?? {})) {
|
|
82
|
-
add(`counter:${name}`, counter.checkedAt, {
|
|
83
|
-
checkedAt: observation.checkedAt,
|
|
84
|
-
counters: { [name]: { ...counter } },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
if (observation.credits)
|
|
88
|
-
add('credits', observation.credits.checkedAt, {
|
|
89
|
-
checkedAt: observation.checkedAt,
|
|
90
|
-
credits: { ...observation.credits },
|
|
91
|
-
});
|
|
92
|
-
// Even an empty observation can bind an account proved by its receipt.
|
|
93
|
-
if (!result.size)
|
|
94
|
-
add('empty', observation.checkedAt, { checkedAt: observation.checkedAt });
|
|
95
|
-
return result;
|
|
45
|
+
]));
|
|
96
46
|
}
|
|
97
47
|
/** Enqueue without filesystem work; each quota field retains its own receipt fence. */
|
|
98
48
|
export function enqueueVaultQuota(path, publication) {
|
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
import { PoolOperationError } from "./04a6357627de5ed0-errors.js";
|
|
2
|
-
import { toFailure, withTransaction } from "./458b934e92490331-mutate.js";
|
|
2
|
+
import { toFailure, withTransaction, } from "./458b934e92490331-mutate.js";
|
|
3
3
|
import { LockStack } from "./43fc9e6b994519c7-refresh-lock.js";
|
|
4
4
|
import { refusal, requireBound, unknownRow, } from "./83bc3ba9d0fe2761-runtime.js";
|
|
5
5
|
import { isCredentialEpoch } from "./cd94316a888b0a8e-schema.js";
|
|
6
|
+
export function assertQuotaEpoch(id, attribution) {
|
|
7
|
+
if (!isCredentialEpoch(attribution?.credentialEpoch))
|
|
8
|
+
throw refusal('pull', id, 'invalid-input', 'the credential epoch the reading was issued for must be a positive safe integer');
|
|
9
|
+
}
|
|
10
|
+
// Shared by preflight and the locked commit: accepting into memory must never
|
|
11
|
+
// hide a refusal the immediate writer would have returned to its caller.
|
|
12
|
+
export function assertQuotaRow(id, attribution, row) {
|
|
13
|
+
if (!row)
|
|
14
|
+
throw unknownRow('pull', id);
|
|
15
|
+
if (row.invalid)
|
|
16
|
+
throw refusal('pull', id, 'invalid-row', `row ${id} is invalid`);
|
|
17
|
+
if (!row.credential)
|
|
18
|
+
throw refusal('pull', id, 'no-credential', `row ${id} holds no credential`);
|
|
19
|
+
requireBound('pull', row);
|
|
20
|
+
if (row.torn ||
|
|
21
|
+
!row.hasEntry ||
|
|
22
|
+
row.credentialEpoch !== attribution.credentialEpoch ||
|
|
23
|
+
row.identity !== attribution.identity)
|
|
24
|
+
throw new PoolOperationError({
|
|
25
|
+
operation: 'pull',
|
|
26
|
+
rowId: id,
|
|
27
|
+
phase: 'pull',
|
|
28
|
+
retryable: true,
|
|
29
|
+
kind: 'attribution',
|
|
30
|
+
message: `quota for ${id} was issued for a credential the row no longer holds`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function mergedQuota(ctx, id, stored, observation) {
|
|
34
|
+
const merged = ctx.codec.merge(stored, observation);
|
|
35
|
+
if (!ctx.codec.validate(merged))
|
|
36
|
+
throw refusal('pull', id, 'invalid-quota', 'the quota codec rejected the merged map');
|
|
37
|
+
return merged;
|
|
38
|
+
}
|
|
6
39
|
/**
|
|
7
40
|
* Merges a quota observation into a row's stored map under the store locks,
|
|
8
41
|
* after attribution passes. Needs no row lock, so it is permitted from
|
|
@@ -13,37 +46,18 @@ export async function recordQuota(rt, id, attribution, observation) {
|
|
|
13
46
|
const locks = new LockStack(ctx.lockDefaults, ctx.lockEnv);
|
|
14
47
|
const progress = { writes: 0 };
|
|
15
48
|
try {
|
|
16
|
-
|
|
17
|
-
throw refusal('pull', id, 'invalid-input', 'the credential epoch the reading was issued for must be a positive safe integer');
|
|
49
|
+
assertQuotaEpoch(id, attribution);
|
|
18
50
|
await withTransaction(ctx, locks, progress, { operation: 'pull', rowId: id }, async (tx) => {
|
|
19
51
|
tx.assertNotStaged(id);
|
|
20
52
|
const row = tx.row(id);
|
|
21
|
-
if (!row)
|
|
22
|
-
throw unknownRow('pull', id);
|
|
23
|
-
if (row.invalid)
|
|
24
|
-
throw refusal('pull', id, 'invalid-row', `row ${id} is invalid`);
|
|
25
53
|
// A reading belongs to one (epoch, identity, credential). A row
|
|
26
54
|
// holding no credential, or torn between the writes of a replace,
|
|
27
55
|
// has no such triple on disk, so no reading is recorded for it.
|
|
28
|
-
|
|
29
|
-
throw refusal('pull', id, 'no-credential', `row ${id} holds no credential`);
|
|
30
|
-
requireBound('pull', row);
|
|
56
|
+
assertQuotaRow(id, attribution, row);
|
|
31
57
|
const entry = tx.entry(id);
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
row.identity !== attribution.identity)
|
|
36
|
-
throw new PoolOperationError({
|
|
37
|
-
operation: 'pull',
|
|
38
|
-
rowId: id,
|
|
39
|
-
phase: 'pull',
|
|
40
|
-
retryable: true,
|
|
41
|
-
kind: 'attribution',
|
|
42
|
-
message: `quota for ${id} was issued for a credential the row no longer holds`,
|
|
43
|
-
});
|
|
44
|
-
const merged = ctx.codec.merge(entry.quota, observation);
|
|
45
|
-
if (!ctx.codec.validate(merged))
|
|
46
|
-
throw refusal('pull', id, 'invalid-quota', 'the quota codec rejected the merged map');
|
|
58
|
+
if (!entry)
|
|
59
|
+
throw unknownRow('pull', id);
|
|
60
|
+
const merged = mergedQuota(ctx, id, entry.quota, observation);
|
|
47
61
|
tx.setEntry(id, { ...entry, quota: merged, needsFirstReading: false });
|
|
48
62
|
await tx.commitConfig();
|
|
49
63
|
},
|
|
@@ -105,7 +105,9 @@ export class PullScheduler {
|
|
|
105
105
|
cause,
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
const publish = rt.publishQuota ??
|
|
109
|
+
((rowId, attribution, value) => recordQuota(rt, rowId, attribution, value));
|
|
110
|
+
await publish(id, {
|
|
109
111
|
credentialEpoch: request.credentialEpoch,
|
|
110
112
|
...(request.identity !== undefined
|
|
111
113
|
? { identity: request.identity }
|
|
@@ -10,12 +10,22 @@ const ENROLLMENT_SCHEMA = 1;
|
|
|
10
10
|
const ENROLLMENT_FILE_MAX_BYTES = 16 * 1024;
|
|
11
11
|
const ENROLLMENT_LOCK_TTL_MS = 30_000;
|
|
12
12
|
const TOKEN_RE = /^[0-9a-f]{64}$/;
|
|
13
|
+
/**
|
|
14
|
+
* The code a vault without the enrollment ceremony answers a proposal with.
|
|
15
|
+
* Setup reports it as "this vault cannot connect plugins" instead of waiting.
|
|
16
|
+
*/
|
|
17
|
+
export const UNSUPPORTED_ENROLLMENT_CODE = 'unknown_method';
|
|
13
18
|
/**
|
|
14
19
|
* Codes the vault's closed enrollment-refusal vocabulary marks permanent. The
|
|
15
20
|
* client labels some module error frames transient/retry regardless, so the
|
|
16
21
|
* producer's code takes precedence over the client's action for these.
|
|
17
22
|
*/
|
|
18
23
|
export const TERMINAL_ENROLLMENT_CODES = new Set([
|
|
24
|
+
// The daemon's answer to an operation it does not implement: a vault built
|
|
25
|
+
// before the enrollment ceremony. It arrives as a transport Error frame with
|
|
26
|
+
// no class, which claustrum-client before 0.13.0 labels transient/retry, but
|
|
27
|
+
// asking again can never succeed.
|
|
28
|
+
UNSUPPORTED_ENROLLMENT_CODE,
|
|
19
29
|
'invalid_params',
|
|
20
30
|
'pending_exists',
|
|
21
31
|
'not_found',
|
|
@@ -638,6 +648,23 @@ export class ClaustrumEnrollmentManager {
|
|
|
638
648
|
retryCode: refusal.code,
|
|
639
649
|
};
|
|
640
650
|
}
|
|
651
|
+
// A vault without the ceremony cannot hold a request for this
|
|
652
|
+
// proposal, now or from an earlier run (the state has no request
|
|
653
|
+
// id). So nothing is blocked and nothing is in progress: remove the
|
|
654
|
+
// pending state, still under the ceremony lock, so status reads as
|
|
655
|
+
// "not enrolled" and a reset or a later run starts afresh.
|
|
656
|
+
if (refusal.code === UNSUPPORTED_ENROLLMENT_CODE) {
|
|
657
|
+
await lock.assertOwned();
|
|
658
|
+
await unlink(this.#paths.statePath).catch((cause) => {
|
|
659
|
+
if (cause.code !== 'ENOENT')
|
|
660
|
+
throw cause;
|
|
661
|
+
});
|
|
662
|
+
return {
|
|
663
|
+
state: 'unavailable',
|
|
664
|
+
proposedName: state.proposedName,
|
|
665
|
+
code: refusal.code,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
641
668
|
return this.#block(state, refusal.code);
|
|
642
669
|
}
|
|
643
670
|
}
|
|
@@ -3,6 +3,7 @@ import { initializePool, readPool, } from "./458b934e92490331-mutate.js";
|
|
|
3
3
|
import { updateProviderStateRow, } from "./97484d9bb98c705b-provider-state.js";
|
|
4
4
|
import { publication, publishRoster, } from "./83068c0dbe8a6f13-publication.js";
|
|
5
5
|
import { PullScheduler } from "./a910277ac43a8626-pull.js";
|
|
6
|
+
import { PoolQuotaWriter } from "./c911dbb64c9d85c7-quota-writer.js";
|
|
6
7
|
import { refreshRow, } from "./9d3fc9e37456504e-refresh.js";
|
|
7
8
|
import { POOL_LOCK_DEFAULTS, } from "./43fc9e6b994519c7-refresh-lock.js";
|
|
8
9
|
import { addRow, disableRow, enableRow, recordRowIdentity, removeRow, reorderRows, replaceRow, rotateRow, } from "./fe02abeadc535f77-rows.js";
|
|
@@ -80,9 +81,21 @@ export function openPoolStore(options) {
|
|
|
80
81
|
rowLockOptions: options.rowLockOptions ?? {},
|
|
81
82
|
firePull: (id, reason) => pulls.fire(id, reason),
|
|
82
83
|
};
|
|
84
|
+
const quotaWriter = options.coalesceQuota
|
|
85
|
+
? new PoolQuotaWriter(rt, options.quotaDrainTimeoutMs)
|
|
86
|
+
: undefined;
|
|
87
|
+
const record = (id, attribution, observation) => quotaWriter
|
|
88
|
+
? quotaWriter.record(id, attribution, observation)
|
|
89
|
+
: recordQuota(rt, id, attribution, observation);
|
|
90
|
+
rt.publishQuota = record;
|
|
91
|
+
const overlay = (result) => {
|
|
92
|
+
if (result.status === 'ready' && quotaWriter)
|
|
93
|
+
return { ...result, rows: quotaWriter.overlay(result.rows) };
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
83
96
|
return {
|
|
84
97
|
async load() {
|
|
85
|
-
const result = await readPool(ctx);
|
|
98
|
+
const result = overlay(await readPool(ctx));
|
|
86
99
|
if (result.status === 'ready') {
|
|
87
100
|
// A torn row is no candidate, but its pull is fired too: the pull
|
|
88
101
|
// completes the row on disk before reading it, so an interrupted
|
|
@@ -96,7 +109,7 @@ export function openPoolStore(options) {
|
|
|
96
109
|
return toLoad(result);
|
|
97
110
|
},
|
|
98
111
|
async read() {
|
|
99
|
-
return toLoad(await readPool(ctx));
|
|
112
|
+
return toLoad(overlay(await readPool(ctx)));
|
|
100
113
|
},
|
|
101
114
|
async initialize(input = {}) {
|
|
102
115
|
return { status: await initializePool(ctx, input.dropKeys ?? []) };
|
|
@@ -115,7 +128,9 @@ export function openPoolStore(options) {
|
|
|
115
128
|
updateSettings: (mutator, callOptions) => updatePoolSettings(rt, mutator, callOptions),
|
|
116
129
|
recordIdentity: (id, identity, attribution, callOptions) => recordRowIdentity(rt, id, identity, attribution, callOptions),
|
|
117
130
|
refresh: (id, provider, callOptions) => refreshRow(rt, id, provider, callOptions),
|
|
118
|
-
recordQuota:
|
|
131
|
+
recordQuota: record,
|
|
132
|
+
close: () => quotaWriter?.close() ?? Promise.resolve(),
|
|
133
|
+
dispose: () => quotaWriter?.close() ?? Promise.resolve(),
|
|
119
134
|
requestReading: (id) => pulls.fire(id, 'admission'),
|
|
120
135
|
pullsSettled: () => pulls.settled(),
|
|
121
136
|
};
|