@cortexkit/common-auth 0.4.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fs/index.d.ts +2 -1
- package/dist/fs/refresh-file-lock.d.ts +15 -4
- package/dist/fs/refresh-file-lock.js +67 -7
- package/dist/fs/with-lock.d.ts +9 -8
- package/dist/opencode2/index.d.ts +2 -2
- package/dist/opencode2/index.js +1 -1
- package/dist/opencode2/install.d.ts +28 -18
- package/dist/opencode2/install.js +236 -84
- package/dist/opencode2/types.d.ts +62 -14
- package/dist/store/errors.d.ts +2 -2
- package/dist/store/index.d.ts +4 -3
- package/dist/store/index.js +1 -1
- package/dist/store/mutate.d.ts +3 -1
- package/dist/store/mutate.js +4 -0
- package/dist/store/pool.d.ts +24 -8
- package/dist/store/pool.js +3 -0
- package/dist/store/provider-state.d.ts +86 -0
- package/dist/store/provider-state.js +186 -0
- package/dist/store/refresh.d.ts +8 -0
- package/dist/store/refresh.js +9 -0
- package/dist/store/rows.d.ts +22 -6
- package/dist/store/rows.js +60 -7
- package/dist/store/schema.d.ts +109 -1
- package/dist/store/schema.js +85 -8
- package/dist/store/torn.d.ts +2 -1
- package/dist/store/torn.js +4 -1
- package/package.json +2 -2
package/dist/fs/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type { AtomicWriteOptions } from './atomic-write.js';
|
|
2
2
|
export { writeJsonAtomic } from './atomic-write.js';
|
|
3
3
|
export { WRITER_LOCK_CONSTANTS } from './lock-constants.js';
|
|
4
|
+
export type { LockLoss, RefreshFileLock } from './refresh-file-lock.js';
|
|
4
5
|
export { acquireRefreshFileLock, isLostMarkerRaceError, } from './refresh-file-lock.js';
|
|
5
|
-
export type { LockOptions } from './with-lock.js';
|
|
6
|
+
export type { LockOptions, LockOwnershipDetails } from './with-lock.js';
|
|
6
7
|
export { LockContentionError, LockOwnershipError, lockPathFor, withLock, } from './with-lock.js';
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
export declare function isLostMarkerRaceError(error: unknown): boolean;
|
|
2
|
+
export interface LockLoss {
|
|
3
|
+
readonly reason: 'taken-over' | 'expired' | 'unreadable' | 'renewal-failed' | 'marker-lost';
|
|
4
|
+
readonly expectedOwnerId: string;
|
|
5
|
+
readonly observedOwnerId?: string;
|
|
6
|
+
readonly observedExpiresAt?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface RefreshFileLock {
|
|
9
|
+
readonly ownerId: string;
|
|
10
|
+
assertOwned(): Promise<void>;
|
|
11
|
+
release(): Promise<void>;
|
|
12
|
+
/** Resolves once on detected loss; remains pending after an owner's release. */
|
|
13
|
+
whenLost(): Promise<LockLoss>;
|
|
14
|
+
hasLost(): boolean;
|
|
15
|
+
}
|
|
2
16
|
export declare function acquireRefreshFileLock(options: {
|
|
3
17
|
name: string;
|
|
4
18
|
ttlMs: number;
|
|
@@ -12,7 +26,4 @@ export declare function acquireRefreshFileLock(options: {
|
|
|
12
26
|
renew?: boolean;
|
|
13
27
|
renewIntervalMs?: number;
|
|
14
28
|
onStep?: (step: 'stale-marker-stat' | 'stale-marker-claimed' | 'stale-lock-confirmed' | 'eviction-marker-acquired' | 'renewal-owner-confirmed' | 'renewal-marker-unavailable' | 'renewal-write-fenced' | 'renewal-write-ready' | 'relinquish-read' | 'renewal-finished' | 'release-owner-confirmed') => void | Promise<void>;
|
|
15
|
-
}): Promise<
|
|
16
|
-
release: () => Promise<void>;
|
|
17
|
-
assertOwned: () => Promise<void>;
|
|
18
|
-
} | null>;
|
|
29
|
+
}): Promise<RefreshFileLock | null>;
|
|
@@ -19,6 +19,30 @@ export async function acquireRefreshFileLock(options) {
|
|
|
19
19
|
const now = options.now ?? Date.now;
|
|
20
20
|
let renewTimer = null;
|
|
21
21
|
let released = false;
|
|
22
|
+
let loss;
|
|
23
|
+
let resolveLoss;
|
|
24
|
+
const lostPromise = new Promise((resolve) => {
|
|
25
|
+
resolveLoss = resolve;
|
|
26
|
+
});
|
|
27
|
+
function recordLoss(reason, owner) {
|
|
28
|
+
if (loss || released)
|
|
29
|
+
return;
|
|
30
|
+
loss = Object.freeze({
|
|
31
|
+
reason,
|
|
32
|
+
expectedOwnerId: ownerId,
|
|
33
|
+
...(typeof owner?.ownerId === 'string'
|
|
34
|
+
? { observedOwnerId: owner.ownerId }
|
|
35
|
+
: {}),
|
|
36
|
+
...(typeof owner?.expiresAt === 'number'
|
|
37
|
+
? { observedExpiresAt: owner.expiresAt }
|
|
38
|
+
: {}),
|
|
39
|
+
});
|
|
40
|
+
if (renewTimer) {
|
|
41
|
+
clearRefreshLockRenewalTimeout(renewTimer);
|
|
42
|
+
renewTimer = null;
|
|
43
|
+
}
|
|
44
|
+
resolveLoss(loss);
|
|
45
|
+
}
|
|
22
46
|
let renewalInFlight = null;
|
|
23
47
|
// Only the owner of this exclusively-created marker may remove or renew a
|
|
24
48
|
// lock. A contender recovering a stale marker can accidentally rename a newer
|
|
@@ -203,7 +227,7 @@ export async function acquireRefreshFileLock(options) {
|
|
|
203
227
|
}
|
|
204
228
|
}
|
|
205
229
|
function scheduleRenewal() {
|
|
206
|
-
if (!options.renew || released)
|
|
230
|
+
if (!options.renew || released || loss)
|
|
207
231
|
return;
|
|
208
232
|
const intervalMs = options.renewIntervalMs ?? Math.max(1_000, Math.floor(options.ttlMs / 3));
|
|
209
233
|
renewTimer = setRefreshLockRenewalTimeout(() => {
|
|
@@ -213,19 +237,25 @@ export async function acquireRefreshFileLock(options) {
|
|
|
213
237
|
const markerAcquired = await withEvictionMarker(async () => {
|
|
214
238
|
const owner = await readOwner();
|
|
215
239
|
const currentNow = now();
|
|
216
|
-
if (released ||
|
|
240
|
+
if (released || loss) {
|
|
241
|
+
shouldReschedule = false;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (owner?.ownerId !== ownerId) {
|
|
245
|
+
recordLoss('taken-over', owner);
|
|
217
246
|
shouldReschedule = false;
|
|
218
247
|
return;
|
|
219
248
|
}
|
|
220
249
|
// An expired lease is no longer ours to extend; a contender may
|
|
221
250
|
// already be eligible to acquire it.
|
|
222
|
-
if (Number(owner?.expiresAt)
|
|
251
|
+
if (!(Number(owner?.expiresAt) > currentNow)) {
|
|
252
|
+
recordLoss('expired', owner);
|
|
223
253
|
shouldReschedule = false;
|
|
224
254
|
return;
|
|
225
255
|
}
|
|
226
256
|
if (options.onStep)
|
|
227
257
|
await options.onStep('renewal-owner-confirmed');
|
|
228
|
-
if (released) {
|
|
258
|
+
if (released || loss) {
|
|
229
259
|
shouldReschedule = false;
|
|
230
260
|
return;
|
|
231
261
|
}
|
|
@@ -233,7 +263,7 @@ export async function acquireRefreshFileLock(options) {
|
|
|
233
263
|
return;
|
|
234
264
|
if (options.onStep)
|
|
235
265
|
await options.onStep('renewal-write-fenced');
|
|
236
|
-
if (released) {
|
|
266
|
+
if (released || loss) {
|
|
237
267
|
shouldReschedule = false;
|
|
238
268
|
return;
|
|
239
269
|
}
|
|
@@ -241,10 +271,15 @@ export async function acquireRefreshFileLock(options) {
|
|
|
241
271
|
return;
|
|
242
272
|
if (options.onStep)
|
|
243
273
|
await options.onStep('renewal-write-ready');
|
|
274
|
+
if (released || loss) {
|
|
275
|
+
shouldReschedule = false;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
244
278
|
await writeOwner();
|
|
245
279
|
if (!(await ownsEvictionMarker())) {
|
|
246
280
|
// If marker ownership cannot be read, stop claiming the lease
|
|
247
281
|
// and remove only a record that still carries our owner id.
|
|
282
|
+
recordLoss('marker-lost');
|
|
248
283
|
shouldReschedule = false;
|
|
249
284
|
await relinquishLockAfterMarkerLoss();
|
|
250
285
|
return;
|
|
@@ -255,7 +290,17 @@ export async function acquireRefreshFileLock(options) {
|
|
|
255
290
|
}
|
|
256
291
|
}
|
|
257
292
|
catch {
|
|
258
|
-
//
|
|
293
|
+
// Retry transient failures only while the lease can still be verified.
|
|
294
|
+
try {
|
|
295
|
+
const owner = await readOwner();
|
|
296
|
+
if (owner?.ownerId !== ownerId)
|
|
297
|
+
recordLoss('taken-over', owner);
|
|
298
|
+
else if (!(Number(owner?.expiresAt) > now()))
|
|
299
|
+
recordLoss('expired', owner);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
recordLoss('renewal-failed');
|
|
303
|
+
}
|
|
259
304
|
}
|
|
260
305
|
finally {
|
|
261
306
|
if (options.onStep) {
|
|
@@ -266,7 +311,7 @@ export async function acquireRefreshFileLock(options) {
|
|
|
266
311
|
// Errors from the onStep observer must not reject the renewal.
|
|
267
312
|
}
|
|
268
313
|
}
|
|
269
|
-
if (shouldReschedule && !released)
|
|
314
|
+
if (shouldReschedule && !released && !loss)
|
|
270
315
|
scheduleRenewal();
|
|
271
316
|
}
|
|
272
317
|
})();
|
|
@@ -341,20 +386,35 @@ export async function acquireRefreshFileLock(options) {
|
|
|
341
386
|
return null;
|
|
342
387
|
scheduleRenewal();
|
|
343
388
|
return {
|
|
389
|
+
ownerId,
|
|
390
|
+
whenLost: () => lostPromise,
|
|
391
|
+
hasLost: () => loss !== undefined,
|
|
344
392
|
assertOwned: async () => {
|
|
393
|
+
let observed;
|
|
345
394
|
try {
|
|
346
395
|
const owner = await readOwner();
|
|
396
|
+
observed = owner;
|
|
347
397
|
if (!released &&
|
|
398
|
+
!loss &&
|
|
348
399
|
owner?.ownerId === ownerId &&
|
|
349
400
|
Number(owner?.expiresAt) > now())
|
|
350
401
|
return;
|
|
402
|
+
recordLoss(owner?.ownerId !== ownerId ? 'taken-over' : 'expired', owner);
|
|
351
403
|
}
|
|
352
404
|
catch {
|
|
353
405
|
// Unreadable ownership is not evidence of a valid lease.
|
|
406
|
+
recordLoss('unreadable');
|
|
354
407
|
}
|
|
355
408
|
throw new LockOwnershipError({
|
|
356
409
|
target: options.path,
|
|
357
410
|
name: options.name,
|
|
411
|
+
expectedOwnerId: ownerId,
|
|
412
|
+
...(typeof observed?.ownerId === 'string'
|
|
413
|
+
? { observedOwnerId: observed.ownerId }
|
|
414
|
+
: {}),
|
|
415
|
+
...(typeof observed?.expiresAt === 'number'
|
|
416
|
+
? { observedExpiresAt: observed.expiresAt }
|
|
417
|
+
: {}),
|
|
358
418
|
});
|
|
359
419
|
},
|
|
360
420
|
release: async () => {
|
package/dist/fs/with-lock.d.ts
CHANGED
|
@@ -11,15 +11,16 @@ export declare class LockContentionError extends Error {
|
|
|
11
11
|
timeoutMs: number;
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
|
+
export interface LockOwnershipDetails {
|
|
15
|
+
target: string;
|
|
16
|
+
name: string;
|
|
17
|
+
expectedOwnerId?: string;
|
|
18
|
+
observedOwnerId?: string;
|
|
19
|
+
observedExpiresAt?: number;
|
|
20
|
+
}
|
|
14
21
|
export declare class LockOwnershipError extends Error {
|
|
15
|
-
readonly details:
|
|
16
|
-
|
|
17
|
-
name: string;
|
|
18
|
-
};
|
|
19
|
-
constructor(details: {
|
|
20
|
-
target: string;
|
|
21
|
-
name: string;
|
|
22
|
-
});
|
|
22
|
+
readonly details: LockOwnershipDetails;
|
|
23
|
+
constructor(details: LockOwnershipDetails);
|
|
23
24
|
}
|
|
24
25
|
export interface LockOptions {
|
|
25
26
|
name: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type { OpenCode2AuthFailureKind } from './errors.js';
|
|
2
2
|
export { OpenCode2AuthError } from './errors.js';
|
|
3
|
-
export { applyHeaderEdits, DEFAULT_MAX_RECORDS, installOpenCode2Auth, } from './install.js';
|
|
3
|
+
export { ATTEMPT_HEADER, applyHeaderEdits, DEFAULT_MAX_RECORDS, installOpenCode2Auth, } from './install.js';
|
|
4
4
|
export type { FormAnswer, PoolAuthorization, PoolLoginMethod, RegisterOpenCode2AuthMethodsOptions, } from './integration.js';
|
|
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 { 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';
|
|
8
|
+
export type { AccountHeadersResult, AccountRequest, Attempt, AttemptEndReason, AttemptOutcome, ChooseAccountInput, EventVerdict, HeaderEdits, HostError, InstallOpenCode2AuthOptions, LimitSignal, OpenCode2AuthAdapter, OpenCode2AuthEventName, OpenCode2AuthEvents, OpenCode2AuthInstallation, OpenCode2AuthLogger, OpenCode2HookContext, RequestKind, RequestScope, ResponseAccount, RetryReason, SelectingHook, Transport, } from './types.js';
|
package/dist/opencode2/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { OpenCode2AuthError } from './errors.js';
|
|
2
|
-
export { applyHeaderEdits, DEFAULT_MAX_RECORDS, installOpenCode2Auth, } from './install.js';
|
|
2
|
+
export { ATTEMPT_HEADER, applyHeaderEdits, DEFAULT_MAX_RECORDS, installOpenCode2Auth, } from './install.js';
|
|
3
3
|
export { isPlaceholderCredential, PLACEHOLDER_LIFETIME_MS, PLACEHOLDER_METADATA_KEY, PLACEHOLDER_PREFIX, placeholderCredential, placeholderSecret, registerOpenCode2AuthMethods, } from './integration.js';
|
|
4
4
|
export { watchServerSentEvents } from './sse.js';
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import type { HeaderEdits, InstallOpenCode2AuthOptions, OpenCode2AuthAdapter, OpenCode2AuthInstallation, OpenCode2HookContext } from './types.js';
|
|
2
2
|
export declare const DEFAULT_MAX_RECORDS = 512;
|
|
3
|
+
/**
|
|
4
|
+
* The request header `model.request` sets to the attempt it started. The
|
|
5
|
+
* host builds the HTTP request and the WebSocket handshake from the headers
|
|
6
|
+
* `model.request` leaves, so `http.request` and `experimental.ws.handshake`
|
|
7
|
+
* read it to find their own attempt among several of one session and kind,
|
|
8
|
+
* and remove it: it never reaches the wire.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ATTEMPT_HEADER = "x-common-auth-attempt";
|
|
3
11
|
/** Applies edits to a plain header record, replacing every spelling of each name. */
|
|
4
12
|
export declare function applyHeaderEdits(target: Record<string, string>, edits: HeaderEdits): void;
|
|
5
13
|
/**
|
|
6
14
|
* Installs multi-account auth on OpenCode 2's own provider drivers. Every
|
|
7
15
|
* hook is scoped to `adapter.providerID`:
|
|
8
16
|
*
|
|
9
|
-
* - `model.request` picks the account for
|
|
10
|
-
* which starts a new attempt,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
17
|
+
* - `model.request` picks the account for one model call of a session and
|
|
18
|
+
* request kind, which starts a new attempt, sets its headers and marks the
|
|
19
|
+
* request with the attempt (`ATTEMPT_HEADER`);
|
|
20
|
+
* - `http.request` and `experimental.ws.handshake` find the attempt by that
|
|
21
|
+
* mark, remove it and set the account headers again, because the host
|
|
22
|
+
* applies its own credential after `model.request`;
|
|
13
23
|
* - `experimental.ws.send` (only when the adapter rewrites frames) rewrites
|
|
14
24
|
* each outgoing frame;
|
|
15
25
|
* - `http.response` and `experimental.ws.receive` read quota, refusals,
|
|
@@ -19,19 +29,19 @@ export declare function applyHeaderEdits(target: Record<string, string>, edits:
|
|
|
19
29
|
* before any output, so `model.request` runs again and can pick another
|
|
20
30
|
* account, and refuses to retry once output has started.
|
|
21
31
|
*
|
|
22
|
-
* Attribution.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* attempt
|
|
33
|
-
* exchange at a time
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* Attribution. A send belongs to the attempt named by the mark
|
|
33
|
+
* `model.request` left on it, so several attempts of one session and kind
|
|
34
|
+
* can be in flight at once, each on its own account. An HTTP response
|
|
35
|
+
* belongs to the attempt whose `http.request` produced its request object,
|
|
36
|
+
* and to nothing else. The host hands `http.response` the request object the
|
|
37
|
+
* `http.request` hooks left, so a different object means a later hook
|
|
38
|
+
* replaced it; nothing then proves which send the response answers, and its
|
|
39
|
+
* feedback is dropped rather than guessed by recency. A WebSocket frame
|
|
40
|
+
* names no attempt, so frames go to the newest open attempt of their session
|
|
41
|
+
* and kind that went out over WebSocket, and an older one is abandoned as
|
|
42
|
+
* soon as a newer attempt of that session and kind begins or goes out over
|
|
43
|
+
* WebSocket: the host runs one exchange at a time on a session's socket.
|
|
44
|
+
* Anything else is attributed to no attempt and reaches no adapter callback
|
|
45
|
+
* or listener.
|
|
36
46
|
*/
|
|
37
47
|
export declare function installOpenCode2Auth<Q, A = unknown>(ctx: OpenCode2HookContext, adapter: OpenCode2AuthAdapter<Q, A>, options?: InstallOpenCode2AuthOptions): Promise<OpenCode2AuthInstallation<Q, A>>;
|