@cortexkit/common-auth 0.2.8 → 0.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/dump/index.js +6 -2
- package/dist/opencode2/index.d.ts +1 -1
- package/dist/opencode2/install.d.ts +18 -5
- package/dist/opencode2/install.js +327 -141
- package/dist/opencode2/types.d.ts +130 -9
- package/package.json +1 -1
package/dist/dump/index.js
CHANGED
|
@@ -68,9 +68,13 @@ export function diffDumpBodies(previous, current) {
|
|
|
68
68
|
currentBytes: current.length,
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
+
// Shared by every dumper in the process. A plugin may create one dumper per
|
|
72
|
+
// project, and two of them writing the same session in the same millisecond
|
|
73
|
+
// would otherwise both number their dump 1 and pick the same file name; the
|
|
74
|
+
// exclusive create then drops the second dump.
|
|
75
|
+
let processDumpCounter = 0;
|
|
71
76
|
export function createDumper(options) {
|
|
72
77
|
let enabled = options.enabled === true;
|
|
73
|
-
let counter = 0;
|
|
74
78
|
const pid = options.pid ?? process.pid;
|
|
75
79
|
const now = options.now ?? Date.now;
|
|
76
80
|
const log = options.logger;
|
|
@@ -189,7 +193,7 @@ export function createDumper(options) {
|
|
|
189
193
|
async function dump(input) {
|
|
190
194
|
if (!enabled)
|
|
191
195
|
return undefined;
|
|
192
|
-
counter
|
|
196
|
+
const counter = ++processDumpCounter;
|
|
193
197
|
const session = input.session?.trim() || UNKNOWN_SESSION;
|
|
194
198
|
const dir = resolveDir();
|
|
195
199
|
const createdAt = new Date(now()).toISOString();
|
|
@@ -5,4 +5,4 @@ export type { FormAnswer, PoolAuthorization, PoolLoginMethod, RegisterOpenCode2A
|
|
|
5
5
|
export { isPlaceholderCredential, PLACEHOLDER_LIFETIME_MS, PLACEHOLDER_METADATA_KEY, PLACEHOLDER_PREFIX, placeholderCredential, placeholderSecret, registerOpenCode2AuthMethods, } from './integration.js';
|
|
6
6
|
export type { ServerSentEvent } from './sse.js';
|
|
7
7
|
export { watchServerSentEvents } from './sse.js';
|
|
8
|
-
export type { AccountRequest, ChooseAccountInput, EventVerdict, HeaderEdits, HostError, InstallOpenCode2AuthOptions, LimitSignal, OpenCode2AuthAdapter, OpenCode2AuthEventName, OpenCode2AuthEvents, OpenCode2AuthInstallation, OpenCode2AuthLogger, OpenCode2HookContext, RequestKind, RequestScope, RetryReason, SelectingHook, Transport, } from './types.js';
|
|
8
|
+
export type { AccountHeadersResult, AccountRequest, Attempt, AttemptEndReason, AttemptOutcome, ChooseAccountInput, EventVerdict, HeaderEdits, HostError, InstallOpenCode2AuthOptions, LimitSignal, OpenCode2AuthAdapter, OpenCode2AuthEventName, OpenCode2AuthEvents, OpenCode2AuthInstallation, OpenCode2AuthLogger, OpenCode2HookContext, RequestKind, RequestScope, RetryReason, SelectingHook, Transport, } from './types.js';
|
|
@@ -6,14 +6,27 @@ export declare function applyHeaderEdits(target: Record<string, string>, edits:
|
|
|
6
6
|
* Installs multi-account auth on OpenCode 2's own provider drivers. Every
|
|
7
7
|
* hook is scoped to `adapter.providerID`:
|
|
8
8
|
*
|
|
9
|
-
* - `model.request` picks the account for the request's `sessionID:kind
|
|
10
|
-
* sets its headers;
|
|
9
|
+
* - `model.request` picks the account for the request's `sessionID:kind`,
|
|
10
|
+
* which starts a new attempt, and sets its headers;
|
|
11
11
|
* - `http.request` and `experimental.ws.handshake` set them again, because
|
|
12
12
|
* the host applies its own credential after `model.request`;
|
|
13
|
-
* - `
|
|
14
|
-
*
|
|
13
|
+
* - `experimental.ws.send` (only when the adapter rewrites frames) rewrites
|
|
14
|
+
* each outgoing frame;
|
|
15
|
+
* - `http.response` and `experimental.ws.receive` read quota, refusals,
|
|
16
|
+
* whether output has started and when the response ended, attributed to
|
|
17
|
+
* an attempt by the rules below;
|
|
15
18
|
* - `retry` asks the host to retry at once when an account was refused
|
|
16
19
|
* before any output, so `model.request` runs again and can pick another
|
|
17
20
|
* account, and refuses to retry once output has started.
|
|
21
|
+
*
|
|
22
|
+
* Attribution. An HTTP response belongs to the attempt whose `http.request`
|
|
23
|
+
* produced its request; if the host hands back a different request object,
|
|
24
|
+
* it belongs to the newest attempt of its session and kind only while that
|
|
25
|
+
* attempt went out over HTTP and has no response yet. A WebSocket frame
|
|
26
|
+
* belongs to the newest attempt of its session and kind only while that
|
|
27
|
+
* attempt went out over WebSocket and has not ended: the host runs one
|
|
28
|
+
* exchange at a time per session socket, so frames between one handshake
|
|
29
|
+
* and the next belong to the earlier attempt. Anything else is attributed to
|
|
30
|
+
* no attempt and reaches no adapter callback or listener.
|
|
18
31
|
*/
|
|
19
|
-
export declare function installOpenCode2Auth<Q>(ctx: OpenCode2HookContext, adapter: OpenCode2AuthAdapter<Q>, options?: InstallOpenCode2AuthOptions): Promise<OpenCode2AuthInstallation<Q>>;
|
|
32
|
+
export declare function installOpenCode2Auth<Q, A = unknown>(ctx: OpenCode2HookContext, adapter: OpenCode2AuthAdapter<Q, A>, options?: InstallOpenCode2AuthOptions): Promise<OpenCode2AuthInstallation<Q, A>>;
|
|
@@ -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,28 @@ 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; if the host hands back a different request object,
|
|
95
|
+
* it belongs to the newest attempt of its session and kind only while that
|
|
96
|
+
* attempt went out over HTTP and has no response yet. A WebSocket frame
|
|
97
|
+
* belongs to the newest attempt of its session and kind only while that
|
|
98
|
+
* attempt went out over WebSocket and has not ended: the host runs one
|
|
99
|
+
* exchange at a time per session socket, so frames between one handshake
|
|
100
|
+
* and the next belong to the earlier attempt. Anything else is attributed to
|
|
101
|
+
* no attempt and reaches no adapter callback or listener.
|
|
46
102
|
*/
|
|
47
103
|
export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
48
104
|
const { providerID } = adapter;
|
|
@@ -76,15 +132,40 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
76
132
|
}
|
|
77
133
|
}));
|
|
78
134
|
};
|
|
135
|
+
const deliverEnd = async (attempt, outcome) => {
|
|
136
|
+
try {
|
|
137
|
+
await adapter.onAttemptEnd?.(attempt, outcome);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
warn('opencode2 auth onAttemptEnd threw; the request is unaffected', {
|
|
141
|
+
attemptId: attempt.attemptId,
|
|
142
|
+
error: describe(error),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/** Ends an attempt once; later calls for the same attempt do nothing. */
|
|
147
|
+
const finish = (rec, error) => {
|
|
148
|
+
if (rec.ended || !rec.attempt)
|
|
149
|
+
return;
|
|
150
|
+
const outcome = {
|
|
151
|
+
...(rec.status === undefined ? {} : { status: rec.status }),
|
|
152
|
+
outputStarted: rec.outputStarted,
|
|
153
|
+
...(rec.limit ? { limit: rec.limit.signal } : {}),
|
|
154
|
+
...(error ? { error } : {}),
|
|
155
|
+
};
|
|
156
|
+
rec.ended = deliverEnd(rec.attempt, outcome);
|
|
157
|
+
};
|
|
158
|
+
const abandon = (rec, message) => finish(rec, { reason: 'abandoned', message });
|
|
79
159
|
const remember = (rec) => {
|
|
80
160
|
const key = keyOf(rec.scope.sessionID, rec.scope.kind);
|
|
81
161
|
records.delete(key);
|
|
82
162
|
records.set(key, rec);
|
|
83
163
|
while (records.size > maxRecords) {
|
|
84
|
-
const oldest = records.
|
|
164
|
+
const oldest = records.entries().next().value;
|
|
85
165
|
if (oldest === undefined)
|
|
86
166
|
break;
|
|
87
|
-
|
|
167
|
+
abandon(oldest[1], 'dropped to keep the record bound');
|
|
168
|
+
records.delete(oldest[0]);
|
|
88
169
|
}
|
|
89
170
|
};
|
|
90
171
|
const accountOf = (rec) => rec.accountId === undefined
|
|
@@ -109,8 +190,10 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
109
190
|
}
|
|
110
191
|
}
|
|
111
192
|
};
|
|
112
|
-
const select = async (scope, hook) => {
|
|
193
|
+
const select = async (scope, hook, transport) => {
|
|
113
194
|
const prior = records.get(keyOf(scope.sessionID, scope.kind));
|
|
195
|
+
if (prior)
|
|
196
|
+
abandon(prior, 'a newer attempt of its session and kind began');
|
|
114
197
|
const input = {
|
|
115
198
|
...scope,
|
|
116
199
|
...(prior?.accountId === undefined
|
|
@@ -121,21 +204,42 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
121
204
|
: { rerouteFrom: prior.rerouteFrom }),
|
|
122
205
|
};
|
|
123
206
|
const accountId = await adapter.chooseAccount(input);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
207
|
+
let headers = {};
|
|
208
|
+
let data;
|
|
209
|
+
if (accountId !== undefined) {
|
|
210
|
+
const result = await adapter.accountHeaders({ ...scope, accountId });
|
|
211
|
+
if (isHeadersResult(result)) {
|
|
212
|
+
headers = result.headers;
|
|
213
|
+
data = result.attempt;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
headers = result;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const id = ++seq;
|
|
127
220
|
const rec = {
|
|
128
221
|
scope,
|
|
129
222
|
accountId,
|
|
130
223
|
headers,
|
|
131
|
-
seq:
|
|
224
|
+
seq: id,
|
|
225
|
+
attempt: accountId === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: {
|
|
228
|
+
...scope,
|
|
229
|
+
accountId,
|
|
230
|
+
attemptId: `attempt-${id}`,
|
|
231
|
+
transport,
|
|
232
|
+
data,
|
|
233
|
+
},
|
|
234
|
+
...(transport === undefined ? {} : { transport }),
|
|
235
|
+
responded: false,
|
|
132
236
|
outputStarted: false,
|
|
133
237
|
};
|
|
134
238
|
remember(rec);
|
|
135
|
-
if (
|
|
239
|
+
if (rec.attempt) {
|
|
136
240
|
void emit('select', {
|
|
137
241
|
...scope,
|
|
138
|
-
accountId,
|
|
242
|
+
accountId: rec.attempt.accountId,
|
|
139
243
|
hook,
|
|
140
244
|
...(input.previousAccountId === undefined
|
|
141
245
|
? {}
|
|
@@ -143,17 +247,40 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
143
247
|
...(input.rerouteFrom === undefined
|
|
144
248
|
? {}
|
|
145
249
|
: { rerouteFrom: input.rerouteFrom }),
|
|
250
|
+
handle: rec.attempt,
|
|
146
251
|
});
|
|
147
252
|
}
|
|
148
253
|
return rec;
|
|
149
254
|
};
|
|
150
|
-
// The transport hooks normally follow `model.request
|
|
151
|
-
// keeps a request that skipped it
|
|
152
|
-
|
|
153
|
-
|
|
255
|
+
// The transport hooks normally follow `model.request` and carry the
|
|
256
|
+
// attempt it started. Choosing here too keeps a request that skipped it
|
|
257
|
+
// from going out under the host credential, and a second send without a
|
|
258
|
+
// `model.request` in between from reusing the first send's attempt.
|
|
259
|
+
const bind = async (scope, hook, transport) => {
|
|
260
|
+
const rec = records.get(keyOf(scope.sessionID, scope.kind));
|
|
261
|
+
if (!rec || rec.transport !== undefined || rec.ended)
|
|
262
|
+
return select(scope, hook, transport);
|
|
263
|
+
rec.transport = transport;
|
|
264
|
+
if (rec.attempt)
|
|
265
|
+
rec.attempt.transport = transport;
|
|
266
|
+
return rec;
|
|
267
|
+
};
|
|
268
|
+
// A transport hook that throws stops the send, so its attempt is over.
|
|
269
|
+
const failing = async (rec, run) => {
|
|
270
|
+
try {
|
|
271
|
+
return await run();
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
finish(rec, { reason: 'failed', message: describe(error) });
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const liveOn = (rec, transport) => rec?.attempt !== undefined && rec.transport === transport && !rec.ended
|
|
279
|
+
? rec
|
|
280
|
+
: undefined;
|
|
154
281
|
const noteLimit = (rec, signal, via) => {
|
|
155
282
|
const account = accountOf(rec);
|
|
156
|
-
if (!account || rec.limit)
|
|
283
|
+
if (!account || !rec.attempt || rec.limit)
|
|
157
284
|
return;
|
|
158
285
|
rec.limit = {
|
|
159
286
|
signal,
|
|
@@ -162,22 +289,29 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
162
289
|
via,
|
|
163
290
|
limit: signal,
|
|
164
291
|
outputStarted: rec.outputStarted,
|
|
292
|
+
handle: rec.attempt,
|
|
165
293
|
}),
|
|
166
294
|
};
|
|
167
295
|
};
|
|
168
296
|
const noteQuota = (rec, transport, quota, status) => {
|
|
169
297
|
const account = accountOf(rec);
|
|
170
|
-
if (!account)
|
|
298
|
+
if (!account || !rec.attempt)
|
|
171
299
|
return;
|
|
172
300
|
void emit('quota', {
|
|
173
301
|
...account,
|
|
174
302
|
transport,
|
|
175
303
|
...(status === undefined ? {} : { status }),
|
|
176
304
|
quota,
|
|
305
|
+
handle: rec.attempt,
|
|
177
306
|
});
|
|
178
307
|
};
|
|
179
308
|
const inspect = (rec, transport, data, event) => {
|
|
180
|
-
const
|
|
309
|
+
const attempt = rec.attempt;
|
|
310
|
+
if (!attempt)
|
|
311
|
+
return;
|
|
312
|
+
const verdict = adapter.inspectEvent?.(event === undefined
|
|
313
|
+
? { transport, data, attempt }
|
|
314
|
+
: { transport, data, event, attempt });
|
|
181
315
|
if (!verdict)
|
|
182
316
|
return;
|
|
183
317
|
if (verdict.quota !== undefined)
|
|
@@ -186,6 +320,10 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
186
320
|
rec.outputStarted = true;
|
|
187
321
|
if (verdict.limit)
|
|
188
322
|
noteLimit(rec, verdict.limit, transport);
|
|
323
|
+
if (verdict.error !== undefined)
|
|
324
|
+
finish(rec, { reason: 'failed', message: verdict.error });
|
|
325
|
+
else if (verdict.done)
|
|
326
|
+
finish(rec);
|
|
189
327
|
};
|
|
190
328
|
const pickForRetry = (sessionID) => {
|
|
191
329
|
let refused;
|
|
@@ -204,22 +342,28 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
204
342
|
return refused ?? primary ?? latest;
|
|
205
343
|
};
|
|
206
344
|
const scoped = { providerID };
|
|
207
|
-
const registrations = [
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
345
|
+
const registrations = [];
|
|
346
|
+
registrations.push(await ctx.session.hook('model.request', async (draft) => {
|
|
347
|
+
const rec = await select(scopeOf(draft), 'model.request');
|
|
348
|
+
if (rec.accountId === undefined)
|
|
349
|
+
throw noAccount(rec.scope);
|
|
350
|
+
applyHeaderEdits(draft.headers, rec.headers);
|
|
351
|
+
}, scoped));
|
|
352
|
+
registrations.push(await ctx.session.hook('http.request', async (draft) => {
|
|
353
|
+
const rec = await bind(scopeOf(draft), 'http.request', 'http');
|
|
354
|
+
const account = accountOf(rec);
|
|
355
|
+
const attempt = rec.attempt;
|
|
356
|
+
if (!account || !attempt)
|
|
357
|
+
throw noAccount(rec.scope);
|
|
358
|
+
await failing(rec, async () => {
|
|
219
359
|
let request = draft.request;
|
|
220
360
|
if (adapter.rewriteRequest) {
|
|
221
361
|
request =
|
|
222
|
-
(await adapter.rewriteRequest({
|
|
362
|
+
(await adapter.rewriteRequest({
|
|
363
|
+
...account,
|
|
364
|
+
request,
|
|
365
|
+
attempt,
|
|
366
|
+
})) ?? request;
|
|
223
367
|
}
|
|
224
368
|
const headers = new Headers(request.headers);
|
|
225
369
|
applyHeaderEditsTo(headers, rec.headers);
|
|
@@ -227,130 +371,170 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
227
371
|
const final = new Request(request, { headers });
|
|
228
372
|
byRequest.set(final, rec);
|
|
229
373
|
draft.request = final;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
374
|
+
});
|
|
375
|
+
}, scoped));
|
|
376
|
+
registrations.push(await ctx.session.hook('http.response', async (draft) => {
|
|
377
|
+
const latest = records.get(keyOf(draft.sessionID, draft.kind));
|
|
378
|
+
const rec = byRequest.get(draft.request) ??
|
|
379
|
+
(latest && !latest.responded ? liveOn(latest, 'http') : undefined);
|
|
380
|
+
const account = rec && accountOf(rec);
|
|
381
|
+
const attempt = rec?.attempt;
|
|
382
|
+
if (!rec || !account || !attempt)
|
|
383
|
+
return;
|
|
384
|
+
const original = draft.response;
|
|
385
|
+
rec.responded = true;
|
|
386
|
+
rec.status = original.status;
|
|
387
|
+
const quota = adapter.quotaFromHeaders?.(original.headers, original.status, attempt);
|
|
388
|
+
if (quota !== undefined)
|
|
389
|
+
noteQuota(rec, 'http', quota, original.status);
|
|
390
|
+
if (!original.ok && adapter.limitFromResponse) {
|
|
391
|
+
const signal = await adapter.limitFromResponse({
|
|
392
|
+
status: original.status,
|
|
393
|
+
headers: original.headers,
|
|
394
|
+
body: () => original.clone().text(),
|
|
395
|
+
attempt,
|
|
396
|
+
});
|
|
397
|
+
if (signal)
|
|
398
|
+
noteLimit(rec, signal, 'http');
|
|
399
|
+
}
|
|
400
|
+
// An error response carries no output, so its status is the outcome;
|
|
401
|
+
// ending here keeps the end from depending on whether the host reads
|
|
402
|
+
// the error body to the end.
|
|
403
|
+
if (!original.ok || !original.body)
|
|
404
|
+
finish(rec);
|
|
405
|
+
let response = original;
|
|
406
|
+
if (original.body && (adapter.inspectEvent || adapter.onAttemptEnd)) {
|
|
407
|
+
let body = original.body;
|
|
408
|
+
if (adapter.inspectEvent) {
|
|
409
|
+
body = body.pipeThrough(watchServerSentEvents((event) => inspect(rec, 'http', event.data, event.event), (error) => warn('opencode2 auth event inspection threw', {
|
|
253
410
|
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
|
-
});
|
|
411
|
+
})));
|
|
260
412
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
413
|
+
response = new Response(trackBody(body, (error) => finish(rec, error)), {
|
|
414
|
+
status: original.status,
|
|
415
|
+
statusText: original.statusText,
|
|
416
|
+
headers: original.headers,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (adapter.rewriteResponse) {
|
|
420
|
+
response =
|
|
421
|
+
(await adapter.rewriteResponse({
|
|
422
|
+
...account,
|
|
423
|
+
request: draft.request,
|
|
424
|
+
response,
|
|
425
|
+
attempt,
|
|
426
|
+
})) ?? response;
|
|
427
|
+
}
|
|
428
|
+
if (response !== original)
|
|
429
|
+
draft.response = response;
|
|
430
|
+
}, scoped));
|
|
431
|
+
registrations.push(await ctx.session.hook('experimental.ws.handshake', async (draft) => {
|
|
432
|
+
const rec = await bind(scopeOf(draft), 'experimental.ws.handshake', 'ws');
|
|
433
|
+
const account = accountOf(rec);
|
|
434
|
+
const attempt = rec.attempt;
|
|
435
|
+
if (!account || !attempt)
|
|
436
|
+
throw noAccount(rec.scope);
|
|
437
|
+
await failing(rec, async () => {
|
|
277
438
|
applyHeaderEdits(draft.headers, rec.headers);
|
|
278
439
|
const url = adapter.rewriteHandshakeURL?.({
|
|
279
440
|
...account,
|
|
280
441
|
url: draft.url,
|
|
442
|
+
attempt,
|
|
281
443
|
});
|
|
282
444
|
if (url !== undefined)
|
|
283
445
|
draft.url = url;
|
|
284
446
|
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
|
-
|
|
447
|
+
});
|
|
448
|
+
}, scoped));
|
|
449
|
+
const rewriteFrame = adapter.rewriteWebSocketFrame?.bind(adapter);
|
|
450
|
+
if (rewriteFrame) {
|
|
451
|
+
registrations.push(await ctx.session.hook('experimental.ws.send', async (draft) => {
|
|
452
|
+
const scope = scopeOf(draft);
|
|
453
|
+
const rec = liveOn(records.get(keyOf(scope.sessionID, scope.kind)), 'ws');
|
|
454
|
+
const frame = await rewriteFrame({
|
|
455
|
+
...scope,
|
|
456
|
+
attempt: rec?.attempt,
|
|
457
|
+
frame: draft.frame,
|
|
458
|
+
});
|
|
459
|
+
if (frame !== undefined)
|
|
460
|
+
draft.frame = frame;
|
|
461
|
+
}, scoped));
|
|
462
|
+
}
|
|
463
|
+
registrations.push(await ctx.session.hook('experimental.ws.receive', (draft) => {
|
|
464
|
+
const rec = liveOn(records.get(keyOf(draft.sessionID, draft.kind)), 'ws');
|
|
465
|
+
if (!rec)
|
|
466
|
+
return;
|
|
467
|
+
try {
|
|
468
|
+
inspect(rec, 'ws', draft.frame);
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
warn('opencode2 auth frame inspection threw', {
|
|
472
|
+
error: describe(error),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}, scoped));
|
|
476
|
+
registrations.push(await ctx.session.hook('retry', async (draft) => {
|
|
477
|
+
const rec = pickForRetry(draft.sessionID);
|
|
478
|
+
if (!rec)
|
|
479
|
+
return;
|
|
480
|
+
const hostDecision = draft.decision;
|
|
481
|
+
let decision = hostDecision;
|
|
482
|
+
let reason;
|
|
483
|
+
const attempt = rec.attempt;
|
|
484
|
+
if (rec.accountId === undefined || !attempt) {
|
|
485
|
+
decision = { retry: false };
|
|
486
|
+
reason = 'no-account';
|
|
487
|
+
}
|
|
488
|
+
else if (rec.outputStarted) {
|
|
489
|
+
// The user has already seen part of this answer; a retry would
|
|
490
|
+
// send it again.
|
|
491
|
+
decision = { retry: false };
|
|
492
|
+
reason = 'output-started';
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
if (!rec.limit) {
|
|
496
|
+
const signal = adapter.limitFromError?.(draft.error, attempt);
|
|
497
|
+
if (signal)
|
|
498
|
+
noteLimit(rec, signal, 'error');
|
|
309
499
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
500
|
+
// The next `chooseAccount` should see whatever the plugin learnt
|
|
501
|
+
// from how this attempt ended.
|
|
502
|
+
if (rec.ended)
|
|
503
|
+
await rec.ended;
|
|
504
|
+
if (rec.limit) {
|
|
505
|
+
await rec.limit.delivered;
|
|
506
|
+
rec.rerouteFrom = {
|
|
507
|
+
accountId: rec.accountId,
|
|
508
|
+
limit: rec.limit.signal,
|
|
509
|
+
};
|
|
510
|
+
// No delay: the next attempt goes to another account, and the
|
|
511
|
+
// host would otherwise wait out the refused account's backoff,
|
|
512
|
+
// or not retry at all for errors it deems final.
|
|
513
|
+
decision = { retry: true, delay: 0 };
|
|
514
|
+
reason = 'reroute';
|
|
315
515
|
}
|
|
316
516
|
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
|
-
}
|
|
517
|
+
reason = 'host-decides';
|
|
337
518
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
519
|
+
}
|
|
520
|
+
draft.decision = decision;
|
|
521
|
+
await emit('retry', {
|
|
522
|
+
sessionID: draft.sessionID,
|
|
523
|
+
...(rec.accountId === undefined ? {} : { accountId: rec.accountId }),
|
|
524
|
+
kind: rec.scope.kind,
|
|
525
|
+
attempt: draft.attempt,
|
|
526
|
+
reason,
|
|
527
|
+
hostDecision,
|
|
528
|
+
decision,
|
|
529
|
+
...(attempt === undefined ? {} : { handle: attempt }),
|
|
530
|
+
});
|
|
531
|
+
}, scoped));
|
|
350
532
|
const forgetSession = (sessionID) => {
|
|
351
533
|
for (const [key, rec] of records) {
|
|
352
|
-
if (rec.scope.sessionID
|
|
353
|
-
|
|
534
|
+
if (rec.scope.sessionID !== sessionID)
|
|
535
|
+
continue;
|
|
536
|
+
abandon(rec, 'its session was forgotten');
|
|
537
|
+
records.delete(key);
|
|
354
538
|
}
|
|
355
539
|
};
|
|
356
540
|
const abort = new AbortController();
|
|
@@ -398,6 +582,8 @@ export async function installOpenCode2Auth(ctx, adapter, options = {}) {
|
|
|
398
582
|
return;
|
|
399
583
|
disposed = true;
|
|
400
584
|
abort.abort();
|
|
585
|
+
for (const rec of records.values())
|
|
586
|
+
abandon(rec, 'the installation was disposed');
|
|
401
587
|
records.clear();
|
|
402
588
|
listeners.clear();
|
|
403
589
|
await Promise.all(registrations.map(async (registration) => {
|
|
@@ -35,6 +35,17 @@ export interface EventVerdict<Q> {
|
|
|
35
35
|
readonly outputStarted?: boolean;
|
|
36
36
|
readonly quota?: Q;
|
|
37
37
|
readonly limit?: LimitSignal;
|
|
38
|
+
/**
|
|
39
|
+
* This is the last event of the response (completed or failed); the
|
|
40
|
+
* attempt ends here. On WebSocket this is the only way an attempt ends
|
|
41
|
+
* normally, because the socket stays open between responses.
|
|
42
|
+
*/
|
|
43
|
+
readonly done?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The response failed with this message. Ends the attempt with
|
|
46
|
+
* `error.reason` `failed`; implies `done`.
|
|
47
|
+
*/
|
|
48
|
+
readonly error?: string;
|
|
38
49
|
}
|
|
39
50
|
/**
|
|
40
51
|
* Header changes for one account. A string sets the header (replacing every
|
|
@@ -57,6 +68,65 @@ export interface ChooseAccountInput extends RequestScope {
|
|
|
57
68
|
export interface AccountRequest extends RequestScope {
|
|
58
69
|
readonly accountId: string;
|
|
59
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* One physical send: created when an account is chosen for a request, and
|
|
73
|
+
* handed back on every hook call and event that belongs to that send, so a
|
|
74
|
+
* plugin can tie a response, a frame, a limit, a retry decision and the end
|
|
75
|
+
* of the send to the credential it chose for it.
|
|
76
|
+
*
|
|
77
|
+
* `A` is the plugin's own per-attempt value, returned by `accountHeaders`
|
|
78
|
+
* next to the headers (a served credential receipt, say); the installer
|
|
79
|
+
* only carries it.
|
|
80
|
+
*/
|
|
81
|
+
export interface Attempt<A = unknown> extends AccountRequest {
|
|
82
|
+
/** Unique within one installation. */
|
|
83
|
+
readonly attemptId: string;
|
|
84
|
+
/**
|
|
85
|
+
* The transport that carried the send. `undefined` until `http.request` or
|
|
86
|
+
* `experimental.ws.handshake` has run for it: an account chosen in
|
|
87
|
+
* `model.request` is chosen before the host has picked the transport.
|
|
88
|
+
*/
|
|
89
|
+
readonly transport: Transport | undefined;
|
|
90
|
+
/** What `accountHeaders` returned as `attempt`, if anything. */
|
|
91
|
+
readonly data: A | undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Why an attempt ended without completing.
|
|
95
|
+
*
|
|
96
|
+
* - `failed`: the response stream errored, or the adapter's `inspectEvent`
|
|
97
|
+
* reported the response as failed;
|
|
98
|
+
* - `cancelled`: the host cancelled the HTTP response body (the user stopped
|
|
99
|
+
* the turn, say);
|
|
100
|
+
* - `abandoned`: the attempt was still open when a newer attempt of the same
|
|
101
|
+
* session and kind began, its session was forgotten, its record was
|
|
102
|
+
* dropped for space, or the installation was disposed. A WebSocket closed
|
|
103
|
+
* or cancelled mid-response ends this way, since the host has no hook for
|
|
104
|
+
* either.
|
|
105
|
+
*/
|
|
106
|
+
export type AttemptEndReason = 'failed' | 'cancelled' | 'abandoned';
|
|
107
|
+
/** How an attempt ended, as `onAttemptEnd` reports it. */
|
|
108
|
+
export interface AttemptOutcome {
|
|
109
|
+
/** The HTTP status of the response, when an HTTP response was seen. */
|
|
110
|
+
readonly status?: number;
|
|
111
|
+
/** Output from this attempt had reached the user. */
|
|
112
|
+
readonly outputStarted: boolean;
|
|
113
|
+
/** The account-level refusal recorded for this attempt, if any. */
|
|
114
|
+
readonly limit?: LimitSignal;
|
|
115
|
+
/** Absent when the response completed, whatever its status. */
|
|
116
|
+
readonly error?: {
|
|
117
|
+
readonly reason: AttemptEndReason;
|
|
118
|
+
readonly message?: string;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `accountHeaders` may return the header edits alone, or the edits together
|
|
123
|
+
* with a per-attempt value that the installer hands back in
|
|
124
|
+
* `Attempt.data`.
|
|
125
|
+
*/
|
|
126
|
+
export interface AccountHeadersResult<A = unknown> {
|
|
127
|
+
readonly headers: HeaderEdits;
|
|
128
|
+
readonly attempt?: A;
|
|
129
|
+
}
|
|
60
130
|
/** A host-supplied error, as the retry hook reports it. */
|
|
61
131
|
export interface HostError {
|
|
62
132
|
readonly type: string;
|
|
@@ -66,8 +136,9 @@ export interface HostError {
|
|
|
66
136
|
/**
|
|
67
137
|
* Everything provider-specific the installer needs. `Q` is the plugin's own
|
|
68
138
|
* quota reading type; the installer only carries it to the `quota` event.
|
|
139
|
+
* `A` is the plugin's per-attempt value (see `Attempt`).
|
|
69
140
|
*/
|
|
70
|
-
export interface OpenCode2AuthAdapter<Q = unknown> {
|
|
141
|
+
export interface OpenCode2AuthAdapter<Q = unknown, A = unknown> {
|
|
71
142
|
/** Every hook is scoped to this provider; other providers are untouched. */
|
|
72
143
|
readonly providerID: string;
|
|
73
144
|
/**
|
|
@@ -81,14 +152,19 @@ export interface OpenCode2AuthAdapter<Q = unknown> {
|
|
|
81
152
|
* `model.request`, `http.request` and `experimental.ws.handshake`. The last
|
|
82
153
|
* two run after the host has applied its own credential, so these headers
|
|
83
154
|
* win on the wire.
|
|
155
|
+
*
|
|
156
|
+
* Called once per attempt, when the account is chosen. Returning
|
|
157
|
+
* `{headers, attempt}` instead of the bare edits stores `attempt` as the
|
|
158
|
+
* attempt's `data`, handed back on everything that belongs to the attempt.
|
|
84
159
|
*/
|
|
85
|
-
accountHeaders(input: AccountRequest): Promise<HeaderEdits
|
|
160
|
+
accountHeaders(input: AccountRequest): Promise<HeaderEdits | AccountHeadersResult<A>> | HeaderEdits | AccountHeadersResult<A>;
|
|
86
161
|
/**
|
|
87
162
|
* Optional request rewrite (URL, body) before the account headers are
|
|
88
163
|
* applied. Return `undefined` to keep the request.
|
|
89
164
|
*/
|
|
90
165
|
rewriteRequest?(input: AccountRequest & {
|
|
91
166
|
readonly request: Request;
|
|
167
|
+
readonly attempt: Attempt<A>;
|
|
92
168
|
}): Promise<Request | undefined> | Request | undefined;
|
|
93
169
|
/**
|
|
94
170
|
* Optional response rewrite (body stream, status). It receives the
|
|
@@ -98,13 +174,35 @@ export interface OpenCode2AuthAdapter<Q = unknown> {
|
|
|
98
174
|
rewriteResponse?(input: AccountRequest & {
|
|
99
175
|
readonly request: Request;
|
|
100
176
|
readonly response: Response;
|
|
177
|
+
readonly attempt: Attempt<A>;
|
|
101
178
|
}): Promise<Response | undefined> | Response | undefined;
|
|
102
179
|
/** Optional WebSocket URL rewrite. Return `undefined` to keep the URL. */
|
|
103
180
|
rewriteHandshakeURL?(input: AccountRequest & {
|
|
104
181
|
readonly url: string;
|
|
182
|
+
readonly attempt: Attempt<A>;
|
|
105
183
|
}): string | undefined;
|
|
184
|
+
/**
|
|
185
|
+
* Optional rewrite of each outgoing WebSocket frame of this provider,
|
|
186
|
+
* from `experimental.ws.send`. Return the frame to send, or `undefined` to
|
|
187
|
+
* send it unchanged.
|
|
188
|
+
*
|
|
189
|
+
* The rewrite must be a deterministic function of the frame (stable for
|
|
190
|
+
* the session): the host chains turns with `previous_response_id` by
|
|
191
|
+
* diffing its own request as it was before this hook ran, so the server
|
|
192
|
+
* only stays in step when every frame is rewritten the same way. Do not
|
|
193
|
+
* touch `input` or `previous_response_id`. Adding or changing settings
|
|
194
|
+
* fields uniformly keeps the follow-up turn incremental.
|
|
195
|
+
*
|
|
196
|
+
* It runs for every frame, including one no attempt can be tied to
|
|
197
|
+
* (`attempt` is then `undefined`), so the rewrite never depends on
|
|
198
|
+
* attribution.
|
|
199
|
+
*/
|
|
200
|
+
rewriteWebSocketFrame?(input: RequestScope & {
|
|
201
|
+
readonly attempt: Attempt<A> | undefined;
|
|
202
|
+
readonly frame: string;
|
|
203
|
+
}): Promise<string | undefined> | string | undefined;
|
|
106
204
|
/** Quota carried in HTTP response headers. */
|
|
107
|
-
quotaFromHeaders?(headers: Headers, status: number): Q | undefined;
|
|
205
|
+
quotaFromHeaders?(headers: Headers, status: number, attempt: Attempt<A>): Q | undefined;
|
|
108
206
|
/**
|
|
109
207
|
* Recognises an account-level refusal from an HTTP response before its
|
|
110
208
|
* body is streamed. `body()` reads a copy, so the host still gets the body.
|
|
@@ -113,21 +211,35 @@ export interface OpenCode2AuthAdapter<Q = unknown> {
|
|
|
113
211
|
readonly status: number;
|
|
114
212
|
readonly headers: Headers;
|
|
115
213
|
readonly body: () => Promise<string>;
|
|
214
|
+
readonly attempt: Attempt<A>;
|
|
116
215
|
}): Promise<LimitSignal | undefined> | LimitSignal | undefined;
|
|
117
216
|
/**
|
|
118
217
|
* Inspects one server-sent event (`data` payload) or one WebSocket frame.
|
|
119
|
-
* Detects output, quota and
|
|
218
|
+
* Detects output, quota, refusals and the end of the response inside the
|
|
219
|
+
* stream.
|
|
120
220
|
*/
|
|
121
221
|
inspectEvent?(input: {
|
|
122
222
|
readonly transport: Transport;
|
|
123
223
|
readonly data: string;
|
|
124
224
|
readonly event?: string;
|
|
225
|
+
readonly attempt: Attempt<A>;
|
|
125
226
|
}): EventVerdict<Q> | undefined;
|
|
126
227
|
/**
|
|
127
228
|
* Recognises an account-level refusal from the error the host hands the
|
|
128
|
-
* retry hook, for refusals no other hook saw.
|
|
229
|
+
* retry hook, for refusals no other hook saw. `attempt` is the attempt
|
|
230
|
+
* the retry hook judged.
|
|
231
|
+
*/
|
|
232
|
+
limitFromError?(error: HostError, attempt: Attempt<A>): LimitSignal | undefined;
|
|
233
|
+
/**
|
|
234
|
+
* Called once per attempt when it ends: an HTTP response body finished,
|
|
235
|
+
* errored or was cancelled; an HTTP response with an error status or no
|
|
236
|
+
* body arrived; an event's verdict said `done` or `error`; or the attempt
|
|
237
|
+
* was abandoned (see `AttemptEndReason`). Errors are logged and never
|
|
238
|
+
* reach the host. The retry hook waits for this call to settle before it
|
|
239
|
+
* decides, so a plugin that records a refusal here has it in place when
|
|
240
|
+
* `chooseAccount` runs again.
|
|
129
241
|
*/
|
|
130
|
-
|
|
242
|
+
onAttemptEnd?(attempt: Attempt<A>, outcome: AttemptOutcome): Promise<void> | void;
|
|
131
243
|
}
|
|
132
244
|
export interface OpenCode2AuthLogger {
|
|
133
245
|
warn(message: string, data?: unknown): void;
|
|
@@ -150,18 +262,24 @@ export type RetryReason = 'reroute' | 'output-started' | 'no-account' | 'host-de
|
|
|
150
262
|
* plugin may want to log as a host change.
|
|
151
263
|
*/
|
|
152
264
|
export type SelectingHook = 'model.request' | 'http.request' | 'experimental.ws.handshake';
|
|
153
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Every event that belongs to one attempt carries it as `handle` (the
|
|
267
|
+
* `retry` event already uses `attempt` for the host's retry count).
|
|
268
|
+
*/
|
|
269
|
+
export interface OpenCode2AuthEvents<Q, A = unknown> {
|
|
154
270
|
/** An account was picked for a model request. */
|
|
155
271
|
readonly select: AccountRequest & {
|
|
156
272
|
readonly hook: SelectingHook;
|
|
157
273
|
readonly previousAccountId?: string;
|
|
158
274
|
readonly rerouteFrom?: ChooseAccountInput['rerouteFrom'];
|
|
275
|
+
readonly handle: Attempt<A>;
|
|
159
276
|
};
|
|
160
277
|
/** A quota reading, attributed through this installer's own record. */
|
|
161
278
|
readonly quota: AccountRequest & {
|
|
162
279
|
readonly transport: Transport;
|
|
163
280
|
readonly status?: number;
|
|
164
281
|
readonly quota: Q;
|
|
282
|
+
readonly handle: Attempt<A>;
|
|
165
283
|
};
|
|
166
284
|
/**
|
|
167
285
|
* An account-level refusal. The retry hook waits for every listener of
|
|
@@ -172,6 +290,7 @@ export interface OpenCode2AuthEvents<Q> {
|
|
|
172
290
|
readonly via: Transport | 'error';
|
|
173
291
|
readonly limit: LimitSignal;
|
|
174
292
|
readonly outputStarted: boolean;
|
|
293
|
+
readonly handle: Attempt<A>;
|
|
175
294
|
};
|
|
176
295
|
/** The retry hook ran for this provider. */
|
|
177
296
|
readonly retry: {
|
|
@@ -182,15 +301,17 @@ export interface OpenCode2AuthEvents<Q> {
|
|
|
182
301
|
readonly reason: RetryReason;
|
|
183
302
|
readonly hostDecision: SessionRetryDecision;
|
|
184
303
|
readonly decision: SessionRetryDecision;
|
|
304
|
+
/** The attempt the decision was about, when it had an account. */
|
|
305
|
+
readonly handle?: Attempt<A>;
|
|
185
306
|
};
|
|
186
307
|
}
|
|
187
308
|
export type OpenCode2AuthEventName = keyof OpenCode2AuthEvents<unknown>;
|
|
188
|
-
export interface OpenCode2AuthInstallation<Q> {
|
|
309
|
+
export interface OpenCode2AuthInstallation<Q, A = unknown> {
|
|
189
310
|
/**
|
|
190
311
|
* Listens to an event. Listener errors are logged and never reach the
|
|
191
312
|
* host. Returns a function that removes the listener.
|
|
192
313
|
*/
|
|
193
|
-
on<E extends OpenCode2AuthEventName>(event: E, listener: (payload: OpenCode2AuthEvents<Q>[E]) => void | Promise<void>): () => void;
|
|
314
|
+
on<E extends OpenCode2AuthEventName>(event: E, listener: (payload: OpenCode2AuthEvents<Q, A>[E]) => void | Promise<void>): () => void;
|
|
194
315
|
/** The account last chosen for a session and request kind. */
|
|
195
316
|
accountFor(sessionID: string, kind: RequestKind): string | undefined;
|
|
196
317
|
/** Drops every record of a session. Session deletion does this itself. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/common-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared code for the CortexKit auth plugins: account pool, quota and routing, commands and auth menu, OpenCode 2 hooks, Claustrum custody, and plumbing (loopback RPC, file locks, logger, sidebar state, TUI preferences and build).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|