@cortexkit/common-auth 0.1.2 → 0.2.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.
Files changed (49) hide show
  1. package/dist/quota/codec.d.ts +11 -0
  2. package/dist/quota/codec.js +12 -0
  3. package/dist/quota/index.d.ts +8 -0
  4. package/dist/quota/index.js +4 -0
  5. package/dist/quota/map.d.ts +63 -0
  6. package/dist/quota/map.js +105 -0
  7. package/dist/quota/merge.d.ts +48 -0
  8. package/dist/quota/merge.js +162 -0
  9. package/dist/quota/projection.d.ts +57 -0
  10. package/dist/quota/projection.js +127 -0
  11. package/dist/routing/admission.d.ts +93 -0
  12. package/dist/routing/admission.js +140 -0
  13. package/dist/routing/index.d.ts +8 -0
  14. package/dist/routing/index.js +4 -0
  15. package/dist/routing/ordered.d.ts +47 -0
  16. package/dist/routing/ordered.js +58 -0
  17. package/dist/routing/pins.d.ts +22 -0
  18. package/dist/routing/pins.js +34 -0
  19. package/dist/routing/sticky.d.ts +118 -0
  20. package/dist/routing/sticky.js +310 -0
  21. package/dist/sidebar-file/sidebar-file.d.ts +4 -3
  22. package/dist/sidebar-file/sidebar-file.js +1 -4
  23. package/dist/store/attribution.d.ts +17 -0
  24. package/dist/store/attribution.js +46 -0
  25. package/dist/store/errors.d.ts +52 -0
  26. package/dist/store/errors.js +38 -0
  27. package/dist/store/hooks.d.ts +12 -0
  28. package/dist/store/hooks.js +34 -0
  29. package/dist/store/identity.d.ts +23 -0
  30. package/dist/store/identity.js +53 -0
  31. package/dist/store/index.d.ts +16 -0
  32. package/dist/store/index.js +5 -0
  33. package/dist/store/mutate.d.ts +111 -0
  34. package/dist/store/mutate.js +294 -0
  35. package/dist/store/pool.d.ts +86 -0
  36. package/dist/store/pool.js +98 -0
  37. package/dist/store/pull.d.ts +36 -0
  38. package/dist/store/pull.js +129 -0
  39. package/dist/store/refresh-lock.d.ts +63 -0
  40. package/dist/store/refresh-lock.js +125 -0
  41. package/dist/store/refresh.d.ts +48 -0
  42. package/dist/store/refresh.js +169 -0
  43. package/dist/store/rows.d.ts +53 -0
  44. package/dist/store/rows.js +250 -0
  45. package/dist/store/runtime.d.ts +28 -0
  46. package/dist/store/runtime.js +45 -0
  47. package/dist/store/schema.d.ts +133 -0
  48. package/dist/store/schema.js +323 -0
  49. package/package.json +13 -1
@@ -0,0 +1,129 @@
1
+ import { recordQuota } from './attribution.js';
2
+ import { PoolOperationError } from './errors.js';
3
+ import { toFailure, withTransaction } from './mutate.js';
4
+ import { LockStack } from './refresh-lock.js';
5
+ /**
6
+ * Fires quota pulls without ever making a caller wait for one. A pull first
7
+ * gives a row without a per-row entry its entry at epoch 1 (its own locked
8
+ * config write), then captures the credential and the attribution tuple in
9
+ * one locked read, requests the observation, and records it only if
10
+ * attribution still holds. Failures go to the store's pull failure hook.
11
+ */
12
+ export class PullScheduler {
13
+ rt;
14
+ hook;
15
+ onFailure;
16
+ firedAtLoad;
17
+ logger;
18
+ inflight = new Set();
19
+ constructor(rt, hook, onFailure,
20
+ /** Rows a load-time pull has fired for in this process. */
21
+ firedAtLoad, logger) {
22
+ this.rt = rt;
23
+ this.hook = hook;
24
+ this.onFailure = onFailure;
25
+ this.firedAtLoad = firedAtLoad;
26
+ this.logger = logger;
27
+ }
28
+ fire(id, reason) {
29
+ if (!this.hook)
30
+ return;
31
+ if (reason === 'load') {
32
+ if (this.firedAtLoad.has(id))
33
+ return;
34
+ this.firedAtLoad.add(id);
35
+ }
36
+ const run = this.run(id, reason);
37
+ this.inflight.add(run);
38
+ void run.finally(() => this.inflight.delete(run));
39
+ }
40
+ /** Resolves once every pull fired so far has settled. */
41
+ async settled() {
42
+ while (this.inflight.size > 0)
43
+ await Promise.allSettled([...this.inflight]);
44
+ }
45
+ async run(id, reason) {
46
+ const rt = this.rt();
47
+ const { ctx } = rt;
48
+ const locks = new LockStack(ctx.lockDefaults, ctx.lockEnv);
49
+ const progress = { writes: 0 };
50
+ try {
51
+ const request = await withTransaction(ctx, locks, progress, { operation: 'pull', rowId: id }, async (tx) => {
52
+ const row = tx.row(id);
53
+ // Disabled, API-key and credential-less rows never pull.
54
+ if (!row?.candidate || row.type !== 'oauth')
55
+ return undefined;
56
+ if (!row.hasEntry) {
57
+ tx.setEntry(id, { credentialEpoch: 1, needsFirstReading: true });
58
+ await tx.commitConfig();
59
+ }
60
+ const current = tx.row(id);
61
+ if (!current?.credential || current.credentialEpoch === undefined)
62
+ return undefined;
63
+ return {
64
+ id,
65
+ credential: current.credential,
66
+ credentialEpoch: current.credentialEpoch,
67
+ ...(current.identity !== undefined
68
+ ? { identity: current.identity }
69
+ : {}),
70
+ reason,
71
+ };
72
+ });
73
+ await locks.releaseAll();
74
+ if (!request) {
75
+ this.firedAtLoad.delete(id);
76
+ return;
77
+ }
78
+ await ctx.hold?.('pull-before-request', id);
79
+ let observation;
80
+ try {
81
+ observation = await this.hook?.(request);
82
+ }
83
+ catch (cause) {
84
+ throw new PoolOperationError({
85
+ operation: 'pull',
86
+ rowId: id,
87
+ phase: 'pull',
88
+ retryable: true,
89
+ kind: 'pull',
90
+ message: 'the quota pull rejected',
91
+ cause,
92
+ });
93
+ }
94
+ await recordQuota(rt, id, {
95
+ credentialEpoch: request.credentialEpoch,
96
+ ...(request.identity !== undefined
97
+ ? { identity: request.identity }
98
+ : {}),
99
+ }, observation);
100
+ }
101
+ catch (error) {
102
+ // Needs-first-reading stays set; a later load may fire again.
103
+ this.firedAtLoad.delete(id);
104
+ const failure = toFailure(error, 'pull', id, progress);
105
+ if (this.onFailure) {
106
+ try {
107
+ await this.onFailure(id, failure);
108
+ }
109
+ catch (hookError) {
110
+ this.logger?.warn('pull failure hook threw', {
111
+ rowId: id,
112
+ error: hookError instanceof Error
113
+ ? hookError.message
114
+ : String(hookError),
115
+ });
116
+ }
117
+ }
118
+ else {
119
+ this.logger?.warn('quota pull failed', {
120
+ rowId: id,
121
+ kind: failure.kind,
122
+ });
123
+ }
124
+ }
125
+ finally {
126
+ await locks.releaseAll();
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,63 @@
1
+ import { acquireRefreshFileLock } from '../fs/refresh-file-lock.js';
2
+ type LockStep = NonNullable<Parameters<typeof acquireRefreshFileLock>[0]['onStep']> extends (step: infer S) => unknown ? S : never;
3
+ /** Tuning shared by every lock the store takes; each field is overridable. */
4
+ export interface PoolLockOptions {
5
+ ttlMs: number;
6
+ timeoutMs: number;
7
+ retryMs: number;
8
+ renew: boolean;
9
+ renewIntervalMs?: number;
10
+ }
11
+ /** A lock: the (name, path) pair naming its file plus optional tuning. */
12
+ export interface PoolLockSpec extends Partial<PoolLockOptions> {
13
+ name: string;
14
+ path: string;
15
+ }
16
+ /**
17
+ * Store lock defaults: a 15 000 ms bounded wait retried every 50 ms plus
18
+ * jitter, and a renewed 10 000 ms lease.
19
+ */
20
+ export declare const POOL_LOCK_DEFAULTS: Readonly<PoolLockOptions>;
21
+ export type LockEvent = {
22
+ type: 'acquired' | 'released';
23
+ name: string;
24
+ path: string;
25
+ };
26
+ export interface LockEnvironment {
27
+ now: () => number;
28
+ onLockEvent?: (event: LockEvent) => void;
29
+ onLockStep?: (lock: {
30
+ name: string;
31
+ path: string;
32
+ }, step: LockStep) => void | Promise<void>;
33
+ }
34
+ export interface HeldLock {
35
+ readonly name: string;
36
+ readonly path: string;
37
+ assertOwned(): Promise<void>;
38
+ release(): Promise<void>;
39
+ }
40
+ /**
41
+ * Takes one lock, retrying while a live holder has it until `timeoutMs` has
42
+ * passed on the real clock, then throws `LockContentionError`. The lease is
43
+ * asserted as owned once acquired, so a caller never proceeds on a lease that
44
+ * expired during the wait.
45
+ */
46
+ export declare function acquirePoolLock(spec: PoolLockSpec, defaults: PoolLockOptions, env: LockEnvironment): Promise<HeldLock>;
47
+ /**
48
+ * Locks taken in order and released in reverse. Every acquisition re-asserts
49
+ * the leases already held, because a wait is exactly when an earlier lease
50
+ * can expire unnoticed.
51
+ */
52
+ export declare class LockStack {
53
+ private readonly defaults;
54
+ private readonly env;
55
+ readonly held: HeldLock[];
56
+ constructor(defaults: PoolLockOptions, env: LockEnvironment);
57
+ acquire(spec: PoolLockSpec): Promise<HeldLock>;
58
+ assertAll(): Promise<void>;
59
+ /** Releases every lock taken after `mark` (a length of `held`), newest first. */
60
+ releaseTo(mark: number): Promise<void>;
61
+ releaseAll(): Promise<void>;
62
+ }
63
+ export {};
@@ -0,0 +1,125 @@
1
+ import { acquireRefreshFileLock } from '../fs/refresh-file-lock.js';
2
+ import { LockContentionError } from '../fs/with-lock.js';
3
+ /**
4
+ * Store lock defaults: a 15 000 ms bounded wait retried every 50 ms plus
5
+ * jitter, and a renewed 10 000 ms lease.
6
+ */
7
+ export const POOL_LOCK_DEFAULTS = Object.freeze({
8
+ ttlMs: 10_000,
9
+ timeoutMs: 15_000,
10
+ retryMs: 50,
11
+ renew: true,
12
+ });
13
+ /**
14
+ * Takes one lock, retrying while a live holder has it until `timeoutMs` has
15
+ * passed on the real clock, then throws `LockContentionError`. The lease is
16
+ * asserted as owned once acquired, so a caller never proceeds on a lease that
17
+ * expired during the wait.
18
+ */
19
+ export async function acquirePoolLock(spec, defaults, env) {
20
+ const options = { ...defaults, ...definedOnly(spec) };
21
+ const started = performance.now();
22
+ for (;;) {
23
+ const lock = await acquireRefreshFileLock({
24
+ name: spec.name,
25
+ path: spec.path,
26
+ ttlMs: options.ttlMs,
27
+ now: env.now,
28
+ renew: options.renew,
29
+ ...(options.renewIntervalMs !== undefined
30
+ ? { renewIntervalMs: options.renewIntervalMs }
31
+ : {}),
32
+ ...(env.onLockStep
33
+ ? {
34
+ onStep: (step) => env.onLockStep?.({ name: spec.name, path: spec.path }, step),
35
+ }
36
+ : {}),
37
+ });
38
+ if (lock) {
39
+ env.onLockEvent?.({
40
+ type: 'acquired',
41
+ name: spec.name,
42
+ path: spec.path,
43
+ });
44
+ const held = {
45
+ name: spec.name,
46
+ path: spec.path,
47
+ assertOwned: () => lock.assertOwned(),
48
+ release: async () => {
49
+ await lock.release();
50
+ env.onLockEvent?.({
51
+ type: 'released',
52
+ name: spec.name,
53
+ path: spec.path,
54
+ });
55
+ },
56
+ };
57
+ try {
58
+ await held.assertOwned();
59
+ }
60
+ catch (error) {
61
+ await held.release();
62
+ throw error;
63
+ }
64
+ return held;
65
+ }
66
+ const remaining = options.timeoutMs - (performance.now() - started);
67
+ if (remaining <= 0) {
68
+ throw new LockContentionError({
69
+ target: spec.path,
70
+ name: spec.name,
71
+ timeoutMs: options.timeoutMs,
72
+ });
73
+ }
74
+ const jitter = Math.floor(Math.random() * (options.retryMs + 1));
75
+ await new Promise((resolve) => setTimeout(resolve, Math.min(options.retryMs + jitter, remaining)));
76
+ }
77
+ }
78
+ function definedOnly(spec) {
79
+ const out = {};
80
+ if (spec.ttlMs !== undefined)
81
+ out.ttlMs = spec.ttlMs;
82
+ if (spec.timeoutMs !== undefined)
83
+ out.timeoutMs = spec.timeoutMs;
84
+ if (spec.retryMs !== undefined)
85
+ out.retryMs = spec.retryMs;
86
+ if (spec.renew !== undefined)
87
+ out.renew = spec.renew;
88
+ if (spec.renewIntervalMs !== undefined)
89
+ out.renewIntervalMs = spec.renewIntervalMs;
90
+ return out;
91
+ }
92
+ /**
93
+ * Locks taken in order and released in reverse. Every acquisition re-asserts
94
+ * the leases already held, because a wait is exactly when an earlier lease
95
+ * can expire unnoticed.
96
+ */
97
+ export class LockStack {
98
+ defaults;
99
+ env;
100
+ held = [];
101
+ constructor(defaults, env) {
102
+ this.defaults = defaults;
103
+ this.env = env;
104
+ }
105
+ async acquire(spec) {
106
+ const lock = await acquirePoolLock(spec, this.defaults, this.env);
107
+ this.held.push(lock);
108
+ await this.assertAll();
109
+ return lock;
110
+ }
111
+ async assertAll() {
112
+ for (const lock of this.held)
113
+ await lock.assertOwned();
114
+ }
115
+ /** Releases every lock taken after `mark` (a length of `held`), newest first. */
116
+ async releaseTo(mark) {
117
+ while (this.held.length > mark) {
118
+ const lock = this.held.pop();
119
+ await lock?.release().catch(() => { });
120
+ }
121
+ }
122
+ async releaseAll() {
123
+ await this.releaseTo(0);
124
+ }
125
+ }
@@ -0,0 +1,48 @@
1
+ import type { PoolLockSpec } from './refresh-lock.js';
2
+ import { type FailureHook } from './rows.js';
3
+ import { type StoreRuntime } from './runtime.js';
4
+ import { type OAuthCredential, type PoolRow, type StoredCredential } from './schema.js';
5
+ /** What the injected provider refresh function returns. */
6
+ export interface ProviderRefreshResult {
7
+ access: string;
8
+ refresh: string;
9
+ expires: number;
10
+ expiresIn?: number;
11
+ /** The account's wire identity, when the provider reports one. */
12
+ identity?: string;
13
+ }
14
+ export type ProviderRefresh = (credential: OAuthCredential & {
15
+ lastRefreshedAt?: number;
16
+ }, row: PoolRow) => Promise<ProviderRefreshResult>;
17
+ export interface RefreshOptions {
18
+ /** The one provider-wide lock serialising every provider call. */
19
+ providerLock?: PoolLockSpec;
20
+ /** Taken after the provider-wide lock in this order, released in reverse. */
21
+ extraLocks?: readonly PoolLockSpec[];
22
+ /** Awaited once the rotation is persisted and the store locks released. */
23
+ onPersisted?: (rowId: string, credential: StoredCredential) => void | Promise<void>;
24
+ onFailure?: FailureHook;
25
+ /**
26
+ * Awaited before any lock is taken, on the locked re-read, and at commit
27
+ * time under the store locks; a reason refuses the refresh there, leaving
28
+ * the stored credential untouched and discarding any rotated material.
29
+ */
30
+ refuse?: (row: PoolRow) => string | undefined | Promise<string | undefined>;
31
+ }
32
+ export type RefreshOutcome = {
33
+ status: 'rotated';
34
+ rowId: string;
35
+ credential: StoredCredential;
36
+ identity?: string;
37
+ } | {
38
+ status: 'refused';
39
+ rowId: string;
40
+ reason: string;
41
+ };
42
+ /**
43
+ * Refreshes one OAuth row. Locks are taken in the fixed order row lock,
44
+ * provider-wide lock, extra locks, and the store locks only around the
45
+ * capture and the commit; ownership of every held lease is asserted after
46
+ * each wait, immediately before the provider call and before each write.
47
+ */
48
+ export declare function refreshRow(rt: StoreRuntime, id: string, provider: ProviderRefresh, options?: RefreshOptions): Promise<RefreshOutcome>;
@@ -0,0 +1,169 @@
1
+ import { PoolOperationError } from './errors.js';
2
+ import { assertNotInsideHook, runInsideHook } from './hooks.js';
3
+ import { recordIdentityIn } from './identity.js';
4
+ import { runOperation, withTransaction } from './mutate.js';
5
+ import { rotateIn } from './rows.js';
6
+ import { readRow, refusal, rowLockSpec } from './runtime.js';
7
+ import { rotationStamp, rotationStampUntrusted, rowLockKey, } from './schema.js';
8
+ function requireRefreshable(id, row) {
9
+ if (!row)
10
+ throw refusal('refresh', id, 'unknown-row', `no row ${id} in the pool`);
11
+ if (row.invalid)
12
+ throw refusal('refresh', id, 'invalid-row', `row ${id} is invalid`);
13
+ if (row.type !== 'oauth' || row.credential?.type !== 'oauth')
14
+ throw refusal('refresh', id, 'no-credential', `row ${id} holds no OAuth credential`);
15
+ if (!row.enabled)
16
+ throw refusal('refresh', id, 'row-disabled', `row ${id} is disabled`);
17
+ return row;
18
+ }
19
+ function stampAhead(id) {
20
+ return refusal('refresh', id, 'refresh-stamp-ahead', `row ${id}'s stored refresh stamp is ahead of the clock; a rotation now could not be stamped newer within the trust bound`, true);
21
+ }
22
+ /**
23
+ * Refreshes one OAuth row. Locks are taken in the fixed order row lock,
24
+ * provider-wide lock, extra locks, and the store locks only around the
25
+ * capture and the commit; ownership of every held lease is asserted after
26
+ * each wait, immediately before the provider call and before each write.
27
+ */
28
+ export async function refreshRow(rt, id, provider, options = {}) {
29
+ assertNotInsideHook('refresh');
30
+ const { ctx } = rt;
31
+ return runOperation(ctx, 'refresh', id, options.onFailure, async (locks, progress) => {
32
+ const { row: seen } = await readRow(rt, 'refresh', id);
33
+ requireRefreshable(id, seen);
34
+ const early = await options.refuse?.(seen);
35
+ if (early !== undefined)
36
+ return { status: 'refused', rowId: id, reason: early };
37
+ let key = rowLockKey(seen);
38
+ let captured;
39
+ for (let attempt = 0;; attempt++) {
40
+ await locks.acquire(rowLockSpec(rt, { id, identity: key === id ? undefined : key }));
41
+ await locks.acquire(options.providerLock ?? rt.providerLock);
42
+ for (const extra of options.extraLocks ?? [])
43
+ await locks.acquire(extra);
44
+ // The capture may write the config once, to give a row its per-row
45
+ // entry. That write is setup, not the rotation, so it is counted apart
46
+ // and never makes a later failure report `after-first-write`.
47
+ const captureProgress = { writes: 0 };
48
+ const read = await withTransaction(ctx, locks, captureProgress, { operation: 'refresh', rowId: id }, async (tx) => {
49
+ let row = requireRefreshable(id, tx.row(id));
50
+ if (!row.hasEntry) {
51
+ tx.setEntry(id, { credentialEpoch: 1, needsFirstReading: true });
52
+ await tx.commitConfig();
53
+ row = requireRefreshable(id, tx.row(id));
54
+ }
55
+ if (rowLockKey(row) !== key)
56
+ return { keyNow: rowLockKey(row) };
57
+ const reason = await options.refuse?.(row);
58
+ if (reason !== undefined)
59
+ return { refused: reason };
60
+ return {
61
+ row,
62
+ credential: row.credential,
63
+ credentialEpoch: row.credentialEpoch,
64
+ identity: row.identity,
65
+ };
66
+ });
67
+ if ('refused' in read)
68
+ return { status: 'refused', rowId: id, reason: read.refused };
69
+ if ('keyNow' in read) {
70
+ await locks.releaseAll();
71
+ if (attempt >= 1)
72
+ throw refusal('refresh', id, 'row-key-changed', `row ${id}'s wire identity changed twice while its lock was held`, true);
73
+ key = read.keyNow;
74
+ continue;
75
+ }
76
+ captured = read;
77
+ break;
78
+ }
79
+ if (rotationStampUntrusted(captured.credential.lastRefreshedAt, ctx.now()))
80
+ throw stampAhead(id);
81
+ await ctx.hold?.('refresh-before-provider', id);
82
+ await locks.assertAll();
83
+ let result;
84
+ try {
85
+ result = await provider(captured.credential, captured.row);
86
+ }
87
+ catch (cause) {
88
+ throw new PoolOperationError({
89
+ operation: 'refresh',
90
+ rowId: id,
91
+ phase: 'before-first-write',
92
+ retryable: true,
93
+ kind: 'provider',
94
+ message: 'the provider refresh failed',
95
+ cause,
96
+ });
97
+ }
98
+ if (typeof result?.refresh !== 'string' || !result.refresh.trim())
99
+ throw refusal('refresh', id, 'provider', 'the provider returned no refresh token', true);
100
+ const commit = await withTransaction(ctx, locks, progress, { operation: 'refresh', rowId: id }, async (tx) => {
101
+ const current = tx.row(id);
102
+ const entry = tx.entry(id);
103
+ if (!current ||
104
+ current.invalid ||
105
+ !entry ||
106
+ entry.credentialEpoch !== captured.credentialEpoch ||
107
+ current.identity !== captured.identity)
108
+ throw new PoolOperationError({
109
+ operation: 'refresh',
110
+ rowId: id,
111
+ phase: 'before-first-write',
112
+ retryable: true,
113
+ kind: 'attribution',
114
+ message: `row ${id} changed credential while its refresh was in flight; the rotation is discarded`,
115
+ });
116
+ const reason = await options.refuse?.(current);
117
+ if (reason !== undefined)
118
+ return { refused: reason };
119
+ const now = ctx.now();
120
+ const prior = current.credential?.type === 'oauth'
121
+ ? current.credential.lastRefreshedAt
122
+ : undefined;
123
+ if (rotationStampUntrusted(prior, now))
124
+ throw stampAhead(id);
125
+ const credential = {
126
+ type: 'oauth',
127
+ access: result.access,
128
+ refresh: result.refresh,
129
+ expires: result.expires,
130
+ };
131
+ const stored = await rotateIn(rt, tx, id, credential, {
132
+ stamp: rotationStamp(prior, now),
133
+ });
134
+ let identity = current.identity;
135
+ if (current.identity === undefined && result.identity) {
136
+ recordIdentityIn(tx, id, result.identity);
137
+ identity = result.identity;
138
+ await tx.commitConfig();
139
+ }
140
+ return { stored, identity, refused: undefined };
141
+ });
142
+ if (commit.refused !== undefined)
143
+ return { status: 'refused', rowId: id, reason: commit.refused };
144
+ if (options.onPersisted) {
145
+ try {
146
+ const persisted = options.onPersisted;
147
+ await runInsideHook('refresh', () => persisted(id, commit.stored));
148
+ }
149
+ catch (cause) {
150
+ throw new PoolOperationError({
151
+ operation: 'refresh',
152
+ rowId: id,
153
+ phase: 'after-first-write',
154
+ retryable: false,
155
+ kind: 'after-persist-hook',
156
+ committed: commit.stored,
157
+ message: 'the after-persist hook threw; the rotation stays committed',
158
+ cause,
159
+ });
160
+ }
161
+ }
162
+ return {
163
+ status: 'rotated',
164
+ rowId: id,
165
+ credential: commit.stored,
166
+ ...(commit.identity !== undefined ? { identity: commit.identity } : {}),
167
+ };
168
+ });
169
+ }
@@ -0,0 +1,53 @@
1
+ import type { PoolOperationError } from './errors.js';
2
+ import { type Transaction } from './mutate.js';
3
+ import type { PoolLockSpec } from './refresh-lock.js';
4
+ import { type StoreRuntime } from './runtime.js';
5
+ import { type PoolCredential, type StoredCredential } from './schema.js';
6
+ export type FailureHook = (rowId: string, error: PoolOperationError) => void | Promise<void>;
7
+ export interface RowOperationOptions {
8
+ /** Called once, awaited, on every non-success path, before locks release. */
9
+ onFailure?: FailureHook;
10
+ /** The provider-wide lock, when an operation may change identity keying. */
11
+ providerLock?: PoolLockSpec;
12
+ }
13
+ export interface AddInput {
14
+ id: string;
15
+ credential: PoolCredential;
16
+ identity?: string;
17
+ label?: string;
18
+ }
19
+ export type AddResult = {
20
+ /** The row holding the credential; an existing row's id on a re-add. */
21
+ id: string;
22
+ outcome: 'added' | 'added-disabled' | 'completed' | 'rotated';
23
+ credential: StoredCredential;
24
+ };
25
+ /**
26
+ * Writes a rotated credential into the state file (one write). A rotation is
27
+ * the same lineage: no epoch bump, no identity or quota change.
28
+ */
29
+ export declare function rotateIn(rt: StoreRuntime, tx: Transaction, id: string, credential: PoolCredential, extra?: {
30
+ stamp?: number;
31
+ clearErrors?: boolean;
32
+ }): Promise<StoredCredential>;
33
+ export declare function addRow(rt: StoreRuntime, input: AddInput, options?: RowOperationOptions): Promise<AddResult>;
34
+ export declare function replaceRow(rt: StoreRuntime, id: string, credential: PoolCredential, input?: {
35
+ identity?: string;
36
+ }, options?: RowOperationOptions): Promise<{
37
+ id: string;
38
+ credential: StoredCredential;
39
+ credentialEpoch: number;
40
+ }>;
41
+ export declare function rotateRow(rt: StoreRuntime, id: string, credential: PoolCredential, input?: {
42
+ identity?: string;
43
+ }, options?: RowOperationOptions): Promise<{
44
+ id: string;
45
+ credential: StoredCredential;
46
+ }>;
47
+ export declare function disableRow(rt: StoreRuntime, id: string, reason: string, options?: Pick<RowOperationOptions, 'onFailure'>): Promise<{
48
+ id: string;
49
+ }>;
50
+ export declare function recordRowIdentity(rt: StoreRuntime, id: string, identity: string, options?: RowOperationOptions): Promise<{
51
+ id: string;
52
+ disabled: string[];
53
+ }>;