@cortexkit/common-auth 0.2.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cachekeep/manager.d.ts +18 -6
- package/dist/cachekeep/manager.js +40 -10
- package/dist/claustrum/consumer.d.ts +13 -4
- package/dist/claustrum/consumer.js +11 -3
- package/dist/claustrum/custody.d.ts +47 -6
- package/dist/claustrum/custody.js +37 -7
- package/dist/claustrum/errors.d.ts +1 -1
- package/dist/claustrum/index.d.ts +3 -3
- package/dist/claustrum/index.js +2 -2
- package/dist/claustrum/interlock.d.ts +15 -17
- package/dist/claustrum/interlock.js +19 -26
- package/dist/claustrum/roster.d.ts +96 -6
- package/dist/claustrum/roster.js +209 -44
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +6 -1
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/commands/menu.d.ts +8 -0
- package/dist/commands/menu.js +34 -13
- package/dist/commands/model.d.ts +9 -0
- package/dist/commands/seam.d.ts +40 -4
- package/dist/commands/seam.js +132 -19
- package/dist/dump/index.d.ts +94 -0
- package/dist/dump/index.js +236 -9
- package/dist/logger/engine.d.ts +52 -17
- package/dist/logger/engine.js +178 -135
- package/dist/logger/index.d.ts +2 -2
- package/dist/logger/index.js +1 -1
- package/dist/opencode2/index.d.ts +1 -1
- package/dist/opencode2/install.d.ts +23 -5
- package/dist/opencode2/install.js +335 -140
- package/dist/opencode2/types.d.ts +149 -9
- package/dist/quota/projection.d.ts +11 -4
- package/dist/quota/projection.js +11 -4
- package/dist/routing/admission.js +3 -1
- package/dist/routing/index.d.ts +2 -2
- package/dist/routing/index.js +1 -1
- package/dist/routing/sticky.d.ts +19 -6
- package/dist/routing/sticky.js +34 -23
- package/dist/rpc/notifications.d.ts +20 -0
- package/dist/rpc/notifications.js +21 -0
- package/dist/rpc/rpc-server.d.ts +9 -1
- package/dist/rpc/rpc-server.js +8 -1
- package/dist/sidebar-file/index.d.ts +1 -1
- package/dist/sidebar-file/sidebar-file.d.ts +50 -2
- package/dist/sidebar-file/sidebar-file.js +92 -21
- package/dist/store/attribution.js +11 -2
- package/dist/store/errors.d.ts +6 -3
- package/dist/store/identity.d.ts +13 -4
- package/dist/store/mutate.d.ts +23 -3
- package/dist/store/mutate.js +43 -26
- package/dist/store/pool.d.ts +8 -1
- package/dist/store/pool.js +7 -2
- package/dist/store/rows.d.ts +17 -4
- package/dist/store/rows.js +82 -33
- package/dist/store/schema.d.ts +57 -4
- package/dist/store/schema.js +100 -7
- package/dist/store/torn.d.ts +29 -0
- package/dist/store/torn.js +113 -0
- package/package.json +1 -1
|
@@ -10,6 +10,49 @@ const scopeOf = (draft) => ({
|
|
|
10
10
|
agent: draft.agent,
|
|
11
11
|
kind: draft.kind,
|
|
12
12
|
});
|
|
13
|
+
// A header value is a string or null, so an object under `headers` can only
|
|
14
|
+
// be the `{headers, attempt}` form.
|
|
15
|
+
function isHeadersResult(value) {
|
|
16
|
+
const headers = value.headers;
|
|
17
|
+
return typeof headers === 'object' && headers !== null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Forwards a response body and reports how it ended: read to the end
|
|
21
|
+
* (`undefined`), errored, or cancelled by whoever was reading it.
|
|
22
|
+
*/
|
|
23
|
+
function trackBody(source, onEnd) {
|
|
24
|
+
const reader = source.getReader();
|
|
25
|
+
return new ReadableStream({
|
|
26
|
+
async pull(controller) {
|
|
27
|
+
let chunk;
|
|
28
|
+
try {
|
|
29
|
+
chunk = await reader.read();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
onEnd({
|
|
33
|
+
reason: 'failed',
|
|
34
|
+
message: error instanceof Error ? error.message : String(error),
|
|
35
|
+
});
|
|
36
|
+
controller.error(error);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (chunk.done) {
|
|
40
|
+
onEnd();
|
|
41
|
+
controller.close();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
controller.enqueue(chunk.value);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
async cancel(reason) {
|
|
48
|
+
onEnd({
|
|
49
|
+
reason: 'cancelled',
|
|
50
|
+
...(reason === undefined ? {} : { message: String(reason) }),
|
|
51
|
+
});
|
|
52
|
+
await reader.cancel(reason);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
13
56
|
/** Applies edits to a plain header record, replacing every spelling of each name. */
|
|
14
57
|
export function applyHeaderEdits(target, edits) {
|
|
15
58
|
for (const [name, value] of Object.entries(edits)) {
|
|
@@ -34,15 +77,33 @@ function applyHeaderEditsTo(target, edits) {
|
|
|
34
77
|
* Installs multi-account auth on OpenCode 2's own provider drivers. Every
|
|
35
78
|
* hook is scoped to `adapter.providerID`:
|
|
36
79
|
*
|
|
37
|
-
* - `model.request` picks the account for the request's `sessionID:kind
|
|
38
|
-
* sets its headers;
|
|
80
|
+
* - `model.request` picks the account for the request's `sessionID:kind`,
|
|
81
|
+
* which starts a new attempt, and sets its headers;
|
|
39
82
|
* - `http.request` and `experimental.ws.handshake` set them again, because
|
|
40
83
|
* the host applies its own credential after `model.request`;
|
|
41
|
-
* - `
|
|
42
|
-
*
|
|
84
|
+
* - `experimental.ws.send` (only when the adapter rewrites frames) rewrites
|
|
85
|
+
* each outgoing frame;
|
|
86
|
+
* - `http.response` and `experimental.ws.receive` read quota, refusals,
|
|
87
|
+
* whether output has started and when the response ended, attributed to
|
|
88
|
+
* an attempt by the rules below;
|
|
43
89
|
* - `retry` asks the host to retry at once when an account was refused
|
|
44
90
|
* before any output, so `model.request` runs again and can pick another
|
|
45
91
|
* account, and refuses to retry once output has started.
|
|
92
|
+
*
|
|
93
|
+
* Attribution. An HTTP response belongs to the attempt whose `http.request`
|
|
94
|
+
* produced its request object, and to nothing else. The host hands
|
|
95
|
+
* `http.response` the request object the `http.request` hooks left, so a
|
|
96
|
+
* different object means a later hook replaced it; nothing then proves which
|
|
97
|
+
* send the response answers (an earlier attempt's send may still be in
|
|
98
|
+
* flight, and only the newest attempt of each session and kind is kept), and
|
|
99
|
+
* its feedback is dropped rather than guessed by recency. No marker can ride
|
|
100
|
+
* on the request instead: the host builds the wire request from that same
|
|
101
|
+
* object, so a marker would be sent to the provider. A WebSocket frame
|
|
102
|
+
* belongs to the newest attempt of its session and kind only while that
|
|
103
|
+
* attempt went out over WebSocket and has not ended: the host runs one
|
|
104
|
+
* exchange at a time per session socket, so frames between one handshake
|
|
105
|
+
* and the next belong to the earlier attempt. Anything else is attributed to
|
|
106
|
+
* no attempt and reaches no adapter callback or listener.
|
|
46
107
|
*/
|
|
47
108
|
export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
48
109
|
const { providerID } = adapter;
|
|
@@ -51,6 +112,7 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
51
112
|
const forbidden = (options.hostCredentials ?? [placeholderSecret(providerID)]).filter((value) => value !== '');
|
|
52
113
|
const records = new Map();
|
|
53
114
|
const byRequest = new WeakMap();
|
|
115
|
+
let warnedUnprovenResponse = false;
|
|
54
116
|
const listeners = new Map();
|
|
55
117
|
let seq = 0;
|
|
56
118
|
const warn = (message, data) => {
|
|
@@ -76,15 +138,40 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
76
138
|
}
|
|
77
139
|
}));
|
|
78
140
|
};
|
|
141
|
+
const deliverEnd = async (attempt, outcome) => {
|
|
142
|
+
try {
|
|
143
|
+
await adapter.onAttemptEnd?.(attempt, outcome);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
warn('opencode2 auth onAttemptEnd threw; the request is unaffected', {
|
|
147
|
+
attemptId: attempt.attemptId,
|
|
148
|
+
error: describe(error),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/** Ends an attempt once; later calls for the same attempt do nothing. */
|
|
153
|
+
const finish = (rec, error) => {
|
|
154
|
+
if (rec.ended || !rec.attempt)
|
|
155
|
+
return;
|
|
156
|
+
const outcome = {
|
|
157
|
+
...(rec.status === undefined ? {} : { status: rec.status }),
|
|
158
|
+
outputStarted: rec.outputStarted,
|
|
159
|
+
...(rec.limit ? { limit: rec.limit.signal } : {}),
|
|
160
|
+
...(error ? { error } : {}),
|
|
161
|
+
};
|
|
162
|
+
rec.ended = deliverEnd(rec.attempt, outcome);
|
|
163
|
+
};
|
|
164
|
+
const abandon = (rec, message) => finish(rec, { reason: 'abandoned', message });
|
|
79
165
|
const remember = (rec) => {
|
|
80
166
|
const key = keyOf(rec.scope.sessionID, rec.scope.kind);
|
|
81
167
|
records.delete(key);
|
|
82
168
|
records.set(key, rec);
|
|
83
169
|
while (records.size > maxRecords) {
|
|
84
|
-
const oldest = records.
|
|
170
|
+
const oldest = records.entries().next().value;
|
|
85
171
|
if (oldest === undefined)
|
|
86
172
|
break;
|
|
87
|
-
|
|
173
|
+
abandon(oldest[1], 'dropped to keep the record bound');
|
|
174
|
+
records.delete(oldest[0]);
|
|
88
175
|
}
|
|
89
176
|
};
|
|
90
177
|
const accountOf = (rec) => rec.accountId === undefined
|
|
@@ -109,8 +196,10 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
109
196
|
}
|
|
110
197
|
}
|
|
111
198
|
};
|
|
112
|
-
const select = async (scope, hook) => {
|
|
199
|
+
const select = async (scope, hook, transport) => {
|
|
113
200
|
const prior = records.get(keyOf(scope.sessionID, scope.kind));
|
|
201
|
+
if (prior)
|
|
202
|
+
abandon(prior, 'a newer attempt of its session and kind began');
|
|
114
203
|
const input = {
|
|
115
204
|
...scope,
|
|
116
205
|
...(prior?.accountId === undefined
|
|
@@ -121,21 +210,41 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
121
210
|
: { rerouteFrom: prior.rerouteFrom }),
|
|
122
211
|
};
|
|
123
212
|
const accountId = await adapter.chooseAccount(input);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
213
|
+
let headers = {};
|
|
214
|
+
let data;
|
|
215
|
+
if (accountId !== undefined) {
|
|
216
|
+
const result = await adapter.accountHeaders({ ...scope, accountId });
|
|
217
|
+
if (isHeadersResult(result)) {
|
|
218
|
+
headers = result.headers;
|
|
219
|
+
data = result.attempt;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
headers = result;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const id = ++seq;
|
|
127
226
|
const rec = {
|
|
128
227
|
scope,
|
|
129
228
|
accountId,
|
|
130
229
|
headers,
|
|
131
|
-
seq:
|
|
230
|
+
seq: id,
|
|
231
|
+
attempt: accountId === undefined
|
|
232
|
+
? undefined
|
|
233
|
+
: {
|
|
234
|
+
...scope,
|
|
235
|
+
accountId,
|
|
236
|
+
attemptId: `attempt-${id}`,
|
|
237
|
+
transport,
|
|
238
|
+
data,
|
|
239
|
+
},
|
|
240
|
+
...(transport === undefined ? {} : { transport }),
|
|
132
241
|
outputStarted: false,
|
|
133
242
|
};
|
|
134
243
|
remember(rec);
|
|
135
|
-
if (
|
|
244
|
+
if (rec.attempt) {
|
|
136
245
|
void emit('select', {
|
|
137
246
|
...scope,
|
|
138
|
-
accountId,
|
|
247
|
+
accountId: rec.attempt.accountId,
|
|
139
248
|
hook,
|
|
140
249
|
...(input.previousAccountId === undefined
|
|
141
250
|
? {}
|
|
@@ -143,17 +252,40 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
143
252
|
...(input.rerouteFrom === undefined
|
|
144
253
|
? {}
|
|
145
254
|
: { rerouteFrom: input.rerouteFrom }),
|
|
255
|
+
handle: rec.attempt,
|
|
146
256
|
});
|
|
147
257
|
}
|
|
148
258
|
return rec;
|
|
149
259
|
};
|
|
150
|
-
// The transport hooks normally follow `model.request
|
|
151
|
-
// keeps a request that skipped it
|
|
152
|
-
|
|
153
|
-
|
|
260
|
+
// The transport hooks normally follow `model.request` and carry the
|
|
261
|
+
// attempt it started. Choosing here too keeps a request that skipped it
|
|
262
|
+
// from going out under the host credential, and a second send without a
|
|
263
|
+
// `model.request` in between from reusing the first send's attempt.
|
|
264
|
+
const bind = async (scope, hook, transport) => {
|
|
265
|
+
const rec = records.get(keyOf(scope.sessionID, scope.kind));
|
|
266
|
+
if (!rec || rec.transport !== undefined || rec.ended)
|
|
267
|
+
return select(scope, hook, transport);
|
|
268
|
+
rec.transport = transport;
|
|
269
|
+
if (rec.attempt)
|
|
270
|
+
rec.attempt.transport = transport;
|
|
271
|
+
return rec;
|
|
272
|
+
};
|
|
273
|
+
// A transport hook that throws stops the send, so its attempt is over.
|
|
274
|
+
const failing = async (rec, run) => {
|
|
275
|
+
try {
|
|
276
|
+
return await run();
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
finish(rec, { reason: 'failed', message: describe(error) });
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const liveOn = (rec, transport) => rec?.attempt !== undefined && rec.transport === transport && !rec.ended
|
|
284
|
+
? rec
|
|
285
|
+
: undefined;
|
|
154
286
|
const noteLimit = (rec, signal, via) => {
|
|
155
287
|
const account = accountOf(rec);
|
|
156
|
-
if (!account || rec.limit)
|
|
288
|
+
if (!account || !rec.attempt || rec.limit)
|
|
157
289
|
return;
|
|
158
290
|
rec.limit = {
|
|
159
291
|
signal,
|
|
@@ -162,22 +294,29 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
162
294
|
via,
|
|
163
295
|
limit: signal,
|
|
164
296
|
outputStarted: rec.outputStarted,
|
|
297
|
+
handle: rec.attempt,
|
|
165
298
|
}),
|
|
166
299
|
};
|
|
167
300
|
};
|
|
168
301
|
const noteQuota = (rec, transport, quota, status) => {
|
|
169
302
|
const account = accountOf(rec);
|
|
170
|
-
if (!account)
|
|
303
|
+
if (!account || !rec.attempt)
|
|
171
304
|
return;
|
|
172
305
|
void emit('quota', {
|
|
173
306
|
...account,
|
|
174
307
|
transport,
|
|
175
308
|
...(status === undefined ? {} : { status }),
|
|
176
309
|
quota,
|
|
310
|
+
handle: rec.attempt,
|
|
177
311
|
});
|
|
178
312
|
};
|
|
179
313
|
const inspect = (rec, transport, data, event) => {
|
|
180
|
-
const
|
|
314
|
+
const attempt = rec.attempt;
|
|
315
|
+
if (!attempt)
|
|
316
|
+
return;
|
|
317
|
+
const verdict = adapter.inspectEvent?.(event === undefined
|
|
318
|
+
? { transport, data, attempt }
|
|
319
|
+
: { transport, data, event, attempt });
|
|
181
320
|
if (!verdict)
|
|
182
321
|
return;
|
|
183
322
|
if (verdict.quota !== undefined)
|
|
@@ -186,6 +325,10 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
186
325
|
rec.outputStarted = true;
|
|
187
326
|
if (verdict.limit)
|
|
188
327
|
noteLimit(rec, verdict.limit, transport);
|
|
328
|
+
if (verdict.error !== undefined)
|
|
329
|
+
finish(rec, { reason: 'failed', message: verdict.error });
|
|
330
|
+
else if (verdict.done)
|
|
331
|
+
finish(rec);
|
|
189
332
|
};
|
|
190
333
|
const pickForRetry = (sessionID) => {
|
|
191
334
|
let refused;
|
|
@@ -204,22 +347,28 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
204
347
|
return refused ?? primary ?? latest;
|
|
205
348
|
};
|
|
206
349
|
const scoped = { providerID };
|
|
207
|
-
const registrations = [
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
350
|
+
const registrations = [];
|
|
351
|
+
registrations.push(await ctx.session.hook('model.request', async (draft) => {
|
|
352
|
+
const rec = await select(scopeOf(draft), 'model.request');
|
|
353
|
+
if (rec.accountId === undefined)
|
|
354
|
+
throw noAccount(rec.scope);
|
|
355
|
+
applyHeaderEdits(draft.headers, rec.headers);
|
|
356
|
+
}, scoped));
|
|
357
|
+
registrations.push(await ctx.session.hook('http.request', async (draft) => {
|
|
358
|
+
const rec = await bind(scopeOf(draft), 'http.request', 'http');
|
|
359
|
+
const account = accountOf(rec);
|
|
360
|
+
const attempt = rec.attempt;
|
|
361
|
+
if (!account || !attempt)
|
|
362
|
+
throw noAccount(rec.scope);
|
|
363
|
+
await failing(rec, async () => {
|
|
219
364
|
let request = draft.request;
|
|
220
365
|
if (adapter.rewriteRequest) {
|
|
221
366
|
request =
|
|
222
|
-
(await adapter.rewriteRequest({
|
|
367
|
+
(await adapter.rewriteRequest({
|
|
368
|
+
...account,
|
|
369
|
+
request,
|
|
370
|
+
attempt,
|
|
371
|
+
})) ?? request;
|
|
223
372
|
}
|
|
224
373
|
const headers = new Headers(request.headers);
|
|
225
374
|
applyHeaderEditsTo(headers, rec.headers);
|
|
@@ -227,130 +376,174 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
227
376
|
const final = new Request(request, { headers });
|
|
228
377
|
byRequest.set(final, rec);
|
|
229
378
|
draft.request = final;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (!
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const quota = adapter.quotaFromHeaders?.(original.headers, original.status);
|
|
239
|
-
if (quota !== undefined)
|
|
240
|
-
noteQuota(rec, 'http', quota, original.status);
|
|
241
|
-
if (!original.ok && adapter.limitFromResponse) {
|
|
242
|
-
const signal = await adapter.limitFromResponse({
|
|
243
|
-
status: original.status,
|
|
244
|
-
headers: original.headers,
|
|
245
|
-
body: () => original.clone().text(),
|
|
246
|
-
});
|
|
247
|
-
if (signal)
|
|
248
|
-
noteLimit(rec, signal, 'http');
|
|
379
|
+
});
|
|
380
|
+
}, scoped));
|
|
381
|
+
registrations.push(await ctx.session.hook('http.response', async (draft) => {
|
|
382
|
+
const rec = byRequest.get(draft.request);
|
|
383
|
+
if (!rec) {
|
|
384
|
+
if (!warnedUnprovenResponse) {
|
|
385
|
+
warnedUnprovenResponse = true;
|
|
386
|
+
warn('opencode2 auth dropped an http.response whose request it did not produce', { sessionID: draft.sessionID, kind: draft.kind });
|
|
249
387
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const account = accountOf(rec);
|
|
391
|
+
const attempt = rec?.attempt;
|
|
392
|
+
if (!rec || !account || !attempt)
|
|
393
|
+
return;
|
|
394
|
+
const original = draft.response;
|
|
395
|
+
rec.status = original.status;
|
|
396
|
+
const quota = adapter.quotaFromHeaders?.(original.headers, original.status, attempt);
|
|
397
|
+
if (quota !== undefined)
|
|
398
|
+
noteQuota(rec, 'http', quota, original.status);
|
|
399
|
+
if (!original.ok && adapter.limitFromResponse) {
|
|
400
|
+
const signal = await adapter.limitFromResponse({
|
|
401
|
+
status: original.status,
|
|
402
|
+
headers: original.headers,
|
|
403
|
+
body: () => original.clone().text(),
|
|
404
|
+
attempt,
|
|
405
|
+
});
|
|
406
|
+
if (signal)
|
|
407
|
+
noteLimit(rec, signal, 'http');
|
|
408
|
+
}
|
|
409
|
+
// An error response carries no output, so its status is the outcome;
|
|
410
|
+
// ending here keeps the end from depending on whether the host reads
|
|
411
|
+
// the error body to the end.
|
|
412
|
+
if (!original.ok || !original.body)
|
|
413
|
+
finish(rec);
|
|
414
|
+
let response = original;
|
|
415
|
+
if (original.body && (adapter.inspectEvent || adapter.onAttemptEnd)) {
|
|
416
|
+
let body = original.body;
|
|
417
|
+
if (adapter.inspectEvent) {
|
|
418
|
+
body = body.pipeThrough(watchServerSentEvents((event) => inspect(rec, 'http', event.data, event.event), (error) => warn('opencode2 auth event inspection threw', {
|
|
253
419
|
error: describe(error),
|
|
254
|
-
}));
|
|
255
|
-
response = new Response(original.body.pipeThrough(watch), {
|
|
256
|
-
status: original.status,
|
|
257
|
-
statusText: original.statusText,
|
|
258
|
-
headers: original.headers,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
if (adapter.rewriteResponse) {
|
|
262
|
-
response =
|
|
263
|
-
(await adapter.rewriteResponse({
|
|
264
|
-
...account,
|
|
265
|
-
request: draft.request,
|
|
266
|
-
response,
|
|
267
|
-
})) ?? response;
|
|
420
|
+
})));
|
|
268
421
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
422
|
+
response = new Response(trackBody(body, (error) => finish(rec, error)), {
|
|
423
|
+
status: original.status,
|
|
424
|
+
statusText: original.statusText,
|
|
425
|
+
headers: original.headers,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (adapter.rewriteResponse) {
|
|
429
|
+
response =
|
|
430
|
+
(await adapter.rewriteResponse({
|
|
431
|
+
...account,
|
|
432
|
+
request: draft.request,
|
|
433
|
+
response,
|
|
434
|
+
attempt,
|
|
435
|
+
})) ?? response;
|
|
436
|
+
}
|
|
437
|
+
if (response !== original)
|
|
438
|
+
draft.response = response;
|
|
439
|
+
}, scoped));
|
|
440
|
+
registrations.push(await ctx.session.hook('experimental.ws.handshake', async (draft) => {
|
|
441
|
+
const rec = await bind(scopeOf(draft), 'experimental.ws.handshake', 'ws');
|
|
442
|
+
const account = accountOf(rec);
|
|
443
|
+
const attempt = rec.attempt;
|
|
444
|
+
if (!account || !attempt)
|
|
445
|
+
throw noAccount(rec.scope);
|
|
446
|
+
await failing(rec, async () => {
|
|
277
447
|
applyHeaderEdits(draft.headers, rec.headers);
|
|
278
448
|
const url = adapter.rewriteHandshakeURL?.({
|
|
279
449
|
...account,
|
|
280
450
|
url: draft.url,
|
|
451
|
+
attempt,
|
|
281
452
|
});
|
|
282
453
|
if (url !== undefined)
|
|
283
454
|
draft.url = url;
|
|
284
455
|
guard(rec.scope, Object.entries(draft.headers));
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
456
|
+
});
|
|
457
|
+
}, scoped));
|
|
458
|
+
const rewriteFrame = adapter.rewriteWebSocketFrame?.bind(adapter);
|
|
459
|
+
if (rewriteFrame) {
|
|
460
|
+
registrations.push(await ctx.session.hook('experimental.ws.send', async (draft) => {
|
|
461
|
+
const scope = scopeOf(draft);
|
|
462
|
+
const rec = liveOn(records.get(keyOf(scope.sessionID, scope.kind)), 'ws');
|
|
463
|
+
const frame = await rewriteFrame({
|
|
464
|
+
...scope,
|
|
465
|
+
attempt: rec?.attempt,
|
|
466
|
+
frame: draft.frame,
|
|
467
|
+
});
|
|
468
|
+
if (frame !== undefined)
|
|
469
|
+
draft.frame = frame;
|
|
470
|
+
}, scoped));
|
|
471
|
+
}
|
|
472
|
+
registrations.push(await ctx.session.hook('experimental.ws.receive', (draft) => {
|
|
473
|
+
const rec = liveOn(records.get(keyOf(draft.sessionID, draft.kind)), 'ws');
|
|
474
|
+
if (!rec)
|
|
475
|
+
return;
|
|
476
|
+
try {
|
|
477
|
+
inspect(rec, 'ws', draft.frame);
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
warn('opencode2 auth frame inspection threw', {
|
|
481
|
+
error: describe(error),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}, scoped));
|
|
485
|
+
registrations.push(await ctx.session.hook('retry', async (draft) => {
|
|
486
|
+
const rec = pickForRetry(draft.sessionID);
|
|
487
|
+
if (!rec)
|
|
488
|
+
return;
|
|
489
|
+
const hostDecision = draft.decision;
|
|
490
|
+
let decision = hostDecision;
|
|
491
|
+
let reason;
|
|
492
|
+
const attempt = rec.attempt;
|
|
493
|
+
if (rec.accountId === undefined || !attempt) {
|
|
494
|
+
decision = { retry: false };
|
|
495
|
+
reason = 'no-account';
|
|
496
|
+
}
|
|
497
|
+
else if (rec.outputStarted) {
|
|
498
|
+
// The user has already seen part of this answer; a retry would
|
|
499
|
+
// send it again.
|
|
500
|
+
decision = { retry: false };
|
|
501
|
+
reason = 'output-started';
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
if (!rec.limit) {
|
|
505
|
+
const signal = adapter.limitFromError?.(draft.error, attempt);
|
|
506
|
+
if (signal)
|
|
507
|
+
noteLimit(rec, signal, 'error');
|
|
309
508
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
509
|
+
// The next `chooseAccount` should see whatever the plugin learnt
|
|
510
|
+
// from how this attempt ended.
|
|
511
|
+
if (rec.ended)
|
|
512
|
+
await rec.ended;
|
|
513
|
+
if (rec.limit) {
|
|
514
|
+
await rec.limit.delivered;
|
|
515
|
+
rec.rerouteFrom = {
|
|
516
|
+
accountId: rec.accountId,
|
|
517
|
+
limit: rec.limit.signal,
|
|
518
|
+
};
|
|
519
|
+
// No delay: the next attempt goes to another account, and the
|
|
520
|
+
// host would otherwise wait out the refused account's backoff,
|
|
521
|
+
// or not retry at all for errors it deems final.
|
|
522
|
+
decision = { retry: true, delay: 0 };
|
|
523
|
+
reason = 'reroute';
|
|
315
524
|
}
|
|
316
525
|
else {
|
|
317
|
-
|
|
318
|
-
const signal = adapter.limitFromError?.(draft.error);
|
|
319
|
-
if (signal)
|
|
320
|
-
noteLimit(rec, signal, 'error');
|
|
321
|
-
}
|
|
322
|
-
if (rec.limit) {
|
|
323
|
-
await rec.limit.delivered;
|
|
324
|
-
rec.rerouteFrom = {
|
|
325
|
-
accountId: rec.accountId,
|
|
326
|
-
limit: rec.limit.signal,
|
|
327
|
-
};
|
|
328
|
-
// No delay: the next attempt goes to another account, and the
|
|
329
|
-
// host would otherwise wait out the refused account's backoff,
|
|
330
|
-
// or not retry at all for errors it deems final.
|
|
331
|
-
decision = { retry: true, delay: 0 };
|
|
332
|
-
reason = 'reroute';
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
reason = 'host-decides';
|
|
336
|
-
}
|
|
526
|
+
reason = 'host-decides';
|
|
337
527
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
528
|
+
}
|
|
529
|
+
draft.decision = decision;
|
|
530
|
+
await emit('retry', {
|
|
531
|
+
sessionID: draft.sessionID,
|
|
532
|
+
...(rec.accountId === undefined ? {} : { accountId: rec.accountId }),
|
|
533
|
+
kind: rec.scope.kind,
|
|
534
|
+
attempt: draft.attempt,
|
|
535
|
+
reason,
|
|
536
|
+
hostDecision,
|
|
537
|
+
decision,
|
|
538
|
+
...(attempt === undefined ? {} : { handle: attempt }),
|
|
539
|
+
});
|
|
540
|
+
}, scoped));
|
|
350
541
|
const forgetSession = (sessionID) => {
|
|
351
542
|
for (const [key, rec] of records) {
|
|
352
|
-
if (rec.scope.sessionID
|
|
353
|
-
|
|
543
|
+
if (rec.scope.sessionID !== sessionID)
|
|
544
|
+
continue;
|
|
545
|
+
abandon(rec, 'its session was forgotten');
|
|
546
|
+
records.delete(key);
|
|
354
547
|
}
|
|
355
548
|
};
|
|
356
549
|
const abort = new AbortController();
|
|
@@ -398,6 +591,8 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
398
591
|
return;
|
|
399
592
|
disposed = true;
|
|
400
593
|
abort.abort();
|
|
594
|
+
for (const rec of records.values())
|
|
595
|
+
abandon(rec, 'the installation was disposed');
|
|
401
596
|
records.clear();
|
|
402
597
|
listeners.clear();
|
|
403
598
|
await Promise.all(registrations.map(async (registration) => {
|