@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/store/index.d.ts
CHANGED
|
@@ -6,13 +6,14 @@ 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, UpdateProviderStateResult, } from './provider-state.js';
|
|
9
10
|
export type { PullHook, PullRequest } from './pull.js';
|
|
10
11
|
export type { ProviderRefresh, ProviderRefreshResult, RefreshOptions, RefreshOutcome, } from './refresh.js';
|
|
11
12
|
export type { LockEvent, PoolLockOptions, PoolLockSpec, } from './refresh-lock.js';
|
|
12
13
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
13
|
-
export type { AddInput, AddResult, FailureHook, RemoveOptions, RemoveResult, RemoveView, ReorderOptions, ReorderResult, RowOperationOptions, RowToggleOptions, } from './rows.js';
|
|
14
|
+
export type { AddInput, AddResult, CredentialWriteInput, FailureHook, RemoveOptions, RemoveResult, RemoveView, ReorderOptions, ReorderResult, RowOperationOptions, RowToggleOptions, } from './rows.js';
|
|
14
15
|
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';
|
|
16
|
+
export type { ApiKeyCredential, CredentialStampStatus, OAuthCredential, PoolCredential, PoolRow, ProviderStateCodec, ProviderStateDrop, ProviderStateReplacement, QuotaCodec, RotateCredential, StoredCredential, } from './schema.js';
|
|
17
|
+
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
18
|
export type { PoolSettings, SettingsMutator, SettingsRead, UpdateSettingsOptions, UpdateSettingsResult, } from './settings.js';
|
|
18
19
|
export { POOL_OWNED_KEYS } from './settings.js';
|
package/dist/store/index.js
CHANGED
|
@@ -2,5 +2,5 @@ export { PoolOperationError, PoolReentryError } from './errors.js';
|
|
|
2
2
|
export { countUnknownIdentityRows, DUPLICATE_IDENTITY_REASON, } from './identity.js';
|
|
3
3
|
export { openPoolStore } from './pool.js';
|
|
4
4
|
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';
|
|
5
|
+
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
6
|
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 } 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,12 +105,17 @@ 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
|
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,86 @@
|
|
|
1
|
+
import type { Attribution } from './attribution.js';
|
|
2
|
+
import type { PoolOperation } from './errors.js';
|
|
3
|
+
import type { RowToggleOptions } from './rows.js';
|
|
4
|
+
import { type StoreRuntime } from './runtime.js';
|
|
5
|
+
import { type PoolRow, type ProviderStateCodec } from './schema.js';
|
|
6
|
+
/**
|
|
7
|
+
* What a credential write does to the provider state beside it: `keep`
|
|
8
|
+
* leaves the value on disk as it is (bound by the new stamp only if the old
|
|
9
|
+
* stamp bound it to the same row, epoch and identity), `set` stores a value
|
|
10
|
+
* the codec accepted, and `clear` deletes it.
|
|
11
|
+
*/
|
|
12
|
+
export type ProviderStateWrite = {
|
|
13
|
+
kind: 'keep';
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'set';
|
|
16
|
+
value: unknown;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'clear';
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A provider state as the store will store it: a JSON round trip of it (so
|
|
22
|
+
* its digest is the same once read back from disk), accepted by the codec.
|
|
23
|
+
* Refused before anything is written when the store has no codec, the value
|
|
24
|
+
* is not JSON, or the codec rejects it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function acceptProviderState(codec: ProviderStateCodec | undefined, operation: PoolOperation, id: string, value: unknown, what?: string): unknown;
|
|
27
|
+
/**
|
|
28
|
+
* The write for a value a credential write brings (`add` of a secret the
|
|
29
|
+
* pool holds, `rotate`, a refresh): merged with the row's value on disk by
|
|
30
|
+
* the codec's `merge` when both exist, else the incoming value as is.
|
|
31
|
+
*/
|
|
32
|
+
export declare function mergedProviderState(codec: ProviderStateCodec | undefined, operation: PoolOperation, id: string, onDisk: unknown, incoming: unknown): ProviderStateWrite;
|
|
33
|
+
/**
|
|
34
|
+
* The provider state a replace leaves on the row, decided before anything is
|
|
35
|
+
* written: whatever the codec's `onReplace` returns (undefined clears it), or
|
|
36
|
+
* without that hook the value handed to `replace`, else nothing. The old
|
|
37
|
+
* credential's value is never kept by default: it describes the account the
|
|
38
|
+
* replaced credential belonged to.
|
|
39
|
+
*/
|
|
40
|
+
export declare function replacementProviderState(codec: ProviderStateCodec | undefined, row: PoolRow, credentialEpoch: number, identity: string | undefined, incoming: unknown): ProviderStateWrite;
|
|
41
|
+
/**
|
|
42
|
+
* The provider-state digest the stamp of a credential write carries, and the
|
|
43
|
+
* state-file fields it changes. `set` binds the new value; `keep` carries the
|
|
44
|
+
* old stamp's digest forward only when that stamp bound it to this row as it
|
|
45
|
+
* stood before the write (same credential lineage, the epoch being written,
|
|
46
|
+
* the identity recorded before the write); `clear` binds nothing.
|
|
47
|
+
*/
|
|
48
|
+
export declare function providerStateCoverage(codec: ProviderStateCodec | undefined, write: ProviderStateWrite, prior: Record<string, unknown> | undefined, priorRow: PoolRow | undefined, credentialEpoch: number): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Receives a private copy of the row's provider state (undefined when the row
|
|
51
|
+
* shows none) and the row as loaded under the locks, and returns the next
|
|
52
|
+
* provider state; returning undefined clears it, so a mutator that means to
|
|
53
|
+
* keep the value returns it. It runs under the row lock and the store locks,
|
|
54
|
+
* so it must not call back into the store (`PoolReentryError`).
|
|
55
|
+
*/
|
|
56
|
+
export type ProviderStateMutator = (current: unknown | undefined, row: PoolRow) => unknown | Promise<unknown>;
|
|
57
|
+
export type UpdateProviderStateResult = {
|
|
58
|
+
id: string;
|
|
59
|
+
/** The provider state now on disk; absent when the row has none. */
|
|
60
|
+
providerState?: unknown;
|
|
61
|
+
/**
|
|
62
|
+
* `unchanged`: the mutator returned what the row already shows (or cleared
|
|
63
|
+
* a row that holds none); nothing was written.
|
|
64
|
+
*/
|
|
65
|
+
outcome: 'updated' | 'cleared' | 'unchanged';
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Changes a row's provider state without touching its credential, in one
|
|
69
|
+
* state-file write under the row lock, the caller's extra locks and the
|
|
70
|
+
* store locks. `fence` is what the caller read the row at: the write is
|
|
71
|
+
* refused (`attribution`, retryable) when the row has since moved to another
|
|
72
|
+
* credential epoch or recorded identity, and (`unknown-row`) once it is
|
|
73
|
+
* removed, so a writer that read the row before a replace or a removal never
|
|
74
|
+
* lands its value on the new credential or brings a removed row's state back.
|
|
75
|
+
*
|
|
76
|
+
* The value is bound by the stamp already beside the credential. When its
|
|
77
|
+
* credential-bound part is unchanged the stamp is left byte for byte as it
|
|
78
|
+
* is; otherwise only the stamp's provider-state digest changes, so the
|
|
79
|
+
* credential's stamp status never moves. With `requireCredentialStamps`, an
|
|
80
|
+
* unbound row refuses (`unbound-credential`) as every other strict path
|
|
81
|
+
* does. Without it, a row whose stamp was not written by this store with this
|
|
82
|
+
* credential at the row's epoch and identity refuses the same way: no stamp
|
|
83
|
+
* could bind the value, so no reader would ever show it. A `rotate` or
|
|
84
|
+
* `replace` stamps such a row.
|
|
85
|
+
*/
|
|
86
|
+
export declare function updateProviderStateRow(rt: StoreRuntime, id: string, fence: Attribution, mutator: ProviderStateMutator, options?: RowToggleOptions): Promise<UpdateProviderStateResult>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
2
|
+
import { runOperation, withTransaction } from './mutate.js';
|
|
3
|
+
import { readRow, refusal, requireBound, rowLockSpec, unknownRow, } from './runtime.js';
|
|
4
|
+
import { boundProviderStateDigest, CREDENTIAL_STAMP_KEY, credentialDigest, isCredentialEpoch, isRecord, PROVIDER_STATE_KEY, parseStamp, providerStateDigest, rowLockKey, } from './schema.js';
|
|
5
|
+
/**
|
|
6
|
+
* A provider state as the store will store it: a JSON round trip of it (so
|
|
7
|
+
* its digest is the same once read back from disk), accepted by the codec.
|
|
8
|
+
* Refused before anything is written when the store has no codec, the value
|
|
9
|
+
* is not JSON, or the codec rejects it.
|
|
10
|
+
*/
|
|
11
|
+
export function acceptProviderState(codec, operation, id, value, what = 'the provider state') {
|
|
12
|
+
if (!codec)
|
|
13
|
+
throw refusal(operation, id, 'invalid-input', 'the store was opened without a provider-state codec');
|
|
14
|
+
let normalized;
|
|
15
|
+
try {
|
|
16
|
+
const text = JSON.stringify(value);
|
|
17
|
+
normalized = text === undefined ? undefined : JSON.parse(text);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
normalized = undefined;
|
|
21
|
+
}
|
|
22
|
+
if (normalized === undefined)
|
|
23
|
+
throw refusal(operation, id, 'invalid-provider-state', `${what} cannot be stored as JSON`);
|
|
24
|
+
if (!codec.validate(normalized))
|
|
25
|
+
throw refusal(operation, id, 'invalid-provider-state', `the provider-state codec rejected ${what}`);
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
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 function mergedProviderState(codec, operation, id, onDisk, incoming) {
|
|
34
|
+
if (onDisk === undefined || !codec?.merge)
|
|
35
|
+
return { kind: 'set', value: incoming };
|
|
36
|
+
return {
|
|
37
|
+
kind: 'set',
|
|
38
|
+
value: acceptProviderState(codec, operation, id, codec.merge(structuredClone(onDisk), structuredClone(incoming)), 'the merged provider state'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The provider state a replace leaves on the row, decided before anything is
|
|
43
|
+
* written: whatever the codec's `onReplace` returns (undefined clears it), or
|
|
44
|
+
* without that hook the value handed to `replace`, else nothing. The old
|
|
45
|
+
* credential's value is never kept by default: it describes the account the
|
|
46
|
+
* replaced credential belonged to.
|
|
47
|
+
*/
|
|
48
|
+
export function replacementProviderState(codec, row, credentialEpoch, identity, incoming) {
|
|
49
|
+
const hook = codec?.onReplace;
|
|
50
|
+
const next = hook
|
|
51
|
+
? hook(row.providerState === undefined
|
|
52
|
+
? undefined
|
|
53
|
+
: structuredClone(row.providerState), {
|
|
54
|
+
id: row.id,
|
|
55
|
+
credentialEpoch,
|
|
56
|
+
...(identity !== undefined ? { identity } : {}),
|
|
57
|
+
...(incoming !== undefined
|
|
58
|
+
? { incoming: structuredClone(incoming) }
|
|
59
|
+
: {}),
|
|
60
|
+
})
|
|
61
|
+
: incoming;
|
|
62
|
+
if (next === undefined)
|
|
63
|
+
return { kind: 'clear' };
|
|
64
|
+
if (!hook)
|
|
65
|
+
return { kind: 'set', value: next };
|
|
66
|
+
return {
|
|
67
|
+
kind: 'set',
|
|
68
|
+
value: acceptProviderState(codec, 'replace', row.id, next, 'the provider state onReplace returned'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The provider-state digest the stamp of a credential write carries, and the
|
|
73
|
+
* state-file fields it changes. `set` binds the new value; `keep` carries the
|
|
74
|
+
* old stamp's digest forward only when that stamp bound it to this row as it
|
|
75
|
+
* stood before the write (same credential lineage, the epoch being written,
|
|
76
|
+
* the identity recorded before the write); `clear` binds nothing.
|
|
77
|
+
*/
|
|
78
|
+
export function providerStateCoverage(codec, write, prior, priorRow, credentialEpoch) {
|
|
79
|
+
if (write.kind === 'set')
|
|
80
|
+
return providerStateDigest(codec, write.value);
|
|
81
|
+
if (write.kind === 'clear')
|
|
82
|
+
return undefined;
|
|
83
|
+
return boundProviderStateDigest(prior, priorRow?.credential, credentialEpoch, priorRow?.identity);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Changes a row's provider state without touching its credential, in one
|
|
87
|
+
* state-file write under the row lock, the caller's extra locks and the
|
|
88
|
+
* store locks. `fence` is what the caller read the row at: the write is
|
|
89
|
+
* refused (`attribution`, retryable) when the row has since moved to another
|
|
90
|
+
* credential epoch or recorded identity, and (`unknown-row`) once it is
|
|
91
|
+
* removed, so a writer that read the row before a replace or a removal never
|
|
92
|
+
* lands its value on the new credential or brings a removed row's state back.
|
|
93
|
+
*
|
|
94
|
+
* The value is bound by the stamp already beside the credential. When its
|
|
95
|
+
* credential-bound part is unchanged the stamp is left byte for byte as it
|
|
96
|
+
* is; otherwise only the stamp's provider-state digest changes, so the
|
|
97
|
+
* credential's stamp status never moves. With `requireCredentialStamps`, an
|
|
98
|
+
* unbound row refuses (`unbound-credential`) as every other strict path
|
|
99
|
+
* does. Without it, a row whose stamp was not written by this store with this
|
|
100
|
+
* credential at the row's epoch and identity refuses the same way: no stamp
|
|
101
|
+
* could bind the value, so no reader would ever show it. A `rotate` or
|
|
102
|
+
* `replace` stamps such a row.
|
|
103
|
+
*/
|
|
104
|
+
export async function updateProviderStateRow(rt, id, fence, mutator, options = {}) {
|
|
105
|
+
assertNotInsideHook('updateProviderState');
|
|
106
|
+
const { ctx } = rt;
|
|
107
|
+
const operation = 'updateProviderState';
|
|
108
|
+
return runOperation(ctx, operation, id, options.onFailure, async (locks, progress) => {
|
|
109
|
+
const codec = ctx.providerState;
|
|
110
|
+
if (!codec)
|
|
111
|
+
throw refusal(operation, id, 'invalid-input', 'the store was opened without a provider-state codec');
|
|
112
|
+
if (typeof mutator !== 'function')
|
|
113
|
+
throw refusal(operation, id, 'invalid-input', 'a mutator is required');
|
|
114
|
+
const captured = isRecord(fence) ? fence.credentialEpoch : undefined;
|
|
115
|
+
if (!isCredentialEpoch(captured))
|
|
116
|
+
throw refusal(operation, id, 'invalid-input', 'the credential epoch the provider state was read at is required');
|
|
117
|
+
const { row: seen } = await readRow(rt, operation, id);
|
|
118
|
+
await locks.acquire(rowLockSpec(rt, seen));
|
|
119
|
+
for (const extra of options.extraLocks ?? [])
|
|
120
|
+
await locks.acquire(extra);
|
|
121
|
+
return withTransaction(ctx, locks, progress, { operation, rowId: id }, async (tx) => {
|
|
122
|
+
const row = tx.row(id);
|
|
123
|
+
if (!row)
|
|
124
|
+
throw unknownRow(operation, id);
|
|
125
|
+
if (rowLockKey(row) !== rowLockKey(seen))
|
|
126
|
+
throw refusal(operation, id, 'row-key-changed', `row ${id}'s wire identity changed while its lock was being taken`, true);
|
|
127
|
+
if (row.invalid)
|
|
128
|
+
throw refusal(operation, id, 'invalid-row', `row ${id} failed validation`);
|
|
129
|
+
if (!row.credential)
|
|
130
|
+
throw refusal(operation, id, 'no-credential', `row ${id} holds no credential`);
|
|
131
|
+
requireBound(operation, row);
|
|
132
|
+
const epoch = row.credentialEpoch ?? 1;
|
|
133
|
+
if (epoch !== captured || row.identity !== fence.identity)
|
|
134
|
+
throw refusal(operation, id, 'attribution', `the provider state for ${id} was read for a credential or account the row no longer holds`, true);
|
|
135
|
+
const account = tx.stateAccount(id);
|
|
136
|
+
const rawStamp = account?.[CREDENTIAL_STAMP_KEY];
|
|
137
|
+
const stamp = parseStamp(rawStamp);
|
|
138
|
+
if (!isRecord(rawStamp) ||
|
|
139
|
+
!stamp?.binding ||
|
|
140
|
+
stamp.digest !== credentialDigest(row.credential) ||
|
|
141
|
+
stamp.credentialEpoch !== epoch ||
|
|
142
|
+
stamp.binding.identity !== row.identity)
|
|
143
|
+
throw refusal(operation, id, 'unbound-credential', `row ${id}'s credential carries no stamp of this store to bind a provider state to (stamp ${row.stamp}); rotate or replace it first`);
|
|
144
|
+
const current = row.providerState === undefined
|
|
145
|
+
? undefined
|
|
146
|
+
: structuredClone(row.providerState);
|
|
147
|
+
const returned = await runInsideHook(operation, () => mutator(current, row));
|
|
148
|
+
const next = returned === undefined
|
|
149
|
+
? undefined
|
|
150
|
+
: acceptProviderState(codec, operation, id, returned, 'the provider state the mutator returned');
|
|
151
|
+
const held = account !== undefined && Object.hasOwn(account, PROVIDER_STATE_KEY);
|
|
152
|
+
if (next === undefined
|
|
153
|
+
? !held
|
|
154
|
+
: row.providerState !== undefined &&
|
|
155
|
+
JSON.stringify(row.providerState) === JSON.stringify(next))
|
|
156
|
+
return {
|
|
157
|
+
id,
|
|
158
|
+
outcome: 'unchanged',
|
|
159
|
+
...(next !== undefined ? { providerState: next } : {}),
|
|
160
|
+
};
|
|
161
|
+
const nextAccount = { ...account };
|
|
162
|
+
if (next === undefined) {
|
|
163
|
+
delete nextAccount[PROVIDER_STATE_KEY];
|
|
164
|
+
const nextStamp = { ...rawStamp };
|
|
165
|
+
delete nextStamp.providerState;
|
|
166
|
+
nextAccount[CREDENTIAL_STAMP_KEY] = nextStamp;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
nextAccount[PROVIDER_STATE_KEY] = next;
|
|
170
|
+
const digest = providerStateDigest(codec, next);
|
|
171
|
+
// A change confined to the part the codec does not bind to the
|
|
172
|
+
// credential leaves the stamp exactly as it was.
|
|
173
|
+
if (stamp.providerState !== digest)
|
|
174
|
+
nextAccount[CREDENTIAL_STAMP_KEY] = {
|
|
175
|
+
...rawStamp,
|
|
176
|
+
providerState: digest,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
tx.setStateAccount(id, nextAccount);
|
|
180
|
+
await tx.commitState();
|
|
181
|
+
return next === undefined
|
|
182
|
+
? { id, outcome: 'cleared' }
|
|
183
|
+
: { id, outcome: 'updated', providerState: next };
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
package/dist/store/refresh.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ export interface ProviderRefreshResult {
|
|
|
10
10
|
expiresIn?: number;
|
|
11
11
|
/** The account's wire identity, when the provider reports one. */
|
|
12
12
|
identity?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Provider state that changes with the new token (needs the store's
|
|
15
|
+
* provider-state codec). It is merged with the row's value on disk
|
|
16
|
+
* (`ProviderStateCodec.merge`) and written in the same state write as the
|
|
17
|
+
* rotated credential, under the same commit fence. Left out, the row keeps
|
|
18
|
+
* its value.
|
|
19
|
+
*/
|
|
20
|
+
providerState?: unknown;
|
|
13
21
|
}
|
|
14
22
|
export type ProviderRefresh = (credential: OAuthCredential & {
|
|
15
23
|
lastRefreshedAt?: number;
|
package/dist/store/refresh.js
CHANGED
|
@@ -2,6 +2,7 @@ import { PoolOperationError } from './errors.js';
|
|
|
2
2
|
import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
3
3
|
import { recordIdentityIn } from './identity.js';
|
|
4
4
|
import { runOperation, withTransaction } from './mutate.js';
|
|
5
|
+
import { acceptProviderState, mergedProviderState } from './provider-state.js';
|
|
5
6
|
import { rotateIn } from './rows.js';
|
|
6
7
|
import { readRow, refusal, requireBound, rowLockSpec, } from './runtime.js';
|
|
7
8
|
import { rotationStamp, rotationStampUntrusted, rowLockKey, } from './schema.js';
|
|
@@ -98,6 +99,9 @@ export async function refreshRow(rt, id, provider, options = {}) {
|
|
|
98
99
|
}
|
|
99
100
|
if (typeof result?.refresh !== 'string' || !result.refresh.trim())
|
|
100
101
|
throw refusal('refresh', id, 'provider', 'the provider returned no refresh token', true);
|
|
102
|
+
const incoming = result.providerState === undefined
|
|
103
|
+
? undefined
|
|
104
|
+
: acceptProviderState(ctx.providerState, 'refresh', id, result.providerState, 'the provider state the provider returned');
|
|
101
105
|
const commit = await withTransaction(ctx, locks, progress, { operation: 'refresh', rowId: id }, async (tx) => {
|
|
102
106
|
const current = tx.row(id);
|
|
103
107
|
const entry = tx.entry(id);
|
|
@@ -142,6 +146,11 @@ export async function refreshRow(rt, id, provider, options = {}) {
|
|
|
142
146
|
const stored = await rotateIn(rt, tx, id, credential, {
|
|
143
147
|
stamp: rotationStamp(prior, now),
|
|
144
148
|
identity: learnt,
|
|
149
|
+
...(incoming !== undefined
|
|
150
|
+
? {
|
|
151
|
+
providerState: mergedProviderState(ctx.providerState, 'refresh', id, current.providerState, incoming),
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
145
154
|
});
|
|
146
155
|
let identity = current.identity;
|
|
147
156
|
if (learnt !== undefined) {
|
package/dist/store/rows.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Attribution } from './attribution.js';
|
|
2
2
|
import { PoolOperationError } from './errors.js';
|
|
3
3
|
import { type Transaction } from './mutate.js';
|
|
4
|
+
import { type ProviderStateWrite } from './provider-state.js';
|
|
4
5
|
import type { PoolLockSpec } from './refresh-lock.js';
|
|
5
6
|
import { type StoreRuntime } from './runtime.js';
|
|
6
7
|
import { type CredentialBinding, type PoolCredential, type PoolRow, type RotateCredential, type StoredCredential } from './schema.js';
|
|
@@ -80,6 +81,24 @@ export interface AddInput {
|
|
|
80
81
|
credential: PoolCredential;
|
|
81
82
|
identity?: string;
|
|
82
83
|
label?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Provider state for the credential, written in the same state write as
|
|
86
|
+
* the credential (needs the store's provider-state codec). On an `add`
|
|
87
|
+
* that rotates a row already holding this secret it is merged with the
|
|
88
|
+
* row's value (`ProviderStateCodec.merge`); left out, that row keeps its
|
|
89
|
+
* value.
|
|
90
|
+
*/
|
|
91
|
+
providerState?: unknown;
|
|
92
|
+
}
|
|
93
|
+
/** What `replace` and `rotate` take beside the credential. */
|
|
94
|
+
export interface CredentialWriteInput {
|
|
95
|
+
identity?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Provider state written in the same state write as the credential. For
|
|
98
|
+
* `rotate` it is merged with the row's value; left out, the row keeps its
|
|
99
|
+
* value. For `replace` see `ProviderStateCodec.onReplace`.
|
|
100
|
+
*/
|
|
101
|
+
providerState?: unknown;
|
|
83
102
|
}
|
|
84
103
|
export type AddResult = {
|
|
85
104
|
/** The row holding the credential; an existing row's id on a re-add. */
|
|
@@ -102,18 +121,15 @@ export declare function rotateIn(rt: StoreRuntime, tx: Transaction, id: string,
|
|
|
102
121
|
clearErrors?: boolean;
|
|
103
122
|
binding?: CredentialBinding;
|
|
104
123
|
identity?: string;
|
|
124
|
+
providerState?: ProviderStateWrite;
|
|
105
125
|
}): Promise<StoredCredential>;
|
|
106
126
|
export declare function addRow(rt: StoreRuntime, input: AddInput, options?: RowOperationOptions): Promise<AddResult>;
|
|
107
|
-
export declare function replaceRow(rt: StoreRuntime, id: string, credential: PoolCredential, input?: {
|
|
108
|
-
identity?: string;
|
|
109
|
-
}, options?: RowOperationOptions): Promise<{
|
|
127
|
+
export declare function replaceRow(rt: StoreRuntime, id: string, credential: PoolCredential, input?: CredentialWriteInput, options?: RowOperationOptions): Promise<{
|
|
110
128
|
id: string;
|
|
111
129
|
credential: StoredCredential;
|
|
112
130
|
credentialEpoch: number;
|
|
113
131
|
}>;
|
|
114
|
-
export declare function rotateRow(rt: StoreRuntime, id: string, credential: RotateCredential, input?: {
|
|
115
|
-
identity?: string;
|
|
116
|
-
}, options?: RowOperationOptions): Promise<{
|
|
132
|
+
export declare function rotateRow(rt: StoreRuntime, id: string, credential: RotateCredential, input?: CredentialWriteInput, options?: RowOperationOptions): Promise<{
|
|
117
133
|
id: string;
|
|
118
134
|
credential: StoredCredential;
|
|
119
135
|
}>;
|