@cortexkit/opencode-openai-auth 1.1.0 → 1.3.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-BXDXToyt.d.ts → codex-request-B9D0e97Q.d.ts} +177 -28
- package/dist/{index-7c6sxtd6.js → index-1mbgsxsr.js} +2 -2
- package/dist/{index-4c8yw54k.js → index-2pgkrebq.js} +2 -2
- package/dist/index-4v2fs42x.js +65 -0
- package/dist/index-8x1aj7q3.js +2 -0
- package/dist/{index-f2fmb596.js → index-bc89akak.js} +1 -1
- package/dist/index-ex6nx3mq.js +2 -0
- package/dist/{index-zpm95mt5.js → index-ftkhhr6d.js} +1 -1
- package/dist/{index-msx2m78k.js → index-gn2b097f.js} +2 -2
- package/dist/{index-j8v2xwq6.js → index-m3ctrabv.js} +1 -1
- package/dist/index-psjyexj3.js +69 -0
- package/dist/{index-k6cydkgt.js → index-tddr876f.js} +2 -2
- package/dist/index-tn8x5tz8.js +90 -0
- package/dist/{index-csky2xpg.js → index-xwvze96g.js} +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/{model-YRg7Uowo.d.ts → model-BdITTmeb.d.ts} +1 -1
- package/dist/{port-file-CxEpgSHY.d.ts → port-file-DbHIVC9a.d.ts} +1 -1
- package/dist/{protocol-CDV1Pc6h.d.ts → protocol-MysYPAho.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-DjTWUbmf.d.ts → sidebar-state-CVSok7zv.d.ts} +114 -5
- package/dist/sidebar-state.d.ts +2 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/token-util-cf0nyh7n.js +1 -0
- package/dist/{token-rz4t3ay6.js → token-z6qq7a1d.js} +1 -1
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +2 -2
- package/dist/v2/server.d.ts +109 -2
- package/dist/v2/server.js +3 -3
- package/package.json +4 -4
- package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +203 -18
- package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +242 -16
- package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +168 -6
- package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +10 -2
- package/src/tui-compiled/raw/shared/94f53cfd572ba4bb-availability.js +106 -0
- package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +286 -21
- package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +1 -1
- package/src/tui-compiled/raw/shared/dc9290c2a7552ba6-watch.js +253 -0
- package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +4 -1
- package/src/tui-compiled/raw/sidebar-state.js +63 -1
- package/src/tui-compiled/raw/tui.tsx +82 -30
- package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +203 -18
- package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +242 -16
- package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +168 -6
- package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +10 -2
- package/src/tui-compiled/runtime/shared/94f53cfd572ba4bb-availability.js +106 -0
- package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +286 -21
- package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +1 -1
- package/src/tui-compiled/runtime/shared/dc9290c2a7552ba6-watch.js +253 -0
- package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +4 -1
- package/src/tui-compiled/runtime/sidebar-state.js +63 -1
- package/src/tui-compiled/runtime/tui.js +245 -208
- package/dist/index-0yh85bwb.js +0 -69
- package/dist/index-7eb3dw37.js +0 -90
- package/dist/index-8re8rprk.js +0 -2
- package/dist/index-edbdscy7.js +0 -65
- package/dist/index-m5qmvr6c.js +0 -2
- package/dist/map-Buq-GW7l.d.ts +0 -56
- package/dist/token-util-tpy2c7h6.js +0 -1
|
@@ -5,6 +5,8 @@ import { storeIdentity } from "@cortexkit/claustrum-client";
|
|
|
5
5
|
// import makes bundlers emit a separate chunk that a running host loses when
|
|
6
6
|
// its dist folder is rebuilt underneath it.
|
|
7
7
|
import { SubcClient } from "@cortexkit/subc-client";
|
|
8
|
+
import { classifyVaultFailure } from "./94f53cfd572ba4bb-availability.js";
|
|
9
|
+
import { ChangeNoticeReader, CoalescedRead, RESUBSCRIBE_BACKOFF_MS, WATCH_RETRY_MS, } from "./dc9290c2a7552ba6-watch.js";
|
|
8
10
|
/** Constructing this transport opens no connection; getUsage opens one per call. */
|
|
9
11
|
export function insulaUsageTransport(options) {
|
|
10
12
|
const identity = options.identity ??
|
|
@@ -49,6 +51,79 @@ export function insulaUsageTransport(options) {
|
|
|
49
51
|
client?.close();
|
|
50
52
|
}
|
|
51
53
|
},
|
|
54
|
+
watchUsage(onNotice) {
|
|
55
|
+
// The watch holds its own connection for as long as it is open: each
|
|
56
|
+
// usage.get above opens and closes a short-lived one, and the scoped
|
|
57
|
+
// credential connection is never shared with insula. The method name
|
|
58
|
+
// alone makes the request a watch; subc-client does not yet set the
|
|
59
|
+
// SUBSCRIPTION flag, and insula does not require it.
|
|
60
|
+
let client;
|
|
61
|
+
let subscription;
|
|
62
|
+
let stopped = false;
|
|
63
|
+
const closed = (async () => {
|
|
64
|
+
let deadline;
|
|
65
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
66
|
+
deadline = setTimeout(
|
|
67
|
+
// Coded as a timeout, so InsulaQuotaSource treats a watch that could
|
|
68
|
+
// not open in time as an unreachable insula and tries again later,
|
|
69
|
+
// not as a refusal.
|
|
70
|
+
() => reject(Object.assign(new Error('insula watch timeout'), {
|
|
71
|
+
code: 'timeout',
|
|
72
|
+
})), 3_000);
|
|
73
|
+
deadline.unref?.();
|
|
74
|
+
});
|
|
75
|
+
const open = (async () => {
|
|
76
|
+
const handshakeTimeoutMs = Math.min(options.handshakeTimeoutMs ?? 3_000, 3_000);
|
|
77
|
+
const connected = options.connector
|
|
78
|
+
? await options.connector({
|
|
79
|
+
connectionFile: options.connectionFile,
|
|
80
|
+
handshakeTimeoutMs,
|
|
81
|
+
})
|
|
82
|
+
: await SubcClient.connect({
|
|
83
|
+
connectionFile: options.connectionFile,
|
|
84
|
+
handshakeTimeoutMs,
|
|
85
|
+
});
|
|
86
|
+
client = connected;
|
|
87
|
+
// A connection that arrives after stop() or after the open deadline
|
|
88
|
+
// must not keep a route or socket alive.
|
|
89
|
+
if (stopped) {
|
|
90
|
+
connected.close();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const route = await connected.routeOpen({ kind: 'management_surface', module_id: 'insula' }, identity);
|
|
94
|
+
if (stopped) {
|
|
95
|
+
connected.close();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
subscription = connected.subscribe(route, { method: 'usage.watch' }, onNotice);
|
|
99
|
+
})();
|
|
100
|
+
try {
|
|
101
|
+
await Promise.race([open, timeout]);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
stopped = true;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
clearTimeout(deadline);
|
|
109
|
+
}
|
|
110
|
+
if (subscription)
|
|
111
|
+
await subscription.closed;
|
|
112
|
+
})().finally(() => {
|
|
113
|
+
stopped = true;
|
|
114
|
+
client?.close();
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
stop() {
|
|
118
|
+
if (stopped)
|
|
119
|
+
return;
|
|
120
|
+
stopped = true;
|
|
121
|
+
subscription?.unsubscribe();
|
|
122
|
+
client?.close();
|
|
123
|
+
},
|
|
124
|
+
closed,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
52
127
|
};
|
|
53
128
|
}
|
|
54
129
|
const MINUTE = 60_000;
|
|
@@ -60,18 +135,92 @@ const attempts = new Map();
|
|
|
60
135
|
function record(value) {
|
|
61
136
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
62
137
|
}
|
|
138
|
+
function decodeUsageNotice(body) {
|
|
139
|
+
let value;
|
|
140
|
+
try {
|
|
141
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (!record(value) ||
|
|
147
|
+
typeof value.kind !== 'string' ||
|
|
148
|
+
typeof value.epoch !== 'string' ||
|
|
149
|
+
value.epoch.length === 0 ||
|
|
150
|
+
typeof value.seq !== 'number' ||
|
|
151
|
+
!Number.isSafeInteger(value.seq) ||
|
|
152
|
+
value.seq < 1 ||
|
|
153
|
+
(value.provider !== undefined && typeof value.provider !== 'string'))
|
|
154
|
+
return undefined;
|
|
155
|
+
return {
|
|
156
|
+
kind: value.kind,
|
|
157
|
+
epoch: value.epoch,
|
|
158
|
+
seq: value.seq,
|
|
159
|
+
...(typeof value.provider === 'string' && { provider: value.provider }),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function refusalCode(error) {
|
|
163
|
+
return error instanceof Error &&
|
|
164
|
+
'code' in error &&
|
|
165
|
+
typeof error.code === 'string'
|
|
166
|
+
? error.code
|
|
167
|
+
: undefined;
|
|
168
|
+
}
|
|
63
169
|
export class InsulaQuotaSource {
|
|
64
170
|
transport;
|
|
65
171
|
key;
|
|
66
172
|
#entries = [];
|
|
67
|
-
|
|
173
|
+
/** Every usage.get, polled or watch-driven, shares this one slot. */
|
|
174
|
+
#reads = new CoalescedRead(async () => {
|
|
175
|
+
try {
|
|
176
|
+
const reply = await this.transport.getUsage();
|
|
177
|
+
if (!record(reply) ||
|
|
178
|
+
!Array.isArray(reply.result) ||
|
|
179
|
+
reply.error !== undefined)
|
|
180
|
+
throw new Error('unavailable insula usage');
|
|
181
|
+
// Each response is a snapshot, not a delta. Missing accounts fall back;
|
|
182
|
+
// neither pending nor the entry count asserts provider completeness.
|
|
183
|
+
this.#entries = reply.result.filter((entry) => record(entry) && typeof entry.provider === 'string');
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Refusal is absence, even if the previous reading was still young.
|
|
187
|
+
this.#entries = [];
|
|
188
|
+
}
|
|
189
|
+
}, () => { });
|
|
190
|
+
#reader = new ChangeNoticeReader();
|
|
191
|
+
#mode = 'polling';
|
|
192
|
+
#watchers = 0;
|
|
193
|
+
/**
|
|
194
|
+
* How many registered watchers read every provider. Watchers limited to one
|
|
195
|
+
* provider are counted per provider in `#providers` instead.
|
|
196
|
+
*/
|
|
197
|
+
#everyProvider = 0;
|
|
198
|
+
#providers = new Map();
|
|
199
|
+
#watch;
|
|
200
|
+
#timer;
|
|
201
|
+
#timers = {};
|
|
202
|
+
/**
|
|
203
|
+
* How many accepted watches in a row have closed; each one doubles the wait
|
|
204
|
+
* before the next re-subscribe. Reset once a watch brings a second notice.
|
|
205
|
+
*/
|
|
206
|
+
#closes = 0;
|
|
68
207
|
constructor(transport, key) {
|
|
69
208
|
this.transport = transport;
|
|
70
209
|
this.key = key;
|
|
71
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* The background poll: at most one usage.get per minute per daemon and
|
|
213
|
+
* enrollment token. While a watch is open it reads nothing, because insula's
|
|
214
|
+
* notices announce every new reading. If insula stops announcing an account,
|
|
215
|
+
* that account's last entry grows older than the freshness limit `current()`
|
|
216
|
+
* applies, `current()` returns nothing for it, and the plugin falls back to
|
|
217
|
+
* its own provider poll for that account.
|
|
218
|
+
*/
|
|
72
219
|
refresh(now) {
|
|
73
|
-
if (this.#
|
|
74
|
-
return this.#
|
|
220
|
+
if (this.#reads.running)
|
|
221
|
+
return this.#reads.running;
|
|
222
|
+
if (this.#mode === 'watching')
|
|
223
|
+
return Promise.resolve();
|
|
75
224
|
for (const [key, time] of attempts) {
|
|
76
225
|
if (now - time >= MINUTE)
|
|
77
226
|
attempts.delete(key);
|
|
@@ -81,25 +230,141 @@ export class InsulaQuotaSource {
|
|
|
81
230
|
return Promise.resolve();
|
|
82
231
|
// Reserve the minute before I/O, so concurrent projects cannot each poll.
|
|
83
232
|
attempts.set(this.key, now);
|
|
84
|
-
this.#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
233
|
+
return this.#reads.request();
|
|
234
|
+
}
|
|
235
|
+
/** How this source currently learns about usage changes, for logs. */
|
|
236
|
+
watchMode() {
|
|
237
|
+
return this.#mode;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Hold a `usage.watch` open while any watcher wants it, and re-read
|
|
241
|
+
* usage.get when a notice says something changed. Watchers of one source
|
|
242
|
+
* share one watch, since insula admits only 64 per process. Returns the
|
|
243
|
+
* release; the last release closes the watch.
|
|
244
|
+
*/
|
|
245
|
+
watch(options = {}) {
|
|
246
|
+
this.#watchers++;
|
|
247
|
+
const provider = options.provider;
|
|
248
|
+
if (provider === undefined)
|
|
249
|
+
this.#everyProvider++;
|
|
250
|
+
else
|
|
251
|
+
this.#providers.set(provider, (this.#providers.get(provider) ?? 0) + 1);
|
|
252
|
+
if (this.#watchers === 1) {
|
|
253
|
+
this.#timers = options;
|
|
254
|
+
this.#open();
|
|
255
|
+
}
|
|
256
|
+
let released = false;
|
|
257
|
+
return () => {
|
|
258
|
+
if (released)
|
|
259
|
+
return;
|
|
260
|
+
released = true;
|
|
261
|
+
if (provider === undefined)
|
|
262
|
+
this.#everyProvider--;
|
|
263
|
+
else {
|
|
264
|
+
const left = (this.#providers.get(provider) ?? 1) - 1;
|
|
265
|
+
if (left > 0)
|
|
266
|
+
this.#providers.set(provider, left);
|
|
267
|
+
else
|
|
268
|
+
this.#providers.delete(provider);
|
|
98
269
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
270
|
+
if (--this.#watchers === 0)
|
|
271
|
+
this.#stopWatch();
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
#stopWatch() {
|
|
275
|
+
if (this.#timer)
|
|
276
|
+
(this.#timers.clearTimeoutImpl ?? clearTimeout)(this.#timer);
|
|
277
|
+
this.#timer = undefined;
|
|
278
|
+
const watch = this.#watch;
|
|
279
|
+
this.#watch = undefined;
|
|
280
|
+
watch?.handle?.stop();
|
|
281
|
+
this.#mode = 'polling';
|
|
282
|
+
}
|
|
283
|
+
#later(delay) {
|
|
284
|
+
this.#timer = (this.#timers.setTimeoutImpl ?? setTimeout)(() => {
|
|
285
|
+
this.#timer = undefined;
|
|
286
|
+
if (this.#watchers > 0 && !this.#watch)
|
|
287
|
+
this.#open();
|
|
288
|
+
}, delay);
|
|
289
|
+
this.#timer.unref?.();
|
|
290
|
+
}
|
|
291
|
+
#open() {
|
|
292
|
+
const watchUsage = this.transport.watchUsage;
|
|
293
|
+
if (!watchUsage) {
|
|
294
|
+
// A custom transport that cannot watch: poll exactly as before.
|
|
295
|
+
this.#mode = 'fallback';
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.#mode = 'polling';
|
|
299
|
+
this.#reader.restart();
|
|
300
|
+
const watch = { notices: 0 };
|
|
301
|
+
this.#watch = watch;
|
|
302
|
+
let handle;
|
|
303
|
+
try {
|
|
304
|
+
handle = watchUsage.call(this.transport, (body) => this.#notice(watch, body));
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
this.#closed(watch, error);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
watch.handle = handle;
|
|
311
|
+
handle.closed.then(() => this.#closed(watch, undefined), (error) => this.#closed(watch, error));
|
|
312
|
+
}
|
|
313
|
+
#notice(watch, body) {
|
|
314
|
+
// Frames already in transport can arrive after a local stop.
|
|
315
|
+
if (this.#watch !== watch)
|
|
316
|
+
return;
|
|
317
|
+
watch.notices++;
|
|
318
|
+
if (watch.notices === 1)
|
|
319
|
+
this.#mode = 'watching';
|
|
320
|
+
// A second notice means the watch stayed up past its acceptance.
|
|
321
|
+
if (watch.notices === 2)
|
|
322
|
+
this.#closes = 0;
|
|
323
|
+
const notice = decodeUsageNotice(body);
|
|
324
|
+
// An unreadable notice may hide any change: treat it as a break, and
|
|
325
|
+
// make the next notice read as one too.
|
|
326
|
+
if (!notice)
|
|
327
|
+
this.#reader.restart();
|
|
328
|
+
const continuous = notice ? this.#reader.continuous(notice) : false;
|
|
329
|
+
// The first notice of every watch names no provider, so it always reads:
|
|
330
|
+
// nothing was watched before it. After a gap or a new epoch notices were
|
|
331
|
+
// lost, so read even when this notice names another provider.
|
|
332
|
+
if (!continuous ||
|
|
333
|
+
notice?.provider === undefined ||
|
|
334
|
+
this.#everyProvider > 0 ||
|
|
335
|
+
this.#providers.has(notice.provider))
|
|
336
|
+
void this.#reads.request();
|
|
337
|
+
}
|
|
338
|
+
#closed(watch, error) {
|
|
339
|
+
if (this.#watch !== watch)
|
|
340
|
+
return;
|
|
341
|
+
this.#watch = undefined;
|
|
342
|
+
const code = refusalCode(error);
|
|
343
|
+
if (code === 'unknown_method' || code === 'usage_watch_limit') {
|
|
344
|
+
// An insula that predates usage.watch, or one already holding 64
|
|
345
|
+
// watches: poll exactly as before until every watcher has released.
|
|
346
|
+
this.#mode = 'fallback';
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (watch.notices > 0) {
|
|
350
|
+
// An accepted watch that closed (insula shut down, or the connection
|
|
351
|
+
// dropped) gets one new watch, whose first notice reads usage.get.
|
|
352
|
+
const delay = Math.min(RESUBSCRIBE_BACKOFF_MS * 2 ** Math.min(this.#closes, 16), WATCH_RETRY_MS);
|
|
353
|
+
this.#closes++;
|
|
354
|
+
this.#mode = 'polling';
|
|
355
|
+
this.#later(delay);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// Closed before any notice, so never accepted: read once to learn
|
|
359
|
+
// whether insula answers at all, then poll as before. A refusal insula
|
|
360
|
+
// answered stands until every watcher has released. A close it did not
|
|
361
|
+
// explain (a
|
|
362
|
+
// dropped connection, a timeout, a stream ended at once while insula
|
|
363
|
+
// shuts down) earns one new watch much later.
|
|
364
|
+
this.#mode = 'fallback';
|
|
365
|
+
void this.#reads.request();
|
|
366
|
+
if (error === undefined || classifyVaultFailure(error) === 'unreachable')
|
|
367
|
+
this.#later(WATCH_RETRY_MS);
|
|
103
368
|
}
|
|
104
369
|
current(provider, accountId, now) {
|
|
105
370
|
for (const entry of this.#entries) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { quotaCodec } from "./053cc17b5419261e-codec.js";
|
|
2
|
-
export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
|
|
2
|
+
export { formatQuota, isQuotaStale, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
|
|
3
3
|
export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, decodeQuotaMap, emptyQuotaMap, isQuotaMap, } from "./905bffc0220f43d0-map.js";
|
|
4
4
|
export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from "./4f36d333d081d040-merge.js";
|
|
5
5
|
export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from "./39c3cb05738fcf6d-projection.js";
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* While a vault watch is healthy, the roster is still read this often: the
|
|
3
|
+
* watch carries no history, so a lost notice or a silently dead stream is
|
|
4
|
+
* caught within this period.
|
|
5
|
+
*/
|
|
6
|
+
export const SAFETY_POLL_MS = 5 * 60_000;
|
|
7
|
+
/**
|
|
8
|
+
* After a watch fails without being accepted, the host polls and tries one
|
|
9
|
+
* new watch this much later, so a refusing or draining module is never asked
|
|
10
|
+
* in a tight loop.
|
|
11
|
+
*/
|
|
12
|
+
export const WATCH_RETRY_MS = 5 * 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* First wait before re-subscribing a watch that closed after it had been
|
|
15
|
+
* accepted. It doubles for each further close in a row, up to WATCH_RETRY_MS.
|
|
16
|
+
*/
|
|
17
|
+
export const RESUBSCRIBE_BACKOFF_MS = 1_000;
|
|
18
|
+
/**
|
|
19
|
+
* Tells whether a notice follows the previous one with nothing lost between
|
|
20
|
+
* them. `seq` restarts at 1 for every watch and `epoch` names the sending
|
|
21
|
+
* module's process lifetime. A sequence gap (notices are sheddable), a
|
|
22
|
+
* different epoch (the module restarted) or a notice of an unknown kind all
|
|
23
|
+
* mean changes may have been missed, so the caller must re-read everything.
|
|
24
|
+
*/
|
|
25
|
+
export class ChangeNoticeReader {
|
|
26
|
+
#epoch;
|
|
27
|
+
#seq = 0;
|
|
28
|
+
/**
|
|
29
|
+
* Begin a new watch, whose first notice is seq 1 again. The epoch is kept,
|
|
30
|
+
* so a module restart between two watches still reads as a break.
|
|
31
|
+
*/
|
|
32
|
+
restart() {
|
|
33
|
+
this.#seq = 0;
|
|
34
|
+
}
|
|
35
|
+
continuous(notice) {
|
|
36
|
+
const continuous = notice.kind === 'changed' &&
|
|
37
|
+
notice.seq === this.#seq + 1 &&
|
|
38
|
+
(this.#epoch === undefined || notice.epoch === this.#epoch);
|
|
39
|
+
this.#epoch = notice.epoch;
|
|
40
|
+
this.#seq = notice.seq;
|
|
41
|
+
return continuous;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Runs `read` with at most one call in flight and at most one more queued
|
|
46
|
+
* behind it, however many requests arrive meanwhile. A request made while a
|
|
47
|
+
* read is running may have arrived after that read took its snapshot, so it
|
|
48
|
+
* earns exactly one more read; any further requests share that one.
|
|
49
|
+
*/
|
|
50
|
+
export class CoalescedRead {
|
|
51
|
+
#read;
|
|
52
|
+
#onError;
|
|
53
|
+
#running;
|
|
54
|
+
#queued = false;
|
|
55
|
+
constructor(read, onError) {
|
|
56
|
+
this.#read = read;
|
|
57
|
+
this.#onError = onError;
|
|
58
|
+
}
|
|
59
|
+
/** The read in flight (including its queued follow-up), if any. */
|
|
60
|
+
get running() {
|
|
61
|
+
return this.#running;
|
|
62
|
+
}
|
|
63
|
+
request() {
|
|
64
|
+
if (this.#running) {
|
|
65
|
+
this.#queued = true;
|
|
66
|
+
return this.#running;
|
|
67
|
+
}
|
|
68
|
+
this.#running = (async () => {
|
|
69
|
+
do {
|
|
70
|
+
this.#queued = false;
|
|
71
|
+
try {
|
|
72
|
+
await this.#read();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.#onError(error);
|
|
76
|
+
}
|
|
77
|
+
} while (this.#queued);
|
|
78
|
+
})().finally(() => {
|
|
79
|
+
this.#running = undefined;
|
|
80
|
+
});
|
|
81
|
+
return this.#running;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Keeps one vault `watchScoped` open for a host and runs the host's discovery
|
|
86
|
+
* when a notice shows the visible account list changed. Discovery runs only
|
|
87
|
+
* when the notice's view differs from the last discovered roster's view, when
|
|
88
|
+
* a notice shows a gap or a new epoch, or on the first notice of a watch that
|
|
89
|
+
* was re-subscribed after a close (the time between two watches was not
|
|
90
|
+
* observed). It never opens a second watch while one is open.
|
|
91
|
+
*/
|
|
92
|
+
export class ScopedRosterWatch {
|
|
93
|
+
#options;
|
|
94
|
+
#reader = new ChangeNoticeReader();
|
|
95
|
+
#reads;
|
|
96
|
+
#mode = 'polling';
|
|
97
|
+
#watch;
|
|
98
|
+
#timer;
|
|
99
|
+
#stopped = false;
|
|
100
|
+
#latestView;
|
|
101
|
+
#force = false;
|
|
102
|
+
/**
|
|
103
|
+
* How many accepted watches in a row have closed; each one doubles the wait
|
|
104
|
+
* before the next re-subscribe. Reset once a watch stays up five minutes.
|
|
105
|
+
*/
|
|
106
|
+
#closes = 0;
|
|
107
|
+
constructor(options) {
|
|
108
|
+
this.#options = options;
|
|
109
|
+
this.#reads = new CoalescedRead(async () => {
|
|
110
|
+
const force = this.#force;
|
|
111
|
+
this.#force = false;
|
|
112
|
+
if (force || this.#latestView !== this.#options.view())
|
|
113
|
+
await this.#options.discover();
|
|
114
|
+
}, (error) => {
|
|
115
|
+
if (!this.#stopped)
|
|
116
|
+
this.#options.onError?.(error);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
get mode() {
|
|
120
|
+
return this.#mode;
|
|
121
|
+
}
|
|
122
|
+
start() {
|
|
123
|
+
if (this.#stopped || this.#watch || this.#timer)
|
|
124
|
+
return;
|
|
125
|
+
if (typeof this.#options.client.watchScoped !== 'function') {
|
|
126
|
+
// A claustrum-client older than 0.12.0: poll exactly as before.
|
|
127
|
+
this.#setMode('fallback', 'client-without-watch');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
void this.#open(false);
|
|
131
|
+
}
|
|
132
|
+
stop() {
|
|
133
|
+
if (this.#stopped)
|
|
134
|
+
return;
|
|
135
|
+
this.#stopped = true;
|
|
136
|
+
if (this.#timer)
|
|
137
|
+
(this.#options.clearTimeoutImpl ?? clearTimeout)(this.#timer);
|
|
138
|
+
this.#timer = undefined;
|
|
139
|
+
const watch = this.#watch;
|
|
140
|
+
this.#watch = undefined;
|
|
141
|
+
watch?.handle.stop();
|
|
142
|
+
}
|
|
143
|
+
#setMode(mode, reason) {
|
|
144
|
+
if (this.#mode === mode)
|
|
145
|
+
return;
|
|
146
|
+
this.#mode = mode;
|
|
147
|
+
this.#options.logger?.debug('claustrum vault watch', { mode, reason });
|
|
148
|
+
this.#options.onMode?.(mode);
|
|
149
|
+
}
|
|
150
|
+
#later(delay, run) {
|
|
151
|
+
this.#timer = (this.#options.setTimeoutImpl ?? setTimeout)(() => {
|
|
152
|
+
this.#timer = undefined;
|
|
153
|
+
if (!this.#stopped)
|
|
154
|
+
run();
|
|
155
|
+
}, delay);
|
|
156
|
+
this.#timer.unref?.();
|
|
157
|
+
}
|
|
158
|
+
async #open(resubscribed) {
|
|
159
|
+
let token;
|
|
160
|
+
try {
|
|
161
|
+
token = await this.#options.enrollmentToken();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (this.#stopped)
|
|
165
|
+
return;
|
|
166
|
+
this.#options.onError?.(error);
|
|
167
|
+
this.#fallback('token-unreadable');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const watchScoped = this.#options.client.watchScoped;
|
|
171
|
+
if (this.#stopped || this.#watch || !watchScoped)
|
|
172
|
+
return;
|
|
173
|
+
this.#reader.restart();
|
|
174
|
+
// A re-subscribed watch reads on its first notice: changes made while no
|
|
175
|
+
// watch was open were never announced.
|
|
176
|
+
this.#force = resubscribed;
|
|
177
|
+
let watch;
|
|
178
|
+
let handle;
|
|
179
|
+
try {
|
|
180
|
+
handle = watchScoped.call(this.#options.client, (notice) => {
|
|
181
|
+
if (this.#stopped || !watch || this.#watch !== watch)
|
|
182
|
+
return;
|
|
183
|
+
watch.notices++;
|
|
184
|
+
if (watch.notices === 1) {
|
|
185
|
+
this.#options.onReachable?.();
|
|
186
|
+
watch.acceptedAt = (this.#options.now ?? Date.now)();
|
|
187
|
+
this.#setMode('watching', 'accepted');
|
|
188
|
+
}
|
|
189
|
+
if (!this.#reader.continuous(notice))
|
|
190
|
+
this.#force = true;
|
|
191
|
+
this.#latestView = notice.view;
|
|
192
|
+
// The read itself decides whether to list, against the roster as
|
|
193
|
+
// it stands when the read runs.
|
|
194
|
+
void this.#reads.request();
|
|
195
|
+
}, { enrollmentToken: token });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.#options.onError?.(error);
|
|
199
|
+
this.#fallback('watch-failed');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
watch = { handle, notices: 0 };
|
|
203
|
+
this.#watch = watch;
|
|
204
|
+
this.#setMode('polling', resubscribed ? 'resubscribing' : 'opening');
|
|
205
|
+
const opened = watch;
|
|
206
|
+
handle.closed.then((end) => this.#closed(opened, end), (error) => this.#closed(opened, { kind: 'transport_lost', error }));
|
|
207
|
+
}
|
|
208
|
+
#closed(watch, end) {
|
|
209
|
+
if (this.#stopped || this.#watch !== watch)
|
|
210
|
+
return;
|
|
211
|
+
this.#watch = undefined;
|
|
212
|
+
if (end.kind === 'stopped')
|
|
213
|
+
return;
|
|
214
|
+
if (end.kind === 'transport_lost')
|
|
215
|
+
this.#options.onTransportLost?.();
|
|
216
|
+
if (end.kind === 'unsupported') {
|
|
217
|
+
// An older vault answers unknown_method: poll exactly as before.
|
|
218
|
+
this.#setMode('fallback', 'vault-without-watch');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (end.kind === 'refused') {
|
|
222
|
+
this.#fallback('refused');
|
|
223
|
+
try {
|
|
224
|
+
this.#options.onRefused(end.error);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
this.#options.onError?.(error);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (watch.notices > 0) {
|
|
232
|
+
// An accepted watch that closed (the vault restarted, or a revocation
|
|
233
|
+
// ended it) gets one new watch. A revocation then shows up as that
|
|
234
|
+
// watch's refusal.
|
|
235
|
+
const now = (this.#options.now ?? Date.now)();
|
|
236
|
+
if (watch.acceptedAt !== undefined &&
|
|
237
|
+
now - watch.acceptedAt >= SAFETY_POLL_MS)
|
|
238
|
+
this.#closes = 0;
|
|
239
|
+
const delay = Math.min(RESUBSCRIBE_BACKOFF_MS * 2 ** Math.min(this.#closes, 16), WATCH_RETRY_MS);
|
|
240
|
+
this.#closes++;
|
|
241
|
+
this.#setMode('polling', 'resubscribing');
|
|
242
|
+
this.#later(delay, () => void this.#open(true));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Zero notices: the watch was never accepted (the vault is draining, or
|
|
246
|
+
// the connection failed). The client already made one read to learn why.
|
|
247
|
+
this.#fallback(end.kind);
|
|
248
|
+
}
|
|
249
|
+
#fallback(reason) {
|
|
250
|
+
this.#setMode('fallback', reason);
|
|
251
|
+
this.#later(WATCH_RETRY_MS, () => void this.#open(false));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
import { ClaustrumClient, } from "@cortexkit/claustrum-client";
|
|
7
7
|
import { insulaUsageTransport } from "./d0f0b6be200311af-insula.js";
|
|
8
8
|
export { ClaustrumCredentialError, } from "@cortexkit/claustrum-client";
|
|
9
|
+
export { ClaustrumUnreachableError, classifyVaultFailure, VAULT_FAILURE_CLASSES, } from "./94f53cfd572ba4bb-availability.js";
|
|
9
10
|
export { ClaustrumConsumer, } from "./169b2b4bf30909bc-consumer.js";
|
|
10
|
-
export { ClaustrumScopedCustody, decideScopedRetryAfter401, isScopedCredentialRotation, SERVING_MARGIN_MS, } from "./0f77ebc3c914c9c6-custody.js";
|
|
11
|
+
export { ClaustrumScopedCustody, decideScopedRetryAfter401, isScopedCredentialRotation, KEPT_RECEIPT_MARGIN_MS, SERVING_MARGIN_MS, } from "./0f77ebc3c914c9c6-custody.js";
|
|
11
12
|
export { ClaustrumEnrollmentManager, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from "./b9588ad4725c1d10-enrollment.js";
|
|
12
13
|
export { ClaustrumConsumerError, } from "./ca0fccaba1d87af4-errors.js";
|
|
13
14
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, isCustodyPlaceholder, isCustodyPlaceholderValue, } from "./d9d2892a4c371301-host-slot.js";
|
|
15
|
+
export { acquireInsulaQuotaSource, } from "./d0f0b6be200311af-insula.js";
|
|
14
16
|
export { acceptAccount, declineAccount, isDeclined, } from "./7c140680fc81be92-interlock.js";
|
|
15
17
|
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
18
|
+
export { ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, WATCH_RETRY_MS, } from "./dc9290c2a7552ba6-watch.js";
|
|
16
19
|
/**
|
|
17
20
|
* Connect the client that lists and fetches this consumer's vault credentials
|
|
18
21
|
* on the request path. `connectionFile` is required: this library
|