@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.
@@ -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 || owner?.ownerId !== ownerId) {
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) <= currentNow) {
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
- // Transient marker and filesystem failures retry on the next interval.
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 () => {
@@ -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
- target: string;
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,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
@@ -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
@@ -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
@@ -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';
@@ -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';
@@ -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;
@@ -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) {
@@ -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
- replace(id: string, credential: PoolCredential, input?: {
85
- identity?: string;
86
- }, options?: RowOperationOptions): Promise<{
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?: RowToggleOptions): Promise<{
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?: RowToggleOptions): Promise<{
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.
@@ -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>;