@cortexkit/common-auth 0.5.0 → 0.7.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/store/errors.d.ts +2 -2
- package/dist/store/identity.d.ts +5 -0
- package/dist/store/identity.js +16 -0
- package/dist/store/index.d.ts +5 -3
- package/dist/store/index.js +2 -1
- package/dist/store/mutate.d.ts +3 -1
- package/dist/store/mutate.js +4 -0
- package/dist/store/pool.d.ts +31 -16
- package/dist/store/pool.js +3 -0
- package/dist/store/provider-state.d.ts +131 -0
- package/dist/store/provider-state.js +223 -0
- package/dist/store/refresh.d.ts +8 -0
- package/dist/store/refresh.js +9 -0
- package/dist/store/rows.d.ts +65 -15
- package/dist/store/rows.js +195 -59
- package/dist/store/schema.d.ts +114 -4
- package/dist/store/schema.js +85 -8
- package/dist/store/torn.d.ts +31 -1
- package/dist/store/torn.js +76 -2
- package/package.json +1 -1
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;
|
package/dist/store/errors.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StoredCredential } from './schema.js';
|
|
2
2
|
/** Every library operation that can fail, as named in the failure value. */
|
|
3
|
-
export type PoolOperation = 'initialize' | 'add' | 'replace' | 'rotate' | 'disable' | 'enable' | 'remove' | 'reorder' | 'updateSettings' | 'recordIdentity' | 'refresh' | 'pull';
|
|
3
|
+
export type PoolOperation = 'initialize' | 'add' | 'replace' | 'rotate' | 'disable' | 'enable' | 'remove' | 'reorder' | 'updateSettings' | 'recordIdentity' | 'updateProviderState' | 'refresh' | 'pull';
|
|
4
4
|
/**
|
|
5
5
|
* How far an operation got before it failed.
|
|
6
6
|
*
|
|
@@ -19,7 +19,7 @@ export type PoolFailurePhase = 'before-first-write' | 'after-first-write' | 'pul
|
|
|
19
19
|
* lock outcomes (a wait that ran out, and a lease found lost); the rest are
|
|
20
20
|
* refusals and failures of the operation itself.
|
|
21
21
|
*/
|
|
22
|
-
export type PoolFailureKind = 'lock-contention' | 'lock-ownership' | 'pending-migration' | 'load-error' | 'unknown-row' | 'invalid-row' | 'invalid-input' | 'id-exists' | 'id-removed' | 'type-mismatch' | 'no-credential' | 'row-disabled' | 'row-protected' | 'duplicate-identity' | 'identity-mismatch' | 'endpoint-mismatch' | 'row-key-changed' | 'invalid-order' | 'refresh-stamp-ahead' | 'unbound-credential' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'after-persist-hook' | 'unexpected';
|
|
22
|
+
export type PoolFailureKind = 'lock-contention' | 'lock-ownership' | 'pending-migration' | 'load-error' | 'unknown-row' | 'invalid-row' | 'invalid-input' | 'id-exists' | 'id-removed' | 'type-mismatch' | 'no-credential' | 'row-disabled' | 'row-protected' | 'duplicate-identity' | 'identity-mismatch' | 'endpoint-mismatch' | 'row-key-changed' | 'invalid-order' | 'refresh-stamp-ahead' | 'unbound-credential' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'invalid-provider-state' | 'after-persist-hook' | 'unexpected';
|
|
23
23
|
/**
|
|
24
24
|
* The single failure value of every store operation. `committed` is present
|
|
25
25
|
* only when the operation had already written a credential to the state file
|
package/dist/store/identity.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export declare function countUnknownIdentityRows(rows: readonly PoolRow[]): numb
|
|
|
22
22
|
* without an entry gets one at epoch 1. Nothing is ever deleted.
|
|
23
23
|
*/
|
|
24
24
|
export declare function disableIn(tx: RowEditor, id: string, reason: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Marks a row enabled: `enabled: true` in the roster row and no
|
|
27
|
+
* `disabledReason` in its entry. A row without an entry is not given one.
|
|
28
|
+
*/
|
|
29
|
+
export declare function enableIn(tx: RowEditor, id: string): void;
|
|
25
30
|
/**
|
|
26
31
|
* Two enabled OAuth rows with one wire identity are the same account: the
|
|
27
32
|
* earlier row in roster order stays enabled and every later one is disabled
|
package/dist/store/identity.js
CHANGED
|
@@ -24,6 +24,22 @@ export function disableIn(tx, id, reason) {
|
|
|
24
24
|
const entry = tx.entry(id) ?? { credentialEpoch: 1, needsFirstReading: true };
|
|
25
25
|
tx.setEntry(id, { ...entry, disabledReason: reason });
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Marks a row enabled: `enabled: true` in the roster row and no
|
|
29
|
+
* `disabledReason` in its entry. A row without an entry is not given one.
|
|
30
|
+
*/
|
|
31
|
+
export function enableIn(tx, id) {
|
|
32
|
+
const raw = tx.rosterRow(id);
|
|
33
|
+
if (!raw)
|
|
34
|
+
return;
|
|
35
|
+
raw.enabled = true;
|
|
36
|
+
const entry = tx.entry(id);
|
|
37
|
+
if (entry && 'disabledReason' in entry) {
|
|
38
|
+
const next = { ...entry };
|
|
39
|
+
delete next.disabledReason;
|
|
40
|
+
tx.setEntry(id, next);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
27
43
|
/**
|
|
28
44
|
* Two enabled OAuth rows with one wire identity are the same account: the
|
|
29
45
|
* earlier row in roster order stays enabled and every later one is disabled
|
package/dist/store/index.d.ts
CHANGED
|
@@ -6,13 +6,15 @@ export { countUnknownIdentityRows, DUPLICATE_IDENTITY_REASON, } from './identity
|
|
|
6
6
|
export type { HoldPoint, InitializeOutcome, WriteStep } from './mutate.js';
|
|
7
7
|
export type { OpenPoolStoreOptions, PoolLoad, PoolStore, } from './pool.js';
|
|
8
8
|
export { openPoolStore } from './pool.js';
|
|
9
|
+
export type { ProviderStateMutator, RowTransitionMutator, UpdateProviderStateResult, } from './provider-state.js';
|
|
10
|
+
export { DECLINE_TRANSITION } from './provider-state.js';
|
|
9
11
|
export type { PullHook, PullRequest } from './pull.js';
|
|
10
12
|
export type { ProviderRefresh, ProviderRefreshResult, RefreshOptions, RefreshOutcome, } from './refresh.js';
|
|
11
13
|
export type { LockEvent, PoolLockOptions, PoolLockSpec, } from './refresh-lock.js';
|
|
12
14
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
13
|
-
export type { AddInput, AddResult, FailureHook, RemoveOptions, RemoveResult, RemoveView, ReorderOptions, ReorderResult, RowOperationOptions, RowToggleOptions, } from './rows.js';
|
|
15
|
+
export type { AddInput, AddResult, CredentialWriteInput, FailureHook, RemoveOptions, RemoveResult, RemoveView, ReorderOptions, ReorderResult, RowOperationOptions, RowToggleOptions, RowTransitionOptions, RowTransitionResult, } from './rows.js';
|
|
14
16
|
export type { PullReason } from './runtime.js';
|
|
15
|
-
export type { ApiKeyCredential, CredentialStampStatus, OAuthCredential, PoolCredential, PoolRow, QuotaCodec, RotateCredential, StoredCredential, } from './schema.js';
|
|
16
|
-
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
|
17
|
+
export type { ApiKeyCredential, CredentialStampStatus, OAuthCredential, PoolCredential, PoolRow, ProviderStateCodec, ProviderStateDrop, ProviderStateReplacement, QuotaCodec, RotateCredential, StoredCredential, } from './schema.js';
|
|
18
|
+
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, PROVIDER_STATE_KEY, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
|
17
19
|
export type { PoolSettings, SettingsMutator, SettingsRead, UpdateSettingsOptions, UpdateSettingsResult, } from './settings.js';
|
|
18
20
|
export { POOL_OWNED_KEYS } from './settings.js';
|
package/dist/store/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { PoolOperationError, PoolReentryError } from './errors.js';
|
|
2
2
|
export { countUnknownIdentityRows, DUPLICATE_IDENTITY_REASON, } from './identity.js';
|
|
3
3
|
export { openPoolStore } from './pool.js';
|
|
4
|
+
export { DECLINE_TRANSITION } from './provider-state.js';
|
|
4
5
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
5
|
-
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
|
6
|
+
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, PROVIDER_STATE_KEY, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
|
6
7
|
export { POOL_OWNED_KEYS } from './settings.js';
|
package/dist/store/mutate.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type PoolFailurePhase, type PoolOperation, PoolOperationError } from './errors.js';
|
|
2
2
|
import { type PoolLogger } from './hooks.js';
|
|
3
3
|
import { type LockEnvironment, LockStack, type PoolLockOptions, type PoolLockSpec } from './refresh-lock.js';
|
|
4
|
-
import { type PoolRow, type QuotaCodec, type StoredCredential } from './schema.js';
|
|
4
|
+
import { type PoolRow, type ProviderStateCodec, type QuotaCodec, type StoredCredential } from './schema.js';
|
|
5
5
|
/** Named points on the write path, for crash and ownership injection. */
|
|
6
6
|
export type WriteStep = 'before-config-write' | 'after-config-write' | 'before-state-write' | 'after-state-write';
|
|
7
7
|
/** Awaitable pause points on the pull and refresh paths. */
|
|
@@ -11,6 +11,8 @@ export interface StoreContext {
|
|
|
11
11
|
configPath: string;
|
|
12
12
|
statePath: string;
|
|
13
13
|
codec: QuotaCodec;
|
|
14
|
+
/** The provider-state codec; without one no row shows a provider state. */
|
|
15
|
+
providerState?: ProviderStateCodec;
|
|
14
16
|
now: () => number;
|
|
15
17
|
storeLocks: readonly PoolLockSpec[];
|
|
16
18
|
lockDefaults: PoolLockOptions;
|
package/dist/store/mutate.js
CHANGED
|
@@ -41,6 +41,7 @@ export async function readPool(ctx) {
|
|
|
41
41
|
state: state.state,
|
|
42
42
|
rows: loadRows(config.config, state.state, ctx.codec, {
|
|
43
43
|
requireCredentialStamps: ctx.requireCredentialStamps === true,
|
|
44
|
+
...(ctx.providerState ? { providerState: ctx.providerState } : {}),
|
|
44
45
|
}),
|
|
45
46
|
};
|
|
46
47
|
}
|
|
@@ -90,6 +91,9 @@ export class Transaction {
|
|
|
90
91
|
rows() {
|
|
91
92
|
return loadRows(this.config, this.state, this.ctx.codec, {
|
|
92
93
|
requireCredentialStamps: this.ctx.requireCredentialStamps === true,
|
|
94
|
+
...(this.ctx.providerState
|
|
95
|
+
? { providerState: this.ctx.providerState }
|
|
96
|
+
: {}),
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
row(id) {
|
package/dist/store/pool.d.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { type Attribution } from './attribution.js';
|
|
|
2
2
|
import type { PoolOperationError } from './errors.js';
|
|
3
3
|
import type { PoolLogger } from './hooks.js';
|
|
4
4
|
import { type HoldPoint, type InitializeOutcome, type StoreContext } from './mutate.js';
|
|
5
|
+
import { type ProviderStateMutator, type UpdateProviderStateResult } from './provider-state.js';
|
|
5
6
|
import { type PullHook } from './pull.js';
|
|
6
7
|
import { type ProviderRefresh, type RefreshOptions, type RefreshOutcome } from './refresh.js';
|
|
7
8
|
import { type LockEnvironment, type PoolLockOptions, type PoolLockSpec } from './refresh-lock.js';
|
|
8
|
-
import { type AddInput, type AddResult, type RemoveOptions, type RemoveResult, type ReorderOptions, type ReorderResult, type RowOperationOptions, type RowToggleOptions } from './rows.js';
|
|
9
|
-
import { type PoolCredential, type PoolRow, type QuotaCodec, type RotateCredential, type StoredCredential } from './schema.js';
|
|
9
|
+
import { type AddInput, type AddResult, type CredentialWriteInput, type RemoveOptions, type RemoveResult, type ReorderOptions, type ReorderResult, type RowOperationOptions, type RowToggleOptions, type RowTransitionOptions, type RowTransitionResult } from './rows.js';
|
|
10
|
+
import { type PoolCredential, type PoolRow, type ProviderStateCodec, type QuotaCodec, type RotateCredential, type StoredCredential } from './schema.js';
|
|
10
11
|
import { type SettingsMutator, type SettingsRead, type UpdateSettingsOptions, type UpdateSettingsResult } from './settings.js';
|
|
11
12
|
export interface OpenPoolStoreOptions {
|
|
12
13
|
/** The provider every row of this pool belongs to; keys the provider-wide lock. */
|
|
@@ -14,6 +15,13 @@ export interface OpenPoolStoreOptions {
|
|
|
14
15
|
configPath: string;
|
|
15
16
|
statePath: string;
|
|
16
17
|
quota: QuotaCodec;
|
|
18
|
+
/**
|
|
19
|
+
* The codec of the provider state kept beside each row's credential (see
|
|
20
|
+
* `ProviderStateCodec`). Without it no row shows a provider state, and
|
|
21
|
+
* every write that would set one refuses (`invalid-input`); writes that
|
|
22
|
+
* leave it alone keep the value on disk as it is, and `replace` clears it.
|
|
23
|
+
*/
|
|
24
|
+
providerState?: ProviderStateCodec;
|
|
17
25
|
/**
|
|
18
26
|
* Refuse every credential this store did not stamp (default false, which
|
|
19
27
|
* loads unstamped and mis-stamped credentials as older writers left them).
|
|
@@ -81,9 +89,12 @@ export interface PoolStore {
|
|
|
81
89
|
status: InitializeOutcome;
|
|
82
90
|
}>;
|
|
83
91
|
add(input: AddInput, options?: RowOperationOptions): Promise<AddResult>;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Gives a row a new credential and a new credential epoch. Since 0.6.0 the
|
|
94
|
+
* row's provider state is whatever `ProviderStateCodec.onReplace` returns;
|
|
95
|
+
* without that hook it is `input.providerState`, else cleared.
|
|
96
|
+
*/
|
|
97
|
+
replace(id: string, credential: PoolCredential, input?: CredentialWriteInput, options?: RowOperationOptions): Promise<{
|
|
87
98
|
id: string;
|
|
88
99
|
credential: StoredCredential;
|
|
89
100
|
credentialEpoch: number;
|
|
@@ -94,28 +105,32 @@ export interface PoolStore {
|
|
|
94
105
|
* to keep the row's, and one that gives another is refused
|
|
95
106
|
* (`endpoint-mismatch`) before writing: that is a `replace`.
|
|
96
107
|
*/
|
|
97
|
-
rotate(id: string, credential: RotateCredential, input?: {
|
|
98
|
-
identity?: string;
|
|
99
|
-
}, options?: RowOperationOptions): Promise<{
|
|
108
|
+
rotate(id: string, credential: RotateCredential, input?: CredentialWriteInput, options?: RowOperationOptions): Promise<{
|
|
100
109
|
id: string;
|
|
101
110
|
credential: StoredCredential;
|
|
102
111
|
}>;
|
|
112
|
+
/**
|
|
113
|
+
* Changes a row's provider state without touching its credential (since
|
|
114
|
+
* 0.6.0), under the row lock, `extraLocks` and the store locks. Refuses
|
|
115
|
+
* (`attribution`) once the row has moved off the credential epoch or
|
|
116
|
+
* identity in `fence`, and (`unknown-row`) once it is removed.
|
|
117
|
+
*/
|
|
118
|
+
updateProviderState(id: string, fence: Attribution, mutator: ProviderStateMutator, options?: RowToggleOptions): Promise<UpdateProviderStateResult>;
|
|
103
119
|
/**
|
|
104
120
|
* Sets `enabled: false` and the entry's `disabledReason`. Takes the row
|
|
105
121
|
* lock, then `extraLocks`, then the store locks (the row lock and
|
|
106
|
-
* `extraLocks` since 0.2.3).
|
|
122
|
+
* `extraLocks` since 0.2.3). Since 0.7.0 it may be fenced on the
|
|
123
|
+
* credential the caller's evidence is about (`attribution`) and carry a
|
|
124
|
+
* provider-state change that lands with it (`providerState`); see
|
|
125
|
+
* `RowTransitionOptions`.
|
|
107
126
|
*/
|
|
108
|
-
disable(id: string, reason: string, options?:
|
|
109
|
-
id: string;
|
|
110
|
-
}>;
|
|
127
|
+
disable(id: string, reason: string, options?: RowTransitionOptions): Promise<RowTransitionResult>;
|
|
111
128
|
/**
|
|
112
129
|
* Clears `enabled: false` and `disabledReason` (since 0.2.3); refuses with
|
|
113
130
|
* `duplicate-identity` when another enabled OAuth row holds the row's
|
|
114
|
-
* identity. Locks as `disable
|
|
131
|
+
* identity. Locks as `disable`, and takes the same options since 0.7.0.
|
|
115
132
|
*/
|
|
116
|
-
enable(id: string, options?:
|
|
117
|
-
id: string;
|
|
118
|
-
}>;
|
|
133
|
+
enable(id: string, options?: RowTransitionOptions): Promise<RowTransitionResult>;
|
|
119
134
|
/**
|
|
120
135
|
* Deletes the roster row, its per-row entry and its state-file credential
|
|
121
136
|
* (since 0.2.3). Locks as `disable`; `protect` can refuse the id.
|
package/dist/store/pool.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { recordQuota } from './attribution.js';
|
|
2
2
|
import { initializePool, readPool, } from './mutate.js';
|
|
3
|
+
import { updateProviderStateRow, } from './provider-state.js';
|
|
3
4
|
import { PullScheduler } from './pull.js';
|
|
4
5
|
import { refreshRow, } from './refresh.js';
|
|
5
6
|
import { POOL_LOCK_DEFAULTS, } from './refresh-lock.js';
|
|
@@ -47,6 +48,7 @@ export function openPoolStore(options) {
|
|
|
47
48
|
configPath: options.configPath,
|
|
48
49
|
statePath: options.statePath,
|
|
49
50
|
codec: options.quota,
|
|
51
|
+
...(options.providerState ? { providerState: options.providerState } : {}),
|
|
50
52
|
now: options.now ?? Date.now,
|
|
51
53
|
storeLocks: options.storeLocks ?? [
|
|
52
54
|
{ name: 'save', path: options.configPath },
|
|
@@ -98,6 +100,7 @@ export function openPoolStore(options) {
|
|
|
98
100
|
add: (input, callOptions) => addRow(rt, input, callOptions),
|
|
99
101
|
replace: (id, credential, input, callOptions) => replaceRow(rt, id, credential, input, callOptions),
|
|
100
102
|
rotate: (id, credential, input, callOptions) => rotateRow(rt, id, credential, input, callOptions),
|
|
103
|
+
updateProviderState: (id, fence, mutator, callOptions) => updateProviderStateRow(rt, id, fence, mutator, callOptions),
|
|
101
104
|
disable: (id, reason, callOptions) => disableRow(rt, id, reason, callOptions),
|
|
102
105
|
enable: (id, callOptions) => enableRow(rt, id, callOptions),
|
|
103
106
|
remove: (id, callOptions) => removeRow(rt, id, callOptions),
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Attribution } from './attribution.js';
|
|
2
|
+
import type { PoolOperation } from './errors.js';
|
|
3
|
+
import { type Transaction } from './mutate.js';
|
|
4
|
+
import type { RowToggleOptions } from './rows.js';
|
|
5
|
+
import { type StoreRuntime } from './runtime.js';
|
|
6
|
+
import { type PoolRow, type ProviderStateCodec } from './schema.js';
|
|
7
|
+
/**
|
|
8
|
+
* What a credential write does to the provider state beside it: `keep`
|
|
9
|
+
* leaves the value on disk as it is (bound by the new stamp only if the old
|
|
10
|
+
* stamp bound it to the same row, epoch and identity), `set` stores a value
|
|
11
|
+
* the codec accepted, and `clear` deletes it.
|
|
12
|
+
*/
|
|
13
|
+
export type ProviderStateWrite = {
|
|
14
|
+
kind: 'keep';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'set';
|
|
17
|
+
value: unknown;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'clear';
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A provider state as the store will store it: a JSON round trip of it (so
|
|
23
|
+
* its digest is the same once read back from disk), accepted by the codec.
|
|
24
|
+
* Refused before anything is written when the store has no codec, the value
|
|
25
|
+
* is not JSON, or the codec rejects it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function acceptProviderState(codec: ProviderStateCodec | undefined, operation: PoolOperation, id: string, value: unknown, what?: string): unknown;
|
|
28
|
+
/**
|
|
29
|
+
* The write for a value a credential write brings (`add` of a secret the
|
|
30
|
+
* pool holds, `rotate`, a refresh): merged with the row's value on disk by
|
|
31
|
+
* the codec's `merge` when both exist, else the incoming value as is.
|
|
32
|
+
*/
|
|
33
|
+
export declare function mergedProviderState(codec: ProviderStateCodec | undefined, operation: PoolOperation, id: string, onDisk: unknown, incoming: unknown): ProviderStateWrite;
|
|
34
|
+
/**
|
|
35
|
+
* The provider state a replace leaves on the row, decided before anything is
|
|
36
|
+
* written: whatever the codec's `onReplace` returns (undefined clears it), or
|
|
37
|
+
* without that hook the value handed to `replace`, else nothing. The old
|
|
38
|
+
* credential's value is never kept by default: it describes the account the
|
|
39
|
+
* replaced credential belonged to.
|
|
40
|
+
*/
|
|
41
|
+
export declare function replacementProviderState(codec: ProviderStateCodec | undefined, row: PoolRow, credentialEpoch: number, identity: string | undefined, incoming: unknown): ProviderStateWrite;
|
|
42
|
+
/**
|
|
43
|
+
* The provider-state digest the stamp of a credential write carries, and the
|
|
44
|
+
* state-file fields it changes. `set` binds the new value; `keep` carries the
|
|
45
|
+
* old stamp's digest forward only when that stamp bound it to this row as it
|
|
46
|
+
* stood before the write (same credential lineage, the epoch being written,
|
|
47
|
+
* the identity recorded before the write); `clear` binds nothing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function providerStateCoverage(codec: ProviderStateCodec | undefined, write: ProviderStateWrite, prior: Record<string, unknown> | undefined, priorRow: PoolRow | undefined, credentialEpoch: number): string | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Receives a private copy of the row's provider state (undefined when the row
|
|
52
|
+
* shows none) and the row as loaded under the locks, and returns the next
|
|
53
|
+
* provider state; returning undefined clears it, so a mutator that means to
|
|
54
|
+
* keep the value returns it. It runs under the row lock and the store locks,
|
|
55
|
+
* so it must not call back into the store (`PoolReentryError`).
|
|
56
|
+
*/
|
|
57
|
+
export type ProviderStateMutator = (current: unknown | undefined, row: PoolRow) => unknown | Promise<unknown>;
|
|
58
|
+
export type UpdateProviderStateResult = {
|
|
59
|
+
id: string;
|
|
60
|
+
/** The provider state now on disk; absent when the row has none. */
|
|
61
|
+
providerState?: unknown;
|
|
62
|
+
/**
|
|
63
|
+
* `unchanged`: the mutator returned what the row already shows (or cleared
|
|
64
|
+
* a row that holds none); nothing was written.
|
|
65
|
+
*/
|
|
66
|
+
outcome: 'updated' | 'cleared' | 'unchanged';
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Changes a row's provider state without touching its credential, in one
|
|
70
|
+
* state-file write under the row lock, the caller's extra locks and the
|
|
71
|
+
* store locks. `fence` is what the caller read the row at: the write is
|
|
72
|
+
* refused (`attribution`, retryable) when the row has since moved to another
|
|
73
|
+
* credential epoch or recorded identity, and (`unknown-row`) once it is
|
|
74
|
+
* removed, so a writer that read the row before a replace or a removal never
|
|
75
|
+
* lands its value on the new credential or brings a removed row's state back.
|
|
76
|
+
*
|
|
77
|
+
* The value is bound by the stamp already beside the credential. When its
|
|
78
|
+
* credential-bound part is unchanged the stamp is left byte for byte as it
|
|
79
|
+
* is; otherwise only the stamp's provider-state digest changes, so the
|
|
80
|
+
* credential's stamp status never moves. With `requireCredentialStamps`, an
|
|
81
|
+
* unbound row refuses (`unbound-credential`) as every other strict path
|
|
82
|
+
* does. Without it, a row whose stamp was not written by this store with this
|
|
83
|
+
* credential at the row's epoch and identity refuses the same way: no stamp
|
|
84
|
+
* could bind the value, so no reader would ever show it. A `rotate` or
|
|
85
|
+
* `replace` stamps such a row.
|
|
86
|
+
*/
|
|
87
|
+
export declare function updateProviderStateRow(rt: StoreRuntime, id: string, fence: Attribution, mutator: ProviderStateMutator, options?: RowToggleOptions): Promise<UpdateProviderStateResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Returned by the provider-state mutator of an attributed `disable` or
|
|
90
|
+
* `enable` to decline the whole transition: nothing is written, neither the
|
|
91
|
+
* provider state nor the row's enabled flag, and the call resolves with
|
|
92
|
+
* `declined: true`. A mutator declines when the state it is shown is newer
|
|
93
|
+
* than what its caller saw, such as an eligibility recorded after the
|
|
94
|
+
* request whose refusal is being acted on. It is a value of its own because
|
|
95
|
+
* `undefined` already means "clear the provider state". `Symbol.for` keeps it
|
|
96
|
+
* equal across two copies of this module loaded in one process.
|
|
97
|
+
*/
|
|
98
|
+
export declare const DECLINE_TRANSITION: unique symbol;
|
|
99
|
+
/**
|
|
100
|
+
* The provider-state mutator of an attributed `disable` or `enable`: as
|
|
101
|
+
* `ProviderStateMutator`, and it may also return `DECLINE_TRANSITION`.
|
|
102
|
+
*/
|
|
103
|
+
export type RowTransitionMutator = (current: unknown | undefined, row: PoolRow) => unknown | typeof DECLINE_TRANSITION | Promise<unknown | typeof DECLINE_TRANSITION>;
|
|
104
|
+
/**
|
|
105
|
+
* What a provider-state mutator asks of a row, worked out under the locks
|
|
106
|
+
* before anything is written. `changed` carries the row's whole next
|
|
107
|
+
* state-file account entry (the value, and the stamp rebound to it when its
|
|
108
|
+
* credential-bound part moved); `value` is the next value, absent when it is
|
|
109
|
+
* cleared.
|
|
110
|
+
*/
|
|
111
|
+
export type ProviderStatePlan = {
|
|
112
|
+
kind: 'declined';
|
|
113
|
+
} | {
|
|
114
|
+
kind: 'unchanged';
|
|
115
|
+
value?: unknown;
|
|
116
|
+
} | {
|
|
117
|
+
kind: 'changed';
|
|
118
|
+
value?: unknown;
|
|
119
|
+
account: Record<string, unknown>;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Runs a provider-state mutator for a row loaded under every lock and
|
|
123
|
+
* already checked by the caller (present, valid, inside its attribution
|
|
124
|
+
* fence), and plans the write. Refuses (`no-credential`) a row holding no
|
|
125
|
+
* credential, and (`unbound-credential`) one whose credential carries no
|
|
126
|
+
* stamp of this store at the row's epoch and identity: no stamp could bind
|
|
127
|
+
* the value, so no reader would ever show it. `DECLINE_TRANSITION` is
|
|
128
|
+
* honoured only when `declinable` is set; elsewhere it is not JSON and is
|
|
129
|
+
* refused as such.
|
|
130
|
+
*/
|
|
131
|
+
export declare function planProviderStateIn(tx: Transaction, codec: ProviderStateCodec, operation: PoolOperation, row: PoolRow, mutator: ProviderStateMutator | RowTransitionMutator, declinable?: boolean): Promise<ProviderStatePlan>;
|