@cortexkit/common-auth 0.2.1 → 0.2.3
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/routing/sticky.d.ts +32 -5
- package/dist/routing/sticky.js +20 -2
- package/dist/store/errors.d.ts +2 -2
- package/dist/store/index.d.ts +1 -1
- package/dist/store/mutate.d.ts +8 -0
- package/dist/store/mutate.js +16 -0
- package/dist/store/pool.d.ts +20 -2
- package/dist/store/pool.js +3 -1
- package/dist/store/rows.d.ts +74 -2
- package/dist/store/rows.js +136 -9
- package/package.json +1 -1
package/dist/routing/sticky.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ProjectedQuota } from '../quota/projection.js';
|
|
2
|
-
import { type AdmissionInput, type AdmissionRefusal, type AdmissionResult, type WindowRef } from './admission.js';
|
|
2
|
+
import { type AdmissionInput, type AdmissionRefusal, type AdmissionResult, type RoutingRow, type WindowRef } from './admission.js';
|
|
3
3
|
import { type StickyPin } from './pins.js';
|
|
4
4
|
export declare const QUOTA_STALENESS_MS: number;
|
|
5
5
|
export declare const MIN_RESET_HOURS: number;
|
|
@@ -63,14 +63,40 @@ export interface StickySelection {
|
|
|
63
63
|
* configured order, preferring one with an applicable reset credit.
|
|
64
64
|
*/
|
|
65
65
|
export declare function selectStickyCandidate(input: StickySelectionInput): StickySelection | undefined;
|
|
66
|
+
/** Reserve percent per window label; a missing label reserves nothing. */
|
|
67
|
+
export type ReservePercent = Readonly<Record<string, number>>;
|
|
68
|
+
/**
|
|
69
|
+
* Reserve percentages per row: a map keyed by row id, or a function of the
|
|
70
|
+
* row. A row the map lacks, or for which the function returns undefined,
|
|
71
|
+
* takes the shared `reservePercent`.
|
|
72
|
+
*/
|
|
73
|
+
export type RowReservePercent = ReadonlyMap<string, ReservePercent> | ((row: RoutingRow) => ReservePercent | undefined);
|
|
74
|
+
/**
|
|
75
|
+
* What a valid pin does when its row is not dispatched.
|
|
76
|
+
*
|
|
77
|
+
* `keep`: the pin is retained whatever kept its row from this request.
|
|
78
|
+
*
|
|
79
|
+
* `move-on-confirmed-exhaustion`: the pin moves to the row this request is
|
|
80
|
+
* dispatched to when its own row was refused as confirmed exhausted (a spent
|
|
81
|
+
* window with a future reset, or a spent credit budget) or killed by the
|
|
82
|
+
* killswitch. A refusal for unknown quota (no reading yet, a missing window,
|
|
83
|
+
* an exhausted reading without a usable reset) and an exclusion (rate-limit
|
|
84
|
+
* mark, refresh backoff) keep the pin while this request is served elsewhere.
|
|
85
|
+
* With no admissible row the pin is retained either way.
|
|
86
|
+
*/
|
|
87
|
+
export type RefusedPinPolicy = 'keep' | 'move-on-confirmed-exhaustion';
|
|
66
88
|
export interface StickyRouteInput extends AdmissionInput {
|
|
67
89
|
requestBytes: number;
|
|
68
90
|
/** Bytes already committed per row, for example from other sessions' pins. */
|
|
69
91
|
pendingBytes?: ReadonlyMap<string, number>;
|
|
70
92
|
/** Killswitch verdict per row; a missing row passes. */
|
|
71
93
|
killswitch?: ReadonlyMap<string, boolean>;
|
|
72
|
-
/** Reserve percent per window label,
|
|
73
|
-
reservePercent?:
|
|
94
|
+
/** Reserve percent per window label, for every row without its own. */
|
|
95
|
+
reservePercent?: ReservePercent;
|
|
96
|
+
/** Per-row reserves, which replace `reservePercent` for the rows they cover. */
|
|
97
|
+
rowReservePercent?: RowReservePercent;
|
|
98
|
+
/** Defaults to `keep`. */
|
|
99
|
+
refusedPinPolicy?: RefusedPinPolicy;
|
|
74
100
|
resetCreditsApplicable?: ReadonlyMap<string, number>;
|
|
75
101
|
/** The session's current pin, if it has one. */
|
|
76
102
|
pin?: StickyPin;
|
|
@@ -80,8 +106,9 @@ export interface StickyRouteInput extends AdmissionInput {
|
|
|
80
106
|
}
|
|
81
107
|
/**
|
|
82
108
|
* What the caller does with the session's pin: keep it, replace it with
|
|
83
|
-
* `pin`, or drop it.
|
|
84
|
-
* routed elsewhere because its row was
|
|
109
|
+
* `pin`, or drop it. Under the default `keep` policy a valid pin is always
|
|
110
|
+
* kept, even when this request was routed elsewhere because its row was
|
|
111
|
+
* refused, excluded or killed; `refusedPinPolicy` can move it instead.
|
|
85
112
|
*/
|
|
86
113
|
export type PinAction = {
|
|
87
114
|
action: 'retain';
|
package/dist/routing/sticky.js
CHANGED
|
@@ -233,6 +233,24 @@ export function routeSticky(input) {
|
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
+
// A spent window with a future reset, a spent credit budget and a killswitch
|
|
237
|
+
// verdict all say the pinned row will not serve until some known later time,
|
|
238
|
+
// so the pin may move. A row refused for want of a usable reading, or
|
|
239
|
+
// excluded by a short rate-limit mark or refresh backoff, may serve again on
|
|
240
|
+
// the next reading, so its pin stays.
|
|
241
|
+
const pinRefusal = input.pin ? refusals.get(input.pin.accountId) : undefined;
|
|
242
|
+
const pinMoves = pinValid &&
|
|
243
|
+
input.pin !== undefined &&
|
|
244
|
+
input.refusedPinPolicy === 'move-on-confirmed-exhaustion' &&
|
|
245
|
+
(input.killswitch?.get(input.pin.accountId) === false ||
|
|
246
|
+
pinRefusal?.reason === 'exhausted' ||
|
|
247
|
+
pinRefusal?.reason === 'budget-spent');
|
|
248
|
+
const reserveFor = (row) => {
|
|
249
|
+
const perRow = typeof input.rowReservePercent === 'function'
|
|
250
|
+
? input.rowReservePercent(row)
|
|
251
|
+
: input.rowReservePercent?.get(row.id);
|
|
252
|
+
return perRow ?? input.reservePercent ?? {};
|
|
253
|
+
};
|
|
236
254
|
const scope = input.scope;
|
|
237
255
|
let candidates = input.rows
|
|
238
256
|
.map((row, configuredOrder) => ({ row, configuredOrder }))
|
|
@@ -246,7 +264,7 @@ export function routeSticky(input) {
|
|
|
246
264
|
? undefined
|
|
247
265
|
: (admitted.get(row.id)?.projection ??
|
|
248
266
|
projectQuota(row.quota, scope)),
|
|
249
|
-
reservePercent:
|
|
267
|
+
reservePercent: reserveFor(row),
|
|
250
268
|
configuredOrder,
|
|
251
269
|
...(credits === undefined ? {} : { resetCreditsApplicable: credits }),
|
|
252
270
|
...(killswitchPasses === undefined ? {} : { killswitchPasses }),
|
|
@@ -284,7 +302,7 @@ export function routeSticky(input) {
|
|
|
284
302
|
...(selection.quotaCheckedAt === undefined
|
|
285
303
|
? {}
|
|
286
304
|
: { quotaCheckedAt: selection.quotaCheckedAt }),
|
|
287
|
-
pin: pinValid
|
|
305
|
+
pin: pinValid && !pinMoves
|
|
288
306
|
? { action: 'retain' }
|
|
289
307
|
: {
|
|
290
308
|
action: 'assign',
|
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' | 'recordIdentity' | 'refresh' | 'pull';
|
|
3
|
+
export type PoolOperation = 'initialize' | 'add' | 'replace' | 'rotate' | 'disable' | 'enable' | 'remove' | 'recordIdentity' | 'refresh' | 'pull';
|
|
4
4
|
/**
|
|
5
5
|
* How far an operation got before it failed.
|
|
6
6
|
*
|
|
@@ -16,7 +16,7 @@ export type PoolFailurePhase = 'before-first-write' | 'after-first-write' | 'pul
|
|
|
16
16
|
* lock outcomes (a wait that ran out, and a lease found lost); the rest are
|
|
17
17
|
* refusals and failures of the operation itself.
|
|
18
18
|
*/
|
|
19
|
-
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-key-changed' | 'refresh-stamp-ahead' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'after-persist-hook' | 'unexpected';
|
|
19
|
+
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' | 'row-key-changed' | 'refresh-stamp-ahead' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'after-persist-hook' | 'unexpected';
|
|
20
20
|
/**
|
|
21
21
|
* The single failure value of every store operation. `committed` is present
|
|
22
22
|
* only when the operation had already written a credential to the state file
|
package/dist/store/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type { PullHook, PullRequest } from './pull.js';
|
|
|
10
10
|
export type { ProviderRefresh, ProviderRefreshResult, RefreshOptions, RefreshOutcome, } from './refresh.js';
|
|
11
11
|
export type { LockEvent, PoolLockOptions, PoolLockSpec, } from './refresh-lock.js';
|
|
12
12
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
13
|
-
export type { AddInput, AddResult, FailureHook, RowOperationOptions, } from './rows.js';
|
|
13
|
+
export type { AddInput, AddResult, FailureHook, RemoveOptions, RemoveResult, RemoveView, RowOperationOptions, RowToggleOptions, } from './rows.js';
|
|
14
14
|
export type { PullReason } from './runtime.js';
|
|
15
15
|
export type { ApiKeyCredential, OAuthCredential, PoolCredential, PoolRow, QuotaCodec, StoredCredential, } from './schema.js';
|
|
16
16
|
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
package/dist/store/mutate.d.ts
CHANGED
|
@@ -76,10 +76,18 @@ export declare class Transaction {
|
|
|
76
76
|
roster(): unknown[];
|
|
77
77
|
/** The first roster row with this id (the one the pool loads). */
|
|
78
78
|
rosterRow(id: string): Record<string, unknown> | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Drops every roster row carrying this id. The row's per-row entry goes with
|
|
81
|
+
* it on the next `commitConfig`, which drops entries for ids no longer in
|
|
82
|
+
* the roster. Returns how many roster rows were dropped.
|
|
83
|
+
*/
|
|
84
|
+
dropRosterRows(id: string): number;
|
|
79
85
|
entries(): Record<string, unknown>;
|
|
80
86
|
entry(id: string): Record<string, unknown> | undefined;
|
|
81
87
|
setEntry(id: string, entry: Record<string, unknown>): void;
|
|
82
88
|
stateAccount(id: string): Record<string, unknown> | undefined;
|
|
89
|
+
/** Drops the row's credential and runtime fields from the state file's accounts. */
|
|
90
|
+
dropStateAccount(id: string): void;
|
|
83
91
|
setStateAccount(id: string, fields: Record<string, unknown>): void;
|
|
84
92
|
/**
|
|
85
93
|
* Writes the config: legacy `version: 1` and the legacy roster beside
|
package/dist/store/mutate.js
CHANGED
|
@@ -94,6 +94,17 @@ export class Transaction {
|
|
|
94
94
|
rosterRow(id) {
|
|
95
95
|
return this.roster().find((raw) => isRecord(raw) && raw.id === id);
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Drops every roster row carrying this id. The row's per-row entry goes with
|
|
99
|
+
* it on the next `commitConfig`, which drops entries for ids no longer in
|
|
100
|
+
* the roster. Returns how many roster rows were dropped.
|
|
101
|
+
*/
|
|
102
|
+
dropRosterRows(id) {
|
|
103
|
+
const roster = this.roster();
|
|
104
|
+
const kept = roster.filter((raw) => !(isRecord(raw) && raw.id === id));
|
|
105
|
+
this.config.accounts = kept;
|
|
106
|
+
return roster.length - kept.length;
|
|
107
|
+
}
|
|
97
108
|
entries() {
|
|
98
109
|
if (!isRecord(this.config[POOL_KEY]))
|
|
99
110
|
this.config[POOL_KEY] = {};
|
|
@@ -120,6 +131,11 @@ export class Transaction {
|
|
|
120
131
|
const entry = Object.hasOwn(accounts, id) ? accounts[id] : undefined;
|
|
121
132
|
return isRecord(entry) ? entry : undefined;
|
|
122
133
|
}
|
|
134
|
+
/** Drops the row's credential and runtime fields from the state file's accounts. */
|
|
135
|
+
dropStateAccount(id) {
|
|
136
|
+
if (isRecord(this.state.accounts) && Object.hasOwn(this.state.accounts, id))
|
|
137
|
+
delete this.state.accounts[id];
|
|
138
|
+
}
|
|
123
139
|
setStateAccount(id, fields) {
|
|
124
140
|
if (!isRecord(this.state.accounts))
|
|
125
141
|
this.state.accounts = {};
|
package/dist/store/pool.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type HoldPoint, type InitializeOutcome, type StoreContext } from './mut
|
|
|
5
5
|
import { type PullHook } from './pull.js';
|
|
6
6
|
import { type ProviderRefresh, type RefreshOptions, type RefreshOutcome } from './refresh.js';
|
|
7
7
|
import { type LockEnvironment, type PoolLockOptions, type PoolLockSpec } from './refresh-lock.js';
|
|
8
|
-
import { type AddInput, type AddResult, type RowOperationOptions } from './rows.js';
|
|
8
|
+
import { type AddInput, type AddResult, type RemoveOptions, type RemoveResult, type RowOperationOptions, type RowToggleOptions } from './rows.js';
|
|
9
9
|
import { type PoolCredential, type PoolRow, type QuotaCodec, type StoredCredential } from './schema.js';
|
|
10
10
|
export interface OpenPoolStoreOptions {
|
|
11
11
|
/** The provider every row of this pool belongs to; keys the provider-wide lock. */
|
|
@@ -80,9 +80,27 @@ export interface PoolStore {
|
|
|
80
80
|
id: string;
|
|
81
81
|
credential: StoredCredential;
|
|
82
82
|
}>;
|
|
83
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Sets `enabled: false` and the entry's `disabledReason`. Takes the row
|
|
85
|
+
* lock, then `extraLocks`, then the store locks (the row lock and
|
|
86
|
+
* `extraLocks` since 0.2.3).
|
|
87
|
+
*/
|
|
88
|
+
disable(id: string, reason: string, options?: RowToggleOptions): Promise<{
|
|
89
|
+
id: string;
|
|
90
|
+
}>;
|
|
91
|
+
/**
|
|
92
|
+
* Clears `enabled: false` and `disabledReason` (since 0.2.3); refuses with
|
|
93
|
+
* `duplicate-identity` when another enabled OAuth row holds the row's
|
|
94
|
+
* identity. Locks as `disable`.
|
|
95
|
+
*/
|
|
96
|
+
enable(id: string, options?: RowToggleOptions): Promise<{
|
|
84
97
|
id: string;
|
|
85
98
|
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Deletes the roster row, its per-row entry and its state-file credential
|
|
101
|
+
* (since 0.2.3). Locks as `disable`; `protect` can refuse the id.
|
|
102
|
+
*/
|
|
103
|
+
remove(id: string, options?: RemoveOptions): Promise<RemoveResult>;
|
|
86
104
|
recordIdentity(id: string, identity: string, options?: RowOperationOptions): Promise<{
|
|
87
105
|
id: string;
|
|
88
106
|
disabled: string[];
|
package/dist/store/pool.js
CHANGED
|
@@ -3,7 +3,7 @@ import { initializePool, readPool, } from './mutate.js';
|
|
|
3
3
|
import { PullScheduler } from './pull.js';
|
|
4
4
|
import { refreshRow, } from './refresh.js';
|
|
5
5
|
import { POOL_LOCK_DEFAULTS, } from './refresh-lock.js';
|
|
6
|
-
import { addRow, disableRow, recordRowIdentity, replaceRow, rotateRow, } from './rows.js';
|
|
6
|
+
import { addRow, disableRow, enableRow, recordRowIdentity, removeRow, replaceRow, rotateRow, } from './rows.js';
|
|
7
7
|
import { POOL_SCHEMA_VERSION, } from './schema.js';
|
|
8
8
|
/**
|
|
9
9
|
* Process-wide memory per config file: ids whose per-row entry a library
|
|
@@ -92,6 +92,8 @@ export function openPoolStore(options) {
|
|
|
92
92
|
replace: (id, credential, input, callOptions) => replaceRow(rt, id, credential, input, callOptions),
|
|
93
93
|
rotate: (id, credential, input, callOptions) => rotateRow(rt, id, credential, input, callOptions),
|
|
94
94
|
disable: (id, reason, callOptions) => disableRow(rt, id, reason, callOptions),
|
|
95
|
+
enable: (id, callOptions) => enableRow(rt, id, callOptions),
|
|
96
|
+
remove: (id, callOptions) => removeRow(rt, id, callOptions),
|
|
95
97
|
recordIdentity: (id, identity, callOptions) => recordRowIdentity(rt, id, identity, callOptions),
|
|
96
98
|
refresh: (id, provider, callOptions) => refreshRow(rt, id, provider, callOptions),
|
|
97
99
|
recordQuota: (id, attribution, observation) => recordQuota(rt, id, attribution, observation),
|
package/dist/store/rows.d.ts
CHANGED
|
@@ -2,14 +2,61 @@ import type { PoolOperationError } from './errors.js';
|
|
|
2
2
|
import { type Transaction } from './mutate.js';
|
|
3
3
|
import type { PoolLockSpec } from './refresh-lock.js';
|
|
4
4
|
import { type StoreRuntime } from './runtime.js';
|
|
5
|
-
import { type PoolCredential, type StoredCredential } from './schema.js';
|
|
5
|
+
import { type PoolCredential, type PoolRow, type StoredCredential } from './schema.js';
|
|
6
6
|
export type FailureHook = (rowId: string, error: PoolOperationError) => void | Promise<void>;
|
|
7
7
|
export interface RowOperationOptions {
|
|
8
8
|
/** Called once, awaited, on every non-success path, before locks release. */
|
|
9
9
|
onFailure?: FailureHook;
|
|
10
10
|
/** The provider-wide lock, when an operation may change identity keying. */
|
|
11
11
|
providerLock?: PoolLockSpec;
|
|
12
|
+
/**
|
|
13
|
+
* Further locks taken after the row lock and the provider-wide lock, in
|
|
14
|
+
* this order, before the store locks: the same place `refresh` takes its
|
|
15
|
+
* extra locks, so a caller holding legacy locks around a row write and a
|
|
16
|
+
* refresh of that row acquire them in one order and cannot deadlock.
|
|
17
|
+
*/
|
|
18
|
+
extraLocks?: readonly PoolLockSpec[];
|
|
12
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Options of `disable`, `enable` and `remove`. The provider-wide lock guards
|
|
22
|
+
* changes to the recorded identity a row lock is named by; none of these
|
|
23
|
+
* three records an identity, so none takes it. The extra locks are taken
|
|
24
|
+
* where every other row write takes them, after the row lock and before the
|
|
25
|
+
* store locks.
|
|
26
|
+
*/
|
|
27
|
+
export type RowToggleOptions = Pick<RowOperationOptions, 'onFailure' | 'extraLocks'>;
|
|
28
|
+
/** What a `remove` protect predicate is shown, read under every lock. */
|
|
29
|
+
export interface RemoveView {
|
|
30
|
+
/**
|
|
31
|
+
* The row as loaded; undefined when the roster no longer holds the id and
|
|
32
|
+
* only its state-file entry is left (a removal interrupted between writes).
|
|
33
|
+
*/
|
|
34
|
+
row: PoolRow | undefined;
|
|
35
|
+
/** The config file as read under the store locks. */
|
|
36
|
+
config: Readonly<Record<string, unknown>>;
|
|
37
|
+
/** The state file as read under the store locks. */
|
|
38
|
+
state: Readonly<Record<string, unknown>>;
|
|
39
|
+
}
|
|
40
|
+
export interface RemoveOptions extends RowToggleOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Awaited under every lock before anything is written; a reason refuses
|
|
43
|
+
* the removal (kind `row-protected`) with both files unchanged. The store
|
|
44
|
+
* keeps no record of a plugin's in-flight work, so this is where a plugin
|
|
45
|
+
* refuses an id it reserves or one its own pending-operation record (kept
|
|
46
|
+
* in the config or state file) still names: reading that record from the
|
|
47
|
+
* locked files here cannot race a writer that holds the store locks.
|
|
48
|
+
*/
|
|
49
|
+
protect?: (id: string, view: RemoveView) => string | undefined | Promise<string | undefined>;
|
|
50
|
+
}
|
|
51
|
+
export type RemoveResult = {
|
|
52
|
+
id: string;
|
|
53
|
+
/**
|
|
54
|
+
* `removed`: the roster row was dropped (and its state entry, if any).
|
|
55
|
+
* `completed`: only a state-file entry was left, by a removal interrupted
|
|
56
|
+
* between its config and state writes, and it is now dropped.
|
|
57
|
+
*/
|
|
58
|
+
outcome: 'removed' | 'completed';
|
|
59
|
+
};
|
|
13
60
|
export interface AddInput {
|
|
14
61
|
id: string;
|
|
15
62
|
credential: PoolCredential;
|
|
@@ -44,9 +91,34 @@ export declare function rotateRow(rt: StoreRuntime, id: string, credential: Pool
|
|
|
44
91
|
id: string;
|
|
45
92
|
credential: StoredCredential;
|
|
46
93
|
}>;
|
|
47
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Marks a row disabled. Since 0.2.3 it takes the row lock and the caller's
|
|
96
|
+
* extra locks before the store locks, as the other row writes do, so it waits
|
|
97
|
+
* for a refresh of the row instead of landing during its provider call.
|
|
98
|
+
*/
|
|
99
|
+
export declare function disableRow(rt: StoreRuntime, id: string, reason: string, options?: RowToggleOptions): Promise<{
|
|
100
|
+
id: string;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Clears a row's `enabled: false` and its `disabledReason` in one config
|
|
104
|
+
* write. An OAuth row whose recorded identity another enabled OAuth row holds
|
|
105
|
+
* stays disabled and the call refuses (`duplicate-identity`): the same rule
|
|
106
|
+
* that makes `add` store such a row disabled. Enabling a row that is already
|
|
107
|
+
* enabled writes nothing.
|
|
108
|
+
*/
|
|
109
|
+
export declare function enableRow(rt: StoreRuntime, id: string, options?: RowToggleOptions): Promise<{
|
|
48
110
|
id: string;
|
|
49
111
|
}>;
|
|
112
|
+
/**
|
|
113
|
+
* Deletes a row: its roster row and per-row entry (quota, epoch; the identity
|
|
114
|
+
* lives in the roster row) in one config write, then its credential and
|
|
115
|
+
* runtime fields in one state write. The config goes first, so a crash
|
|
116
|
+
* between the two leaves a row every reader already sees as removed, with
|
|
117
|
+
* only an orphaned state entry that no reader loads; calling `remove` again
|
|
118
|
+
* drops that entry (`completed`). As with every id the store drops, the id is
|
|
119
|
+
* not reused by `add` in this process.
|
|
120
|
+
*/
|
|
121
|
+
export declare function removeRow(rt: StoreRuntime, id: string, options?: RemoveOptions): Promise<RemoveResult>;
|
|
50
122
|
export declare function recordRowIdentity(rt: StoreRuntime, id: string, identity: string, options?: RowOperationOptions): Promise<{
|
|
51
123
|
id: string;
|
|
52
124
|
disabled: string[];
|
package/dist/store/rows.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { assertNotInsideHook } from './hooks.js';
|
|
1
|
+
import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
2
2
|
import { DUPLICATE_IDENTITY_REASON, disableIdentityDuplicates, disableIn, recordIdentityIn, } from './identity.js';
|
|
3
|
-
import { runOperation, withTransaction } from './mutate.js';
|
|
3
|
+
import { notReadyError, readPool, runOperation, withTransaction, } from './mutate.js';
|
|
4
4
|
import { readRow, refusal, rowLockSpec, unknownRow, } from './runtime.js';
|
|
5
5
|
import { credentialProblem, fingerprintOf, idProblem, isRecord, rosterRowFor, rotationStamp, rowLockKey, stateFieldsFor, storedCredential, } from './schema.js';
|
|
6
6
|
/** Fields of a state entry that belong to the credential it replaces. */
|
|
@@ -78,6 +78,8 @@ export async function addRow(rt, input, options = {}) {
|
|
|
78
78
|
await locks.acquire(rowLockSpec(rt, { id, identity }));
|
|
79
79
|
if (credential.type === 'oauth')
|
|
80
80
|
await locks.acquire(options.providerLock ?? rt.providerLock);
|
|
81
|
+
for (const extra of options.extraLocks ?? [])
|
|
82
|
+
await locks.acquire(extra);
|
|
81
83
|
return withTransaction(ctx, locks, progress, { operation: 'add', rowId: id }, async (tx) => {
|
|
82
84
|
const rows = tx.rows();
|
|
83
85
|
const fingerprint = fingerprintOf(credential);
|
|
@@ -150,6 +152,8 @@ export async function replaceRow(rt, id, credential, input = {}, options = {}) {
|
|
|
150
152
|
await locks.acquire(rowLockSpec(rt, seen));
|
|
151
153
|
if (credential.type === 'oauth')
|
|
152
154
|
await locks.acquire(options.providerLock ?? rt.providerLock);
|
|
155
|
+
for (const extra of options.extraLocks ?? [])
|
|
156
|
+
await locks.acquire(extra);
|
|
153
157
|
return withTransaction(ctx, locks, progress, { operation: 'replace', rowId: id }, async (tx) => {
|
|
154
158
|
const row = requireUsableRow('replace', id, tx.row(id), credential);
|
|
155
159
|
if (rowLockKey(row) !== rowLockKey(seen))
|
|
@@ -197,6 +201,8 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
197
201
|
await locks.acquire(rowLockSpec(rt, seen));
|
|
198
202
|
if (input.identity !== undefined && credential.type === 'oauth')
|
|
199
203
|
await locks.acquire(options.providerLock ?? rt.providerLock);
|
|
204
|
+
for (const extra of options.extraLocks ?? [])
|
|
205
|
+
await locks.acquire(extra);
|
|
200
206
|
return withTransaction(ctx, locks, progress, { operation: 'rotate', rowId: id }, async (tx) => {
|
|
201
207
|
const row = requireUsableRow('rotate', id, tx.row(id), credential);
|
|
202
208
|
if (rowLockKey(row) !== rowLockKey(seen))
|
|
@@ -220,15 +226,134 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
220
226
|
});
|
|
221
227
|
});
|
|
222
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Marks a row disabled. Since 0.2.3 it takes the row lock and the caller's
|
|
231
|
+
* extra locks before the store locks, as the other row writes do, so it waits
|
|
232
|
+
* for a refresh of the row instead of landing during its provider call.
|
|
233
|
+
*/
|
|
223
234
|
export async function disableRow(rt, id, reason, options = {}) {
|
|
224
235
|
assertNotInsideHook('disable');
|
|
225
|
-
return runOperation(rt.ctx, 'disable', id, options.onFailure, async (locks, progress) =>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return { id }
|
|
231
|
-
|
|
236
|
+
return runOperation(rt.ctx, 'disable', id, options.onFailure, async (locks, progress) => {
|
|
237
|
+
const { row: seen } = await readRow(rt, 'disable', id);
|
|
238
|
+
await locks.acquire(rowLockSpec(rt, seen));
|
|
239
|
+
for (const extra of options.extraLocks ?? [])
|
|
240
|
+
await locks.acquire(extra);
|
|
241
|
+
return withTransaction(rt.ctx, locks, progress, { operation: 'disable', rowId: id }, async (tx) => {
|
|
242
|
+
const row = tx.row(id);
|
|
243
|
+
if (!row || !tx.rosterRow(id))
|
|
244
|
+
throw unknownRow('disable', id);
|
|
245
|
+
if (rowLockKey(row) !== rowLockKey(seen))
|
|
246
|
+
throw keyChanged('disable', id);
|
|
247
|
+
disableIn(tx, id, reason);
|
|
248
|
+
await tx.commitConfig();
|
|
249
|
+
return { id };
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Clears a row's `enabled: false` and its `disabledReason` in one config
|
|
255
|
+
* write. An OAuth row whose recorded identity another enabled OAuth row holds
|
|
256
|
+
* stays disabled and the call refuses (`duplicate-identity`): the same rule
|
|
257
|
+
* that makes `add` store such a row disabled. Enabling a row that is already
|
|
258
|
+
* enabled writes nothing.
|
|
259
|
+
*/
|
|
260
|
+
export async function enableRow(rt, id, options = {}) {
|
|
261
|
+
assertNotInsideHook('enable');
|
|
262
|
+
return runOperation(rt.ctx, 'enable', id, options.onFailure, async (locks, progress) => {
|
|
263
|
+
const { row: seen } = await readRow(rt, 'enable', id);
|
|
264
|
+
await locks.acquire(rowLockSpec(rt, seen));
|
|
265
|
+
for (const extra of options.extraLocks ?? [])
|
|
266
|
+
await locks.acquire(extra);
|
|
267
|
+
return withTransaction(rt.ctx, locks, progress, { operation: 'enable', rowId: id }, async (tx) => {
|
|
268
|
+
const row = requireUsableRow('enable', id, tx.row(id));
|
|
269
|
+
if (rowLockKey(row) !== rowLockKey(seen))
|
|
270
|
+
throw keyChanged('enable', id);
|
|
271
|
+
if (row.enabled && row.disabledReason === undefined)
|
|
272
|
+
return { id };
|
|
273
|
+
if (row.type === 'oauth' && row.identity !== undefined) {
|
|
274
|
+
const holder = tx
|
|
275
|
+
.rows()
|
|
276
|
+
.find((other) => other.id !== id &&
|
|
277
|
+
other.invalid === undefined &&
|
|
278
|
+
other.type === 'oauth' &&
|
|
279
|
+
other.enabled &&
|
|
280
|
+
other.identity === row.identity);
|
|
281
|
+
if (holder)
|
|
282
|
+
throw refusal('enable', id, 'duplicate-identity', `row ${holder.id} is enabled with the same identity as row ${id}`);
|
|
283
|
+
}
|
|
284
|
+
const raw = tx.rosterRow(id);
|
|
285
|
+
raw.enabled = true;
|
|
286
|
+
const entry = tx.entry(id);
|
|
287
|
+
if (entry && 'disabledReason' in entry) {
|
|
288
|
+
const next = { ...entry };
|
|
289
|
+
delete next.disabledReason;
|
|
290
|
+
tx.setEntry(id, next);
|
|
291
|
+
}
|
|
292
|
+
await tx.commitConfig();
|
|
293
|
+
return { id };
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Deletes a row: its roster row and per-row entry (quota, epoch; the identity
|
|
299
|
+
* lives in the roster row) in one config write, then its credential and
|
|
300
|
+
* runtime fields in one state write. The config goes first, so a crash
|
|
301
|
+
* between the two leaves a row every reader already sees as removed, with
|
|
302
|
+
* only an orphaned state entry that no reader loads; calling `remove` again
|
|
303
|
+
* drops that entry (`completed`). As with every id the store drops, the id is
|
|
304
|
+
* not reused by `add` in this process.
|
|
305
|
+
*/
|
|
306
|
+
export async function removeRow(rt, id, options = {}) {
|
|
307
|
+
assertNotInsideHook('remove');
|
|
308
|
+
const { ctx } = rt;
|
|
309
|
+
return runOperation(ctx, 'remove', id, options.onFailure, async (locks, progress) => {
|
|
310
|
+
// Only a non-string or empty id is refused: a roster row whose id the
|
|
311
|
+
// older readers would trim is invalid, and removing it is a repair.
|
|
312
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
313
|
+
throw refusal('remove', id, 'invalid-input', 'id must be non-empty');
|
|
314
|
+
const result = await readPool(ctx);
|
|
315
|
+
if (result.status !== 'ready')
|
|
316
|
+
throw notReadyError(result, 'remove', id);
|
|
317
|
+
const seen = result.rows.find((row) => row.id === id);
|
|
318
|
+
if (!seen && !hasStateAccount(result.state, id))
|
|
319
|
+
throw unknownRow('remove', id);
|
|
320
|
+
const seenKey = rowLockKey(seen ?? { id });
|
|
321
|
+
await locks.acquire(rowLockSpec(rt, seen ?? { id }));
|
|
322
|
+
for (const extra of options.extraLocks ?? [])
|
|
323
|
+
await locks.acquire(extra);
|
|
324
|
+
return withTransaction(ctx, locks, progress, { operation: 'remove', rowId: id }, async (tx) => {
|
|
325
|
+
const row = tx.row(id);
|
|
326
|
+
const orphan = hasStateAccount(tx.state, id);
|
|
327
|
+
if (!row && !orphan)
|
|
328
|
+
throw unknownRow('remove', id);
|
|
329
|
+
if (rowLockKey(row ?? { id }) !== seenKey)
|
|
330
|
+
throw keyChanged('remove', id);
|
|
331
|
+
const protect = options.protect;
|
|
332
|
+
if (protect) {
|
|
333
|
+
const view = {
|
|
334
|
+
row,
|
|
335
|
+
config: tx.snapshot.config,
|
|
336
|
+
state: tx.snapshot.state,
|
|
337
|
+
};
|
|
338
|
+
const reason = await runInsideHook('remove', () => protect(id, view));
|
|
339
|
+
if (reason !== undefined)
|
|
340
|
+
throw refusal('remove', id, 'row-protected', reason);
|
|
341
|
+
}
|
|
342
|
+
if (row) {
|
|
343
|
+
tx.dropRosterRows(id);
|
|
344
|
+
await tx.commitConfig();
|
|
345
|
+
}
|
|
346
|
+
if (orphan) {
|
|
347
|
+
tx.dropStateAccount(id);
|
|
348
|
+
await tx.commitState();
|
|
349
|
+
}
|
|
350
|
+
return { id, outcome: row ? 'removed' : 'completed' };
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function hasStateAccount(state, id) {
|
|
355
|
+
const accounts = state.accounts;
|
|
356
|
+
return isRecord(accounts) && Object.hasOwn(accounts, id);
|
|
232
357
|
}
|
|
233
358
|
export async function recordRowIdentity(rt, id, identity, options = {}) {
|
|
234
359
|
assertNotInsideHook('recordIdentity');
|
|
@@ -238,6 +363,8 @@ export async function recordRowIdentity(rt, id, identity, options = {}) {
|
|
|
238
363
|
const { row: seen } = await readRow(rt, 'recordIdentity', id);
|
|
239
364
|
await locks.acquire(rowLockSpec(rt, seen));
|
|
240
365
|
await locks.acquire(options.providerLock ?? rt.providerLock);
|
|
366
|
+
for (const extra of options.extraLocks ?? [])
|
|
367
|
+
await locks.acquire(extra);
|
|
241
368
|
return withTransaction(rt.ctx, locks, progress, { operation: 'recordIdentity', rowId: id }, async (tx) => {
|
|
242
369
|
const row = requireUsableRow('recordIdentity', id, tx.row(id));
|
|
243
370
|
if (rowLockKey(row) !== rowLockKey(seen))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/common-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Shared plumbing for the CortexKit auth plugins: loopback RPC, file locks and atomic writes, logger, sidebar state file, TUI preferences and TUI build.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|