@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,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 { acceptVaultRoute, declineVaultRoute, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
8
|
+
import { SAFETY_POLL_MS, ScopedRosterWatch } from "./dc9290c2a7552ba6-watch.js";
|
|
8
9
|
/**
|
|
9
10
|
* One plugin host's view of its vault accounts. It turns vault credentials
|
|
10
11
|
* into rows for `/routing`, authorizes each physical send with the vault, and
|
|
@@ -18,12 +19,23 @@ export class ClaustrumConsumer {
|
|
|
18
19
|
#shutdown = new AbortController();
|
|
19
20
|
#logger;
|
|
20
21
|
#custody;
|
|
22
|
+
/** The custody's client, kept so `start()` can open its vault watch. */
|
|
23
|
+
#client;
|
|
21
24
|
#connecting;
|
|
22
25
|
#refreshing;
|
|
23
26
|
#roster;
|
|
27
|
+
/**
|
|
28
|
+
* The grants named by this instance's latest discovery, with the view that
|
|
29
|
+
* discovery committed. Memory only: the roster file never carries it.
|
|
30
|
+
*/
|
|
31
|
+
#grantReach;
|
|
24
32
|
#timer;
|
|
25
33
|
#started = false;
|
|
26
34
|
#insula;
|
|
35
|
+
#vaultWatch;
|
|
36
|
+
#releaseInsulaWatch;
|
|
37
|
+
/** When this instance last completed a discovery, by `now`. */
|
|
38
|
+
#discoveredAt;
|
|
27
39
|
constructor(options) {
|
|
28
40
|
this.#options = options;
|
|
29
41
|
this.#logger = options.logger ?? createLogger('claustrum');
|
|
@@ -78,6 +90,7 @@ export class ClaustrumConsumer {
|
|
|
78
90
|
client.close();
|
|
79
91
|
this.#assertOpen();
|
|
80
92
|
}
|
|
93
|
+
this.#client = client;
|
|
81
94
|
this.#custody = new ClaustrumScopedCustody({
|
|
82
95
|
client,
|
|
83
96
|
family: this.#options.family,
|
|
@@ -86,6 +99,8 @@ export class ClaustrumConsumer {
|
|
|
86
99
|
requireAssertion: this.#options.requireAssertion,
|
|
87
100
|
now: this.#options.now,
|
|
88
101
|
logger: this.#logger,
|
|
102
|
+
setTimeoutImpl: this.#options.setTimeoutImpl,
|
|
103
|
+
clearTimeoutImpl: this.#options.clearTimeoutImpl,
|
|
89
104
|
});
|
|
90
105
|
if (this.#options.insula && client.insula) {
|
|
91
106
|
this.#insula = acquireInsulaQuotaSource(client.insula, this.#options.tokenPath);
|
|
@@ -102,6 +117,43 @@ export class ClaustrumConsumer {
|
|
|
102
117
|
snapshot() {
|
|
103
118
|
return this.#roster;
|
|
104
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* What this consumer is granted, from its latest discovery, without any I/O.
|
|
122
|
+
* It explains an empty roster: `grants === 0` means no grant at all, while
|
|
123
|
+
* `grants > 0` means a grant that reaches no credential (`tuples` names its
|
|
124
|
+
* selector). Undefined means unknown, never zero: the vault or client is too
|
|
125
|
+
* old to send it, nothing was discovered yet, or the roster now in memory is
|
|
126
|
+
* a different view than the one this instance discovered (a peer process
|
|
127
|
+
* held the discovery lease and its newer roster was served instead).
|
|
128
|
+
*/
|
|
129
|
+
grantReach() {
|
|
130
|
+
const latest = this.#grantReach;
|
|
131
|
+
return latest && this.#roster?.view === latest.view
|
|
132
|
+
? latest.reach
|
|
133
|
+
: undefined;
|
|
134
|
+
}
|
|
135
|
+
/** How this consumer currently learns about changes, without I/O. */
|
|
136
|
+
watchState() {
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
vault: this.#vaultWatch?.mode ?? 'polling',
|
|
139
|
+
...(this.#options.insula && {
|
|
140
|
+
insula: this.#insula?.source.watchMode() ?? 'polling',
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Close the watches with the connection they belong to. */
|
|
145
|
+
#stopWatch() {
|
|
146
|
+
this.#vaultWatch?.stop();
|
|
147
|
+
this.#vaultWatch = undefined;
|
|
148
|
+
this.#client = undefined;
|
|
149
|
+
this.#releaseInsulaWatch?.();
|
|
150
|
+
this.#releaseInsulaWatch = undefined;
|
|
151
|
+
}
|
|
152
|
+
/** Whether any account is using an in-memory outage receipt, without I/O. */
|
|
153
|
+
keptReceiptState() {
|
|
154
|
+
return (this.#custody?.keptReceiptState() ??
|
|
155
|
+
Object.freeze({ active: false, since: null }));
|
|
156
|
+
}
|
|
105
157
|
/** Rows for `/routing`, from the last committed roster. */
|
|
106
158
|
routingRows() {
|
|
107
159
|
return vaultRoutingRows(this.#roster);
|
|
@@ -113,6 +165,8 @@ export class ClaustrumConsumer {
|
|
|
113
165
|
this.#refreshing = (async () => {
|
|
114
166
|
if (!(await this.#active())) {
|
|
115
167
|
this.#roster = undefined;
|
|
168
|
+
this.#grantReach = undefined;
|
|
169
|
+
this.#stopWatch();
|
|
116
170
|
this.#custody?.close();
|
|
117
171
|
this.#custody = undefined;
|
|
118
172
|
this.#insula?.release();
|
|
@@ -120,9 +174,17 @@ export class ClaustrumConsumer {
|
|
|
120
174
|
return undefined;
|
|
121
175
|
}
|
|
122
176
|
const custody = await this.#getCustody();
|
|
177
|
+
// Capture the inventory so its grant fields can be kept beside the roster;
|
|
178
|
+
// the roster file itself never stores them.
|
|
179
|
+
let discovered;
|
|
123
180
|
const roster = await refreshVaultRoster({
|
|
124
181
|
path: this.#options.rosterPath,
|
|
125
|
-
custody
|
|
182
|
+
custody: {
|
|
183
|
+
discover: async (signal) => {
|
|
184
|
+
discovered = await custody.discover(signal);
|
|
185
|
+
return discovered;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
126
188
|
isActive: () => this.#active(),
|
|
127
189
|
signal: this.#shutdown.signal,
|
|
128
190
|
projection: () => ({
|
|
@@ -135,16 +197,35 @@ export class ClaustrumConsumer {
|
|
|
135
197
|
this.#assertOpen();
|
|
136
198
|
const changed = roster?.view !== this.#roster?.view;
|
|
137
199
|
this.#roster = roster;
|
|
200
|
+
custody.retain(roster?.rows ?? []);
|
|
201
|
+
// Each discovery replaces the previous value, including with unknown. A
|
|
202
|
+
// refresh that served a peer's roster discovered nothing and leaves it;
|
|
203
|
+
// grantReach() hides it once the roster's view no longer matches.
|
|
204
|
+
if (discovered && roster?.view !== undefined)
|
|
205
|
+
this.#grantReach = discovered.grantReach
|
|
206
|
+
? { view: roster.view, reach: discovered.grantReach }
|
|
207
|
+
: undefined;
|
|
138
208
|
// Notify on view changes only: subscribers rewrite shared state on each
|
|
139
209
|
// notification, and the poll runs every few seconds.
|
|
140
210
|
if (roster && changed)
|
|
141
211
|
this.#options.onRoster?.(roster);
|
|
212
|
+
this.#discoveredAt = (this.#options.now ?? Date.now)();
|
|
142
213
|
return roster;
|
|
143
214
|
})().finally(() => {
|
|
144
215
|
this.#refreshing = undefined;
|
|
145
216
|
});
|
|
146
217
|
return this.#refreshing;
|
|
147
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
|
+
*/
|
|
148
229
|
start() {
|
|
149
230
|
if (this.#started)
|
|
150
231
|
return;
|
|
@@ -152,8 +233,10 @@ export class ClaustrumConsumer {
|
|
|
152
233
|
this.#started = true;
|
|
153
234
|
const tick = async () => {
|
|
154
235
|
try {
|
|
155
|
-
await this
|
|
236
|
+
if (await this.#listDue())
|
|
237
|
+
await this.refresh();
|
|
156
238
|
await this.refreshInsulaQuota();
|
|
239
|
+
this.#openWatch();
|
|
157
240
|
}
|
|
158
241
|
catch (error) {
|
|
159
242
|
if (!this.#shutdown.signal.aborted)
|
|
@@ -172,6 +255,65 @@ export class ClaustrumConsumer {
|
|
|
172
255
|
};
|
|
173
256
|
void tick();
|
|
174
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 client = this.#client;
|
|
275
|
+
if (this.#shutdown.signal.aborted || !client || !this.#custody)
|
|
276
|
+
return;
|
|
277
|
+
if (!this.#vaultWatch) {
|
|
278
|
+
const watch = new ScopedRosterWatch({
|
|
279
|
+
client,
|
|
280
|
+
enrollmentToken: async () => (await readClaustrumEnrollmentToken(this.#options.tokenPath)).token,
|
|
281
|
+
view: () => this.#roster?.view,
|
|
282
|
+
// The existing discovery, so a watched change commits the roster and
|
|
283
|
+
// re-applies it to kept receipts exactly as a poll does. A refresh
|
|
284
|
+
// already in flight may have listed before the change was announced,
|
|
285
|
+
// so wait for it and list again.
|
|
286
|
+
discover: async () => {
|
|
287
|
+
while (this.#refreshing)
|
|
288
|
+
await this.#refreshing.catch(() => { });
|
|
289
|
+
await this.refresh();
|
|
290
|
+
},
|
|
291
|
+
onRefused: (error) => {
|
|
292
|
+
// As a poll that met the same refusal from list_scoped would.
|
|
293
|
+
this.#custody?.listFailed(error);
|
|
294
|
+
this.#options.onError?.(error);
|
|
295
|
+
},
|
|
296
|
+
onTransportLost: () => this.#custody?.transportLost(),
|
|
297
|
+
onReachable: () => this.#custody?.reachable(),
|
|
298
|
+
onError: (error) => {
|
|
299
|
+
if (!this.#shutdown.signal.aborted)
|
|
300
|
+
this.#options.onError?.(error);
|
|
301
|
+
},
|
|
302
|
+
logger: this.#logger,
|
|
303
|
+
now: this.#options.now,
|
|
304
|
+
setTimeoutImpl: this.#options.setTimeoutImpl,
|
|
305
|
+
clearTimeoutImpl: this.#options.clearTimeoutImpl,
|
|
306
|
+
});
|
|
307
|
+
this.#vaultWatch = watch;
|
|
308
|
+
watch.start();
|
|
309
|
+
}
|
|
310
|
+
if (this.#options.insula && this.#insula && !this.#releaseInsulaWatch)
|
|
311
|
+
this.#releaseInsulaWatch = this.#insula.source.watch({
|
|
312
|
+
provider: this.#options.insula.provider,
|
|
313
|
+
setTimeoutImpl: this.#options.setTimeoutImpl,
|
|
314
|
+
clearTimeoutImpl: this.#options.clearTimeoutImpl,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
175
317
|
/**
|
|
176
318
|
* Fetch on a background tick, at most once per minute across projects with
|
|
177
319
|
* the same daemon and enrollment token path. Admission must use the cache
|
|
@@ -246,20 +388,33 @@ export class ClaustrumConsumer {
|
|
|
246
388
|
async authorize(routeId, signal) {
|
|
247
389
|
this.#assertOpen(signal);
|
|
248
390
|
const roster = this.#roster ?? (await this.#wait(this.refresh(), signal));
|
|
249
|
-
if (!roster || !(await this.#active()))
|
|
391
|
+
if (!roster || !(await this.#active())) {
|
|
392
|
+
this.#custody?.close();
|
|
393
|
+
this.#custody = undefined;
|
|
394
|
+
this.#stopWatch();
|
|
395
|
+
this.#insula?.release();
|
|
396
|
+
this.#insula = undefined;
|
|
250
397
|
throw new ClaustrumConsumerError('not-active', 'Claustrum scoped custody is not active');
|
|
398
|
+
}
|
|
251
399
|
const current = await readVaultRoster(this.#options.rosterPath);
|
|
252
400
|
this.#assertOpen(signal);
|
|
253
401
|
const observed = roster.rows.find((row) => row.routeId === routeId);
|
|
254
402
|
const configured = current?.rows.find((row) => row.routeId === routeId);
|
|
255
|
-
if (configured && !configured.enabled)
|
|
403
|
+
if (configured && !configured.enabled) {
|
|
404
|
+
if (observed)
|
|
405
|
+
this.#custody?.forget(observed.credentialId);
|
|
256
406
|
throw new ClaustrumConsumerError('route-declined', 'Claustrum route is disabled');
|
|
407
|
+
}
|
|
257
408
|
if (!observed ||
|
|
258
409
|
!configured ||
|
|
259
410
|
configured.state !== 'active' ||
|
|
260
411
|
configured.credentialId !== observed.credentialId ||
|
|
261
|
-
configured.accountIdentity !== observed.accountIdentity
|
|
412
|
+
configured.accountIdentity !== observed.accountIdentity ||
|
|
413
|
+
configured.credentialType !== observed.credentialType) {
|
|
414
|
+
if (observed)
|
|
415
|
+
this.#custody?.forget(observed.credentialId);
|
|
262
416
|
throw new ClaustrumConsumerError('route-unavailable', 'Claustrum route is disabled, removed or changed');
|
|
417
|
+
}
|
|
263
418
|
const custody = await this.#wait(this.#getCustody(), signal);
|
|
264
419
|
const combined = combineAbortSignals([
|
|
265
420
|
this.#shutdown.signal,
|
|
@@ -272,7 +427,7 @@ export class ClaustrumConsumer {
|
|
|
272
427
|
...(configured.accountIdentity !== undefined && {
|
|
273
428
|
accountIdentity: configured.accountIdentity,
|
|
274
429
|
}),
|
|
275
|
-
}, combined.signal);
|
|
430
|
+
}, combined.signal, routeId);
|
|
276
431
|
}
|
|
277
432
|
finally {
|
|
278
433
|
combined.dispose();
|
|
@@ -296,6 +451,10 @@ export class ClaustrumConsumer {
|
|
|
296
451
|
const signal = options.signal;
|
|
297
452
|
let served = await this.authorize(routeId, signal);
|
|
298
453
|
let response = await dispatch(served, signal);
|
|
454
|
+
if (response.status === 401 && served.fromKeptReceipt) {
|
|
455
|
+
await this.reportFailure(served, 401, options.reporterSource ?? 'direct');
|
|
456
|
+
return response;
|
|
457
|
+
}
|
|
299
458
|
if (response.status === 401 && !signal?.aborted) {
|
|
300
459
|
let current;
|
|
301
460
|
try {
|
|
@@ -330,6 +489,7 @@ export class ClaustrumConsumer {
|
|
|
330
489
|
this.#assertOpen();
|
|
331
490
|
await declineVaultRoute(this.#options.rosterPath, routeId);
|
|
332
491
|
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
492
|
+
this.#custody?.retain(this.#roster?.rows ?? []);
|
|
333
493
|
}
|
|
334
494
|
async accept(routeId) {
|
|
335
495
|
this.#assertOpen();
|
|
@@ -385,9 +545,11 @@ export class ClaustrumConsumer {
|
|
|
385
545
|
if (this.#timer)
|
|
386
546
|
(this.#options.clearTimeoutImpl ?? clearTimeout)(this.#timer);
|
|
387
547
|
this.#timer = undefined;
|
|
548
|
+
this.#stopWatch();
|
|
388
549
|
this.#custody?.close();
|
|
389
550
|
this.#insula?.release();
|
|
390
551
|
this.#insula = undefined;
|
|
391
552
|
this.#roster = undefined;
|
|
553
|
+
this.#grantReach = undefined;
|
|
392
554
|
}
|
|
393
555
|
}
|
|
@@ -184,19 +184,25 @@ export async function replaceWindowsFile(from, to, options = {}) {
|
|
|
184
184
|
to = resolve(to);
|
|
185
185
|
const now = options.now ?? (() => performance.now());
|
|
186
186
|
const start = now();
|
|
187
|
-
const
|
|
187
|
+
const leaseDeadline = options.deadline ?? Infinity;
|
|
188
|
+
let retryDeadline;
|
|
188
189
|
const delays = [0, 10, 20, 40, 80, 100];
|
|
189
190
|
let original;
|
|
190
191
|
let attempts = 0;
|
|
191
192
|
for (const delay of delays) {
|
|
193
|
+
const deadline = Math.min(retryDeadline ?? Infinity, leaseDeadline);
|
|
192
194
|
if (now() >= deadline)
|
|
193
195
|
break;
|
|
194
196
|
if (delay)
|
|
195
197
|
await (options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(Math.min(delay, Math.max(0, deadline - now())));
|
|
196
198
|
if (now() >= deadline)
|
|
197
199
|
break;
|
|
200
|
+
const fenceStart = now();
|
|
198
201
|
await options.beforeRename?.();
|
|
199
|
-
|
|
202
|
+
// Ownership checks fence every attempt but do not spend the sharing-retry budget.
|
|
203
|
+
if (retryDeadline !== undefined)
|
|
204
|
+
retryDeadline += now() - fenceStart;
|
|
205
|
+
if (now() >= leaseDeadline)
|
|
200
206
|
break;
|
|
201
207
|
attempts++;
|
|
202
208
|
try {
|
|
@@ -212,6 +218,8 @@ export async function replaceWindowsFile(from, to, options = {}) {
|
|
|
212
218
|
!detail.reason;
|
|
213
219
|
if (!candidate)
|
|
214
220
|
break;
|
|
221
|
+
// Start the retry window only once a rename has actually failed.
|
|
222
|
+
retryDeadline ??= now() + 500;
|
|
215
223
|
}
|
|
216
224
|
}
|
|
217
225
|
const error = new PrivateFileError(to, 'rename', original ??
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ClaustrumCredentialError } from "@cortexkit/claustrum-client";
|
|
2
|
+
import { SocketClosedError, SocketTimeoutError, SubcCallError, } from "@cortexkit/subc-client";
|
|
3
|
+
import { ClaustrumConsumerError } from "./ca0fccaba1d87af4-errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* Codes from claustrum-client's decoder and its subc transport. A retry action
|
|
6
|
+
* alone is not evidence of an outage: even transient vault refusals must win
|
|
7
|
+
* over a previously served credential. New codes fail closed until reviewed.
|
|
8
|
+
*/
|
|
9
|
+
export const VAULT_FAILURE_CLASSES = Object.freeze({
|
|
10
|
+
ECONNREFUSED: 'unreachable',
|
|
11
|
+
ENOENT: 'unreachable',
|
|
12
|
+
ECONNRESET: 'unreachable',
|
|
13
|
+
EPIPE: 'unreachable',
|
|
14
|
+
ETIMEDOUT: 'unreachable',
|
|
15
|
+
transport_error: 'unreachable',
|
|
16
|
+
unavailable: 'unreachable',
|
|
17
|
+
timeout: 'unreachable',
|
|
18
|
+
route_closed: 'unreachable',
|
|
19
|
+
daemon_draining: 'unreachable',
|
|
20
|
+
connection_closed: 'unreachable',
|
|
21
|
+
connection_dropped: 'unreachable',
|
|
22
|
+
request_deadline: 'unreachable',
|
|
23
|
+
deadline_exceeded_no_drop_observed: 'unreachable',
|
|
24
|
+
module_reloading: 'unreachable',
|
|
25
|
+
module_warming: 'unreachable',
|
|
26
|
+
target_unavailable: 'unreachable',
|
|
27
|
+
module_timeout: 'unreachable',
|
|
28
|
+
invalid_response: 'answered',
|
|
29
|
+
invalid_record_version: 'answered',
|
|
30
|
+
invalid_expiry: 'answered',
|
|
31
|
+
invalid_status: 'answered',
|
|
32
|
+
invalid_params: 'answered',
|
|
33
|
+
ttl_unsatisfiable: 'answered',
|
|
34
|
+
not_found: 'answered',
|
|
35
|
+
enrollment_revoked: 'answered',
|
|
36
|
+
enrollment_invalid: 'answered',
|
|
37
|
+
grant_denied: 'answered',
|
|
38
|
+
credential_unavailable: 'answered',
|
|
39
|
+
needs_reauth: 'answered',
|
|
40
|
+
declined: 'answered',
|
|
41
|
+
disabled: 'answered',
|
|
42
|
+
identity_changed: 'answered',
|
|
43
|
+
closed: 'answered',
|
|
44
|
+
client_closed: 'answered',
|
|
45
|
+
pending_exists: 'answered',
|
|
46
|
+
pending_queue_full: 'answered',
|
|
47
|
+
already_consumed: 'answered',
|
|
48
|
+
superseded: 'answered',
|
|
49
|
+
stale_generation: 'answered',
|
|
50
|
+
missing_identity: 'answered',
|
|
51
|
+
invalid_control_body: 'answered',
|
|
52
|
+
unknown_module: 'answered',
|
|
53
|
+
module_removed: 'answered',
|
|
54
|
+
capability_denied: 'answered',
|
|
55
|
+
unknown_channel: 'unreachable',
|
|
56
|
+
stale_route_epoch: 'unreachable',
|
|
57
|
+
capability_forbidden: 'answered',
|
|
58
|
+
capability_unprovided: 'answered',
|
|
59
|
+
capability_ambiguous: 'answered',
|
|
60
|
+
invalid_capability_identifier: 'answered',
|
|
61
|
+
binary_body_required: 'answered',
|
|
62
|
+
binary_call_requires_call_binary: 'answered',
|
|
63
|
+
corr_exhausted: 'answered',
|
|
64
|
+
late_route_cleanup_failed: 'answered',
|
|
65
|
+
reverse_request_capability_not_declared: 'answered',
|
|
66
|
+
stale_route_handle: 'answered',
|
|
67
|
+
reverse_request_unhandled: 'answered',
|
|
68
|
+
handler_error: 'answered',
|
|
69
|
+
health_error: 'answered',
|
|
70
|
+
duplicate_module_id: 'answered',
|
|
71
|
+
route_rejected: 'answered',
|
|
72
|
+
lock_busy: 'answered',
|
|
73
|
+
owner_invalid: 'answered',
|
|
74
|
+
renewal_failed: 'answered',
|
|
75
|
+
unsupported_platform: 'answered',
|
|
76
|
+
EACCES: 'answered',
|
|
77
|
+
EPERM: 'answered',
|
|
78
|
+
ERR_PRIVATE_FILES_SECURITY: 'answered',
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* An injected adapter may throw this ONLY when its connection/transport is
|
|
82
|
+
* unreachable, including a failed connect before getScoped. Do not wrap an
|
|
83
|
+
* adapter shutdown, cancellation, validation error or vault refusal in it.
|
|
84
|
+
* Its fixed message cannot echo enrollment or credential material, and its
|
|
85
|
+
* kind stays `unavailable` for callers handling ClaustrumConsumerError.
|
|
86
|
+
*/
|
|
87
|
+
export class ClaustrumUnreachableError extends ClaustrumConsumerError {
|
|
88
|
+
constructor() {
|
|
89
|
+
super('unavailable', 'Claustrum scoped operation unavailable');
|
|
90
|
+
this.name = 'ClaustrumUnreachableError';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function classifyVaultFailure(error) {
|
|
94
|
+
if (error instanceof ClaustrumUnreachableError ||
|
|
95
|
+
error instanceof SocketClosedError ||
|
|
96
|
+
error instanceof SocketTimeoutError)
|
|
97
|
+
return 'unreachable';
|
|
98
|
+
const code = error instanceof ClaustrumCredentialError || error instanceof SubcCallError
|
|
99
|
+
? error.code
|
|
100
|
+
: error instanceof Error && 'code' in error
|
|
101
|
+
? error.code
|
|
102
|
+
: undefined;
|
|
103
|
+
if (typeof code !== 'string' || !Object.hasOwn(VAULT_FAILURE_CLASSES, code))
|
|
104
|
+
return 'answered';
|
|
105
|
+
return VAULT_FAILURE_CLASSES[code];
|
|
106
|
+
}
|