@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
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { createLogger } from "./cc9e0702a752cea7-index.js";
|
|
2
|
+
import { assertQuotaEpoch, assertQuotaRow, mergedQuota, } from "./a62a27a204266acd-attribution.js";
|
|
3
|
+
import { PoolOperationError } from "./04a6357627de5ed0-errors.js";
|
|
4
|
+
import { toFailure, withTransaction } from "./458b934e92490331-mutate.js";
|
|
5
|
+
import { LockStack } from "./43fc9e6b994519c7-refresh-lock.js";
|
|
6
|
+
import { readRow, refusal } from "./83bc3ba9d0fe2761-runtime.js";
|
|
7
|
+
import { entryIn } from "./cd94316a888b0a8e-schema.js";
|
|
8
|
+
const COALESCE_MS = 100;
|
|
9
|
+
/**
|
|
10
|
+
* The most readings one row keeps waiting for a flush. Hosts record a reading
|
|
11
|
+
* about every 100 ms at most, so for them a queue this long means another
|
|
12
|
+
* writer has held the store locks for many seconds. Reaching it starts
|
|
13
|
+
* a flush at once instead of waiting for the coalescing timer or a retry
|
|
14
|
+
* backoff; a further reading for the row waits for room (never dropping an
|
|
15
|
+
* older one) for as long as the store's normal lock budget, and is then
|
|
16
|
+
* refused with `lock-contention`, as the immediate writer would refuse it.
|
|
17
|
+
*/
|
|
18
|
+
export const POOL_QUOTA_QUEUE_LIMIT = 128;
|
|
19
|
+
// Readings are queued whole per row and applied at flush in arrival order
|
|
20
|
+
// through the codec's own merge, so the codec alone decides which fields a
|
|
21
|
+
// newer or older reading replaces. Like the roster writer, a reading stays
|
|
22
|
+
// queued until the flush that took it commits: one queued while a flush waits
|
|
23
|
+
// for its locks must survive that flush.
|
|
24
|
+
export class PoolQuotaWriter {
|
|
25
|
+
rt;
|
|
26
|
+
timeoutMs;
|
|
27
|
+
pending = new Map();
|
|
28
|
+
warned = new Set();
|
|
29
|
+
wake = new Set();
|
|
30
|
+
logger;
|
|
31
|
+
timer;
|
|
32
|
+
running;
|
|
33
|
+
closing;
|
|
34
|
+
closed = false;
|
|
35
|
+
stopped = false;
|
|
36
|
+
failures = 0;
|
|
37
|
+
constructor(rt, timeoutMs = 1_000) {
|
|
38
|
+
this.rt = rt;
|
|
39
|
+
this.timeoutMs = timeoutMs;
|
|
40
|
+
this.logger = rt.ctx.logger ?? createLogger('store');
|
|
41
|
+
}
|
|
42
|
+
async record(id, attribution, observation) {
|
|
43
|
+
try {
|
|
44
|
+
this.assertOpen(id);
|
|
45
|
+
assertQuotaEpoch(id, attribution);
|
|
46
|
+
// Snapshot before the first await: callers may reuse their headers object.
|
|
47
|
+
const publication = structuredClone({
|
|
48
|
+
id,
|
|
49
|
+
attribution,
|
|
50
|
+
observation,
|
|
51
|
+
});
|
|
52
|
+
const { row, result } = await readRow(this.rt, 'pull', id);
|
|
53
|
+
assertQuotaRow(id, publication.attribution, row);
|
|
54
|
+
// Merge into what read() shows now, so a reading the codec refuses is
|
|
55
|
+
// refused to its caller here, as the immediate writer would refuse it.
|
|
56
|
+
mergedQuota(this.rt.ctx, id, this.overlayRow({ ...row, quota: entryIn(result.config, id)?.quota })
|
|
57
|
+
.quota, publication.observation);
|
|
58
|
+
await this.room(id);
|
|
59
|
+
this.assertOpen(id);
|
|
60
|
+
// No await between the room check and the push, so the cap holds.
|
|
61
|
+
const queue = this.pending.get(id) ?? [];
|
|
62
|
+
queue.push(publication);
|
|
63
|
+
this.pending.set(id, queue);
|
|
64
|
+
if (queue.length >= POOL_QUOTA_QUEUE_LIMIT)
|
|
65
|
+
this.flushNow();
|
|
66
|
+
else
|
|
67
|
+
this.schedule(COALESCE_MS);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw toFailure(error, 'pull', id, { writes: 0 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
queued(id) {
|
|
74
|
+
return this.pending.get(id)?.length ?? 0;
|
|
75
|
+
}
|
|
76
|
+
// Waits while the row's queue is full, flushing at once on every wake-up,
|
|
77
|
+
// for at most the store's own lock acquisition budget.
|
|
78
|
+
async room(id) {
|
|
79
|
+
if (this.queued(id) < POOL_QUOTA_QUEUE_LIMIT)
|
|
80
|
+
return;
|
|
81
|
+
const { ctx } = this.rt;
|
|
82
|
+
const budget = Math.max(0, ...ctx.storeLocks.map((spec) => spec.timeoutMs ?? ctx.lockDefaults.timeoutMs));
|
|
83
|
+
const deadline = performance.now() + budget;
|
|
84
|
+
while (this.queued(id) >= POOL_QUOTA_QUEUE_LIMIT) {
|
|
85
|
+
this.assertOpen(id);
|
|
86
|
+
this.flushNow();
|
|
87
|
+
const left = deadline - performance.now();
|
|
88
|
+
if (left <= 0)
|
|
89
|
+
throw refusal('pull', id, 'lock-contention', `pool quota queue for ${id} is full (${POOL_QUOTA_QUEUE_LIMIT} readings) and the store locks stayed held`, true);
|
|
90
|
+
await new Promise((resolve) => {
|
|
91
|
+
const done = () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
this.wake.delete(done);
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
const timer = setTimeout(done, left);
|
|
97
|
+
this.wake.add(done);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
assertOpen(id) {
|
|
102
|
+
if (this.closed)
|
|
103
|
+
throw refusal('pull', id, 'invalid-input', 'pool quota writer is closed');
|
|
104
|
+
}
|
|
105
|
+
// What read() and load() show: the stored map, then each queued reading of
|
|
106
|
+
// the row still bound to it, in arrival order through the codec's merge.
|
|
107
|
+
// A reading the codec now refuses is left out, as the flush drops it.
|
|
108
|
+
overlayRow(row) {
|
|
109
|
+
let next = row;
|
|
110
|
+
for (const publication of this.pending.get(row.id) ?? []) {
|
|
111
|
+
try {
|
|
112
|
+
assertQuotaRow(row.id, publication.attribution, row);
|
|
113
|
+
next = {
|
|
114
|
+
...next,
|
|
115
|
+
quota: mergedQuota(this.rt.ctx, row.id, next.quota, publication.observation),
|
|
116
|
+
needsFirstReading: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch { }
|
|
120
|
+
}
|
|
121
|
+
return next;
|
|
122
|
+
}
|
|
123
|
+
overlay(rows) {
|
|
124
|
+
return rows.map((row) => this.overlayRow(row));
|
|
125
|
+
}
|
|
126
|
+
schedule(delay) {
|
|
127
|
+
if (this.running || this.timer || !this.pending.size || this.stopped)
|
|
128
|
+
return;
|
|
129
|
+
this.timer = setTimeout(() => {
|
|
130
|
+
this.timer = undefined;
|
|
131
|
+
this.flush();
|
|
132
|
+
}, delay);
|
|
133
|
+
if (!this.closed)
|
|
134
|
+
this.timer.unref?.();
|
|
135
|
+
}
|
|
136
|
+
// Starts a flush now rather than at the coalescing timer or retry backoff;
|
|
137
|
+
// a flush already running is left to finish.
|
|
138
|
+
flushNow() {
|
|
139
|
+
if (this.running)
|
|
140
|
+
return;
|
|
141
|
+
clearTimeout(this.timer);
|
|
142
|
+
this.timer = undefined;
|
|
143
|
+
this.flush();
|
|
144
|
+
}
|
|
145
|
+
flush() {
|
|
146
|
+
if (this.running || this.stopped || !this.pending.size)
|
|
147
|
+
return;
|
|
148
|
+
// Every queued reading, each row's in arrival order.
|
|
149
|
+
const batch = [...this.pending.values()].flat();
|
|
150
|
+
// Short lock attempts allow retries to pick up freshly queued readings and
|
|
151
|
+
// bound shutdown even when the normal store acquisition budget is long.
|
|
152
|
+
const ctx = {
|
|
153
|
+
...this.rt.ctx,
|
|
154
|
+
storeLocks: this.rt.ctx.storeLocks.map((spec) => ({
|
|
155
|
+
...spec,
|
|
156
|
+
timeoutMs: Math.min(spec.timeoutMs ?? this.rt.ctx.lockDefaults.timeoutMs, 100),
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
const locks = new LockStack(ctx.lockDefaults, ctx.lockEnv);
|
|
160
|
+
this.running = (async () => {
|
|
161
|
+
try {
|
|
162
|
+
await withTransaction(ctx, locks, { writes: 0 }, { operation: 'pull', rowId: undefined }, async (tx) => {
|
|
163
|
+
if (this.stopped)
|
|
164
|
+
return;
|
|
165
|
+
let changed = false;
|
|
166
|
+
for (const publication of batch) {
|
|
167
|
+
const { id, attribution, observation } = publication;
|
|
168
|
+
const witness = JSON.stringify([
|
|
169
|
+
id,
|
|
170
|
+
attribution.credentialEpoch,
|
|
171
|
+
attribution.identity,
|
|
172
|
+
]);
|
|
173
|
+
try {
|
|
174
|
+
tx.assertNotStaged(id);
|
|
175
|
+
assertQuotaRow(id, attribution, tx.row(id));
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (!(error instanceof PoolOperationError))
|
|
179
|
+
throw error;
|
|
180
|
+
if (!this.warned.has(witness)) {
|
|
181
|
+
this.warned.add(witness);
|
|
182
|
+
this.warn('pool quota publication dropped; account binding changed', { id, kind: error.kind });
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const entry = tx.entry(id);
|
|
187
|
+
if (!entry)
|
|
188
|
+
continue;
|
|
189
|
+
let quota;
|
|
190
|
+
try {
|
|
191
|
+
quota = mergedQuota(ctx, id, entry.quota, observation);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
// The codec accepted this reading when it was queued, but not
|
|
195
|
+
// against what is stored now (another writer changed it).
|
|
196
|
+
// Drop only this reading: failing the transaction would retry
|
|
197
|
+
// the same refusal forever and hold back every other row.
|
|
198
|
+
const key = `codec:${witness}`;
|
|
199
|
+
if (!this.warned.has(key)) {
|
|
200
|
+
this.warned.add(key);
|
|
201
|
+
this.warn('pool quota publication dropped; the quota codec refused it', {
|
|
202
|
+
id,
|
|
203
|
+
error: error instanceof Error ? error.message : String(error),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
tx.setEntry(id, { ...entry, quota, needsFirstReading: false });
|
|
209
|
+
changed = true;
|
|
210
|
+
}
|
|
211
|
+
if (changed && !this.stopped)
|
|
212
|
+
await tx.commitConfig();
|
|
213
|
+
}, { completeTorn: false });
|
|
214
|
+
const flushed = new Set(batch);
|
|
215
|
+
for (const [id, queue] of this.pending) {
|
|
216
|
+
const rest = queue.filter((publication) => !flushed.has(publication));
|
|
217
|
+
if (rest.length)
|
|
218
|
+
this.pending.set(id, rest);
|
|
219
|
+
else
|
|
220
|
+
this.pending.delete(id);
|
|
221
|
+
}
|
|
222
|
+
this.failures = 0;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (!this.stopped && this.failures++ === 0)
|
|
226
|
+
this.warn('pool quota publication delayed; retrying', {
|
|
227
|
+
error: error instanceof Error ? error.message : String(error),
|
|
228
|
+
});
|
|
229
|
+
// Retain the mailbox on failure, including fresher readings queued meanwhile.
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
await locks.releaseAll();
|
|
233
|
+
this.running = undefined;
|
|
234
|
+
for (const wake of this.wake)
|
|
235
|
+
wake();
|
|
236
|
+
const delay = this.failures
|
|
237
|
+
? Math.min(5_000, 100 * 2 ** Math.min(this.failures, 6)) *
|
|
238
|
+
(0.5 + Math.random())
|
|
239
|
+
: COALESCE_MS;
|
|
240
|
+
this.schedule(delay);
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
243
|
+
}
|
|
244
|
+
warn(message, data) {
|
|
245
|
+
try {
|
|
246
|
+
this.logger.warn(message, data);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
/* Logging cannot stop publication. */
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
close() {
|
|
253
|
+
if (this.closing)
|
|
254
|
+
return this.closing;
|
|
255
|
+
this.closed = true;
|
|
256
|
+
this.closing = this.drain();
|
|
257
|
+
return this.closing;
|
|
258
|
+
}
|
|
259
|
+
async drain() {
|
|
260
|
+
clearTimeout(this.timer);
|
|
261
|
+
this.timer = undefined;
|
|
262
|
+
let deadline;
|
|
263
|
+
let wake;
|
|
264
|
+
const drained = new Promise((done) => {
|
|
265
|
+
wake = () => {
|
|
266
|
+
if (!this.pending.size)
|
|
267
|
+
done();
|
|
268
|
+
};
|
|
269
|
+
this.wake.add(wake);
|
|
270
|
+
wake();
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
this.flush();
|
|
274
|
+
await Promise.race([
|
|
275
|
+
drained,
|
|
276
|
+
new Promise((done) => {
|
|
277
|
+
deadline = setTimeout(done, Math.max(0, this.timeoutMs));
|
|
278
|
+
}),
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
this.stopped = true;
|
|
283
|
+
clearTimeout(deadline);
|
|
284
|
+
clearTimeout(this.timer);
|
|
285
|
+
this.wake.delete(wake);
|
|
286
|
+
if (this.pending.size)
|
|
287
|
+
this.warn('pool quota drain timed out; pending readings left', {
|
|
288
|
+
accounts: this.pending.size,
|
|
289
|
+
readings: [...this.pending.values()].reduce((sum, queue) => sum + queue.length, 0),
|
|
290
|
+
});
|
|
291
|
+
this.pending.clear();
|
|
292
|
+
// A reading waiting for room in a full queue is refused as closed when
|
|
293
|
+
// the drain ends, not when its own lock budget runs out.
|
|
294
|
+
for (const wake of this.wake)
|
|
295
|
+
wake();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Use the same independently dated parts as mergeQuotaObservation. A header
|
|
2
|
+
// reading cannot erase a poll-only window, counter, credits record or budget.
|
|
3
|
+
export function quotaFields(publication, witness = '') {
|
|
4
|
+
const result = new Map();
|
|
5
|
+
const { observation } = publication;
|
|
6
|
+
const add = (key, at, part) => result.set(`${witness}:${key}`, {
|
|
7
|
+
at,
|
|
8
|
+
order: 0,
|
|
9
|
+
publication: { ...publication, observation: part },
|
|
10
|
+
});
|
|
11
|
+
const keyFor = (scope, label) => JSON.stringify([scope ?? 'all', label]);
|
|
12
|
+
const read = new Set();
|
|
13
|
+
for (const reading of observation.readings ?? []) {
|
|
14
|
+
const key = keyFor(reading.scope, reading.label);
|
|
15
|
+
read.add(key);
|
|
16
|
+
add(key, observation.checkedAt, {
|
|
17
|
+
checkedAt: observation.checkedAt,
|
|
18
|
+
readings: [{ ...reading }],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
for (const pair of observation.coverage ?? []) {
|
|
22
|
+
const key = keyFor(pair.scope, pair.label);
|
|
23
|
+
if (!read.has(key))
|
|
24
|
+
add(key, observation.checkedAt, {
|
|
25
|
+
checkedAt: observation.checkedAt,
|
|
26
|
+
coverage: [{ ...pair }],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (observation.budget)
|
|
30
|
+
add('budget', observation.checkedAt, {
|
|
31
|
+
checkedAt: observation.checkedAt,
|
|
32
|
+
budget: { ...observation.budget },
|
|
33
|
+
});
|
|
34
|
+
for (const [name, counter] of Object.entries(observation.counters ?? {})) {
|
|
35
|
+
add(`counter:${name}`, counter.checkedAt, {
|
|
36
|
+
checkedAt: observation.checkedAt,
|
|
37
|
+
counters: { [name]: { ...counter } },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (observation.credits)
|
|
41
|
+
add('credits', observation.credits.checkedAt, {
|
|
42
|
+
checkedAt: observation.checkedAt,
|
|
43
|
+
credits: { ...observation.credits },
|
|
44
|
+
});
|
|
45
|
+
if (!result.size)
|
|
46
|
+
add('empty', observation.checkedAt, { checkedAt: observation.checkedAt });
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
@@ -15,7 +15,7 @@ export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHO
|
|
|
15
15
|
export { acquireInsulaQuotaSource, } from "./d0f0b6be200311af-insula.js";
|
|
16
16
|
export { acceptAccount, declineAccount, isDeclined, } from "./7c140680fc81be92-interlock.js";
|
|
17
17
|
export { drainVaultQuota } from "./81dddd203f1b92a3-quota-writer.js";
|
|
18
|
-
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
18
|
+
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, reorderVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
19
19
|
export { ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, WATCH_RETRY_MS, } from "./dc9290c2a7552ba6-watch.js";
|
|
20
20
|
/**
|
|
21
21
|
* Connect the client that lists and fetches this consumer's vault credentials
|
|
@@ -5,7 +5,7 @@ import { readClaustrumEnrollmentToken } from "./b9588ad4725c1d10-enrollment.js";
|
|
|
5
5
|
import { ClaustrumConsumerError } from "./ca0fccaba1d87af4-errors.js";
|
|
6
6
|
import { acquireInsulaQuotaSource, } from "./d0f0b6be200311af-insula.js";
|
|
7
7
|
import { pendingVaultQuota, retainVaultQuotaWriter } from "./81dddd203f1b92a3-quota-writer.js";
|
|
8
|
-
import { acceptVaultRoute, applyVaultQuota, declineVaultRoute, forgetPendingVaultDiscovery, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
8
|
+
import { acceptVaultRoute, applyVaultQuota, declineVaultRoute, forgetPendingVaultDiscovery, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, reorderVaultRoster, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
9
9
|
import { SAFETY_POLL_MS } from "./dc9290c2a7552ba6-watch.js";
|
|
10
10
|
/**
|
|
11
11
|
* One plugin host's view of its vault accounts. It turns vault credentials
|
|
@@ -485,8 +485,12 @@ export class ClaustrumConsumer {
|
|
|
485
485
|
let current;
|
|
486
486
|
try {
|
|
487
487
|
current = await this.authorize(routeId, signal);
|
|
488
|
+
// Cancellation can land after authorize settles, before this continuation.
|
|
489
|
+
signal?.throwIfAborted();
|
|
488
490
|
}
|
|
489
|
-
catch {
|
|
491
|
+
catch (error) {
|
|
492
|
+
if (signal?.aborted)
|
|
493
|
+
throw error;
|
|
490
494
|
// Re-authorization failed: keep this 401 and report it against the
|
|
491
495
|
// receipt (served record version) this request was sent with.
|
|
492
496
|
}
|
|
@@ -522,6 +526,17 @@ export class ClaustrumConsumer {
|
|
|
522
526
|
await acceptVaultRoute(this.#options.rosterPath, routeId);
|
|
523
527
|
this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
|
|
524
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;
|
|
539
|
+
}
|
|
525
540
|
/**
|
|
526
541
|
* Store a quota or profile observation for a vault route. The receipt the
|
|
527
542
|
* reading was taken with is required: the observation lands only while the
|
|
@@ -378,6 +378,78 @@ export function acceptVaultRoute(path, routeId) {
|
|
|
378
378
|
};
|
|
379
379
|
});
|
|
380
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
|
+
}
|
|
381
453
|
/**
|
|
382
454
|
* What a receipt may do to a row's quota: `hold` (the row holds the receipt's
|
|
383
455
|
* credential and account), `bind` (the row has never had a known account and
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { lstat, readFile, rename, unlink } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
export const windowsPrivateFiles = process.platform === 'win32';
|
|
5
6
|
const migrationNotices = new Set();
|
|
6
7
|
function ioCode(detail) {
|
|
@@ -20,8 +21,51 @@ let backend;
|
|
|
20
21
|
export function setPrivateFilesBackendForTest(api) {
|
|
21
22
|
backend = api ? Promise.resolve(api) : undefined;
|
|
22
23
|
}
|
|
24
|
+
/** The fixed `reason` of a refusal because the Windows native addon did not load. */
|
|
25
|
+
export const PRIVATE_FILES_UNAVAILABLE_REASON = 'native-addon-unavailable';
|
|
26
|
+
function privateFilesUnavailable(cause) {
|
|
27
|
+
return Object.assign(new Error('Windows private-files native addon is unavailable', { cause }), {
|
|
28
|
+
code: 'ERR_PRIVATE_FILES_UNAVAILABLE',
|
|
29
|
+
reason: PRIVATE_FILES_UNAVAILABLE_REASON,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function isUnavailable(api, error) {
|
|
33
|
+
return (error instanceof api.PrivateFilesUnavailableError ||
|
|
34
|
+
error?.code ===
|
|
35
|
+
'ERR_PRIVATE_FILES_UNAVAILABLE');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Importing @cortexkit/private-files succeeds even when its Windows addon
|
|
39
|
+
* cannot load: the package loads the addon on first use, and some of its
|
|
40
|
+
* checks then report the failure as an ordinary refusal such as
|
|
41
|
+
* `unsupported-filesystem`. So prove the addon loads before handing the
|
|
42
|
+
* package out. Verifying a path that does not exist loads the addon first and
|
|
43
|
+
* then fails harmlessly; only the package's own "unavailable" error means the
|
|
44
|
+
* addon is missing.
|
|
45
|
+
*/
|
|
46
|
+
async function assertNativeAddonLoads(api) {
|
|
47
|
+
try {
|
|
48
|
+
await api.verifyPrivatePath(join(tmpdir(), `.common-auth-private-files-probe-${randomUUID()}`), 'file');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (isUnavailable(api, error))
|
|
52
|
+
throw privateFilesUnavailable(error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Load the package once per process. On Windows a failed import or addon load
|
|
57
|
+
* is kept, so every later private-file operation refuses at once with
|
|
58
|
+
* ERR_PRIVATE_FILES_UNAVAILABLE. There is no fallback to permission bits:
|
|
59
|
+
* they do not protect files on Windows.
|
|
60
|
+
*/
|
|
23
61
|
function loadBackend() {
|
|
24
|
-
backend ??= import("@cortexkit/private-files")
|
|
62
|
+
backend ??= import("@cortexkit/private-files").then(async (api) => {
|
|
63
|
+
if (windowsPrivateFiles)
|
|
64
|
+
await assertNativeAddonLoads(api);
|
|
65
|
+
return api;
|
|
66
|
+
}, (cause) => {
|
|
67
|
+
throw windowsPrivateFiles ? privateFilesUnavailable(cause) : cause;
|
|
68
|
+
});
|
|
25
69
|
return backend;
|
|
26
70
|
}
|
|
27
71
|
export class PrivateFileError extends Error {
|
|
@@ -38,7 +82,9 @@ export class PrivateFileError extends Error {
|
|
|
38
82
|
orphanPath;
|
|
39
83
|
constructor(path, stage, cause) {
|
|
40
84
|
const detail = cause;
|
|
41
|
-
super(
|
|
85
|
+
super(detail?.code === 'ERR_PRIVATE_FILES_UNAVAILABLE'
|
|
86
|
+
? `Private file refused at ${stage}: ${path} (${PRIVATE_FILES_UNAVAILABLE_REASON}). The Windows native addon of @cortexkit/private-files could not be loaded, so owner-only access cannot be enforced and nothing was read or written. Reinstall the plugin so the private-files Windows prebuild for this architecture is present; Windows has no safe fallback to permission bits.`
|
|
87
|
+
: `Private file refused at ${stage}: ${path} (${detail?.reason ?? detail?.code ?? 'io-error'}). Keep this file in a folder that only you, the system or administrators can change, such as a folder in your home directory. If other users could read the file before, sign in again to replace the credentials it held. On Windows the file must also be on a local drive that keeps Windows file permissions, such as NTFS.`, { cause });
|
|
42
88
|
this.path = path;
|
|
43
89
|
this.stage = stage;
|
|
44
90
|
this.name = 'PrivateFileError';
|