@cortexkit/common-auth 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/store/identity.d.ts +5 -0
- package/dist/store/identity.js +16 -0
- package/dist/store/index.d.ts +3 -2
- package/dist/store/index.js +1 -0
- package/dist/store/pool.d.ts +8 -9
- package/dist/store/provider-state.d.ts +45 -0
- package/dist/store/provider-state.js +80 -43
- package/dist/store/rows.d.ts +44 -10
- package/dist/store/rows.js +136 -53
- package/dist/store/schema.d.ts +5 -3
- package/dist/store/torn.d.ts +29 -0
- package/dist/store/torn.js +72 -1
- package/package.json +1 -1
package/dist/store/identity.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export declare function countUnknownIdentityRows(rows: readonly PoolRow[]): numb
|
|
|
22
22
|
* without an entry gets one at epoch 1. Nothing is ever deleted.
|
|
23
23
|
*/
|
|
24
24
|
export declare function disableIn(tx: RowEditor, id: string, reason: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Marks a row enabled: `enabled: true` in the roster row and no
|
|
27
|
+
* `disabledReason` in its entry. A row without an entry is not given one.
|
|
28
|
+
*/
|
|
29
|
+
export declare function enableIn(tx: RowEditor, id: string): void;
|
|
25
30
|
/**
|
|
26
31
|
* Two enabled OAuth rows with one wire identity are the same account: the
|
|
27
32
|
* earlier row in roster order stays enabled and every later one is disabled
|
package/dist/store/identity.js
CHANGED
|
@@ -24,6 +24,22 @@ export function disableIn(tx, id, reason) {
|
|
|
24
24
|
const entry = tx.entry(id) ?? { credentialEpoch: 1, needsFirstReading: true };
|
|
25
25
|
tx.setEntry(id, { ...entry, disabledReason: reason });
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Marks a row enabled: `enabled: true` in the roster row and no
|
|
29
|
+
* `disabledReason` in its entry. A row without an entry is not given one.
|
|
30
|
+
*/
|
|
31
|
+
export function enableIn(tx, id) {
|
|
32
|
+
const raw = tx.rosterRow(id);
|
|
33
|
+
if (!raw)
|
|
34
|
+
return;
|
|
35
|
+
raw.enabled = true;
|
|
36
|
+
const entry = tx.entry(id);
|
|
37
|
+
if (entry && 'disabledReason' in entry) {
|
|
38
|
+
const next = { ...entry };
|
|
39
|
+
delete next.disabledReason;
|
|
40
|
+
tx.setEntry(id, next);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
27
43
|
/**
|
|
28
44
|
* Two enabled OAuth rows with one wire identity are the same account: the
|
|
29
45
|
* earlier row in roster order stays enabled and every later one is disabled
|
package/dist/store/index.d.ts
CHANGED
|
@@ -6,12 +6,13 @@ 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
|
+
export type { ProviderStateMutator, RowTransitionMutator, UpdateProviderStateResult, } from './provider-state.js';
|
|
10
|
+
export { DECLINE_TRANSITION } from './provider-state.js';
|
|
10
11
|
export type { PullHook, PullRequest } from './pull.js';
|
|
11
12
|
export type { ProviderRefresh, ProviderRefreshResult, RefreshOptions, RefreshOutcome, } from './refresh.js';
|
|
12
13
|
export type { LockEvent, PoolLockOptions, PoolLockSpec, } from './refresh-lock.js';
|
|
13
14
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
14
|
-
export type { AddInput, AddResult, CredentialWriteInput, 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';
|
|
15
16
|
export type { PullReason } from './runtime.js';
|
|
16
17
|
export type { ApiKeyCredential, CredentialStampStatus, OAuthCredential, PoolCredential, PoolRow, ProviderStateCodec, ProviderStateDrop, ProviderStateReplacement, QuotaCodec, RotateCredential, StoredCredential, } from './schema.js';
|
|
17
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';
|
package/dist/store/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { PoolOperationError, PoolReentryError } from './errors.js';
|
|
2
2
|
export { countUnknownIdentityRows, DUPLICATE_IDENTITY_REASON, } from './identity.js';
|
|
3
3
|
export { openPoolStore } from './pool.js';
|
|
4
|
+
export { DECLINE_TRANSITION } from './provider-state.js';
|
|
4
5
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
5
6
|
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, PROVIDER_STATE_KEY, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
|
6
7
|
export { POOL_OWNED_KEYS } from './settings.js';
|
package/dist/store/pool.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type ProviderStateMutator, type UpdateProviderStateResult } from './pro
|
|
|
6
6
|
import { type PullHook } from './pull.js';
|
|
7
7
|
import { type ProviderRefresh, type RefreshOptions, type RefreshOutcome } from './refresh.js';
|
|
8
8
|
import { type LockEnvironment, type PoolLockOptions, type PoolLockSpec } from './refresh-lock.js';
|
|
9
|
-
import { type AddInput, type AddResult, type CredentialWriteInput, type RemoveOptions, type RemoveResult, type ReorderOptions, type ReorderResult, type RowOperationOptions, type RowToggleOptions } from './rows.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
10
|
import { type PoolCredential, type PoolRow, type ProviderStateCodec, type QuotaCodec, type RotateCredential, type StoredCredential } from './schema.js';
|
|
11
11
|
import { type SettingsMutator, type SettingsRead, type UpdateSettingsOptions, type UpdateSettingsResult } from './settings.js';
|
|
12
12
|
export interface OpenPoolStoreOptions {
|
|
@@ -119,19 +119,18 @@ export interface PoolStore {
|
|
|
119
119
|
/**
|
|
120
120
|
* Sets `enabled: false` and the entry's `disabledReason`. Takes the row
|
|
121
121
|
* lock, then `extraLocks`, then the store locks (the row lock and
|
|
122
|
-
* `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`.
|
|
123
126
|
*/
|
|
124
|
-
disable(id: string, reason: string, options?:
|
|
125
|
-
id: string;
|
|
126
|
-
}>;
|
|
127
|
+
disable(id: string, reason: string, options?: RowTransitionOptions): Promise<RowTransitionResult>;
|
|
127
128
|
/**
|
|
128
129
|
* Clears `enabled: false` and `disabledReason` (since 0.2.3); refuses with
|
|
129
130
|
* `duplicate-identity` when another enabled OAuth row holds the row's
|
|
130
|
-
* identity. Locks as `disable
|
|
131
|
+
* identity. Locks as `disable`, and takes the same options since 0.7.0.
|
|
131
132
|
*/
|
|
132
|
-
enable(id: string, options?:
|
|
133
|
-
id: string;
|
|
134
|
-
}>;
|
|
133
|
+
enable(id: string, options?: RowTransitionOptions): Promise<RowTransitionResult>;
|
|
135
134
|
/**
|
|
136
135
|
* Deletes the roster row, its per-row entry and its state-file credential
|
|
137
136
|
* (since 0.2.3). Locks as `disable`; `protect` can refuse the id.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Attribution } from './attribution.js';
|
|
2
2
|
import type { PoolOperation } from './errors.js';
|
|
3
|
+
import { type Transaction } from './mutate.js';
|
|
3
4
|
import type { RowToggleOptions } from './rows.js';
|
|
4
5
|
import { type StoreRuntime } from './runtime.js';
|
|
5
6
|
import { type PoolRow, type ProviderStateCodec } from './schema.js';
|
|
@@ -84,3 +85,47 @@ export type UpdateProviderStateResult = {
|
|
|
84
85
|
* `replace` stamps such a row.
|
|
85
86
|
*/
|
|
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>;
|
|
@@ -132,55 +132,92 @@ export async function updateProviderStateRow(rt, id, fence, mutator, options = {
|
|
|
132
132
|
const epoch = row.credentialEpoch ?? 1;
|
|
133
133
|
if (epoch !== captured || row.identity !== fence.identity)
|
|
134
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
|
|
136
|
-
|
|
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))
|
|
135
|
+
const plan = await planProviderStateIn(tx, codec, operation, row, mutator);
|
|
136
|
+
if (plan.kind !== 'changed')
|
|
156
137
|
return {
|
|
157
138
|
id,
|
|
158
139
|
outcome: 'unchanged',
|
|
159
|
-
...(
|
|
140
|
+
...(plan.kind === 'unchanged' && plan.value !== undefined
|
|
141
|
+
? { providerState: plan.value }
|
|
142
|
+
: {}),
|
|
160
143
|
};
|
|
161
|
-
|
|
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);
|
|
144
|
+
tx.setStateAccount(id, plan.account);
|
|
180
145
|
await tx.commitState();
|
|
181
|
-
return
|
|
146
|
+
return plan.value === undefined
|
|
182
147
|
? { id, outcome: 'cleared' }
|
|
183
|
-
: { id, outcome: 'updated', providerState:
|
|
148
|
+
: { id, outcome: 'updated', providerState: plan.value };
|
|
184
149
|
});
|
|
185
150
|
});
|
|
186
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Returned by the provider-state mutator of an attributed `disable` or
|
|
154
|
+
* `enable` to decline the whole transition: nothing is written, neither the
|
|
155
|
+
* provider state nor the row's enabled flag, and the call resolves with
|
|
156
|
+
* `declined: true`. A mutator declines when the state it is shown is newer
|
|
157
|
+
* than what its caller saw, such as an eligibility recorded after the
|
|
158
|
+
* request whose refusal is being acted on. It is a value of its own because
|
|
159
|
+
* `undefined` already means "clear the provider state". `Symbol.for` keeps it
|
|
160
|
+
* equal across two copies of this module loaded in one process.
|
|
161
|
+
*/
|
|
162
|
+
export const DECLINE_TRANSITION = Symbol.for('@cortexkit/common-auth/store/decline-transition');
|
|
163
|
+
/**
|
|
164
|
+
* Runs a provider-state mutator for a row loaded under every lock and
|
|
165
|
+
* already checked by the caller (present, valid, inside its attribution
|
|
166
|
+
* fence), and plans the write. Refuses (`no-credential`) a row holding no
|
|
167
|
+
* credential, and (`unbound-credential`) one whose credential carries no
|
|
168
|
+
* stamp of this store at the row's epoch and identity: no stamp could bind
|
|
169
|
+
* the value, so no reader would ever show it. `DECLINE_TRANSITION` is
|
|
170
|
+
* honoured only when `declinable` is set; elsewhere it is not JSON and is
|
|
171
|
+
* refused as such.
|
|
172
|
+
*/
|
|
173
|
+
export async function planProviderStateIn(tx, codec, operation, row, mutator, declinable = false) {
|
|
174
|
+
const id = row.id;
|
|
175
|
+
const credential = row.credential;
|
|
176
|
+
if (!credential)
|
|
177
|
+
throw refusal(operation, id, 'no-credential', `row ${id} holds no credential`);
|
|
178
|
+
const epoch = row.credentialEpoch ?? 1;
|
|
179
|
+
const account = tx.stateAccount(id);
|
|
180
|
+
const rawStamp = account?.[CREDENTIAL_STAMP_KEY];
|
|
181
|
+
const stamp = parseStamp(rawStamp);
|
|
182
|
+
if (!isRecord(rawStamp) ||
|
|
183
|
+
!stamp?.binding ||
|
|
184
|
+
stamp.digest !== credentialDigest(credential) ||
|
|
185
|
+
stamp.credentialEpoch !== epoch ||
|
|
186
|
+
stamp.binding.identity !== row.identity)
|
|
187
|
+
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`);
|
|
188
|
+
const current = row.providerState === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: structuredClone(row.providerState);
|
|
191
|
+
const returned = await runInsideHook(operation, () => mutator(current, row));
|
|
192
|
+
if (declinable && returned === DECLINE_TRANSITION)
|
|
193
|
+
return { kind: 'declined' };
|
|
194
|
+
const next = returned === undefined
|
|
195
|
+
? undefined
|
|
196
|
+
: acceptProviderState(codec, operation, id, returned, 'the provider state the mutator returned');
|
|
197
|
+
const held = account !== undefined && Object.hasOwn(account, PROVIDER_STATE_KEY);
|
|
198
|
+
if (next === undefined
|
|
199
|
+
? !held
|
|
200
|
+
: row.providerState !== undefined &&
|
|
201
|
+
JSON.stringify(row.providerState) === JSON.stringify(next))
|
|
202
|
+
return { kind: 'unchanged', ...(next !== undefined ? { value: next } : {}) };
|
|
203
|
+
const nextAccount = { ...account };
|
|
204
|
+
if (next === undefined) {
|
|
205
|
+
delete nextAccount[PROVIDER_STATE_KEY];
|
|
206
|
+
const nextStamp = { ...rawStamp };
|
|
207
|
+
delete nextStamp.providerState;
|
|
208
|
+
nextAccount[CREDENTIAL_STAMP_KEY] = nextStamp;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
nextAccount[PROVIDER_STATE_KEY] = next;
|
|
212
|
+
const digest = providerStateDigest(codec, next);
|
|
213
|
+
// A change confined to the part the codec does not bind to the
|
|
214
|
+
// credential leaves the stamp exactly as it was.
|
|
215
|
+
if (stamp.providerState !== digest)
|
|
216
|
+
nextAccount[CREDENTIAL_STAMP_KEY] = { ...rawStamp, providerState: digest };
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
kind: 'changed',
|
|
220
|
+
...(next !== undefined ? { value: next } : {}),
|
|
221
|
+
account: nextAccount,
|
|
222
|
+
};
|
|
223
|
+
}
|
package/dist/store/rows.d.ts
CHANGED
|
@@ -1,7 +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
|
+
import { type ProviderStateWrite, type RowTransitionMutator, type UpdateProviderStateResult } from './provider-state.js';
|
|
5
5
|
import type { PoolLockSpec } from './refresh-lock.js';
|
|
6
6
|
import { type StoreRuntime } from './runtime.js';
|
|
7
7
|
import { type CredentialBinding, type PoolCredential, type PoolRow, type RotateCredential, type StoredCredential } from './schema.js';
|
|
@@ -134,23 +134,57 @@ export declare function rotateRow(rt: StoreRuntime, id: string, credential: Rota
|
|
|
134
134
|
credential: StoredCredential;
|
|
135
135
|
}>;
|
|
136
136
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* for a refresh of the row instead of landing during its provider call.
|
|
137
|
+
* Options of `disable` and `enable`. A call that passes neither
|
|
138
|
+
* `attribution` nor `providerState` behaves exactly as it did before 0.7.0.
|
|
140
139
|
*/
|
|
141
|
-
export
|
|
140
|
+
export interface RowTransitionOptions extends RowToggleOptions {
|
|
141
|
+
/**
|
|
142
|
+
* The credential epoch and recorded identity the caller's evidence for the
|
|
143
|
+
* transition was obtained under (as `recordQuota`'s attribution: an
|
|
144
|
+
* identity left out means the row had none). The call is refused
|
|
145
|
+
* (`attribution`, retryable, nothing written) once the row holds another
|
|
146
|
+
* epoch or identity, so a provider's late answer about a replaced
|
|
147
|
+
* credential never disables, or switches back on, the row now holding its
|
|
148
|
+
* successor.
|
|
149
|
+
*/
|
|
150
|
+
attribution?: Attribution;
|
|
151
|
+
/**
|
|
152
|
+
* A provider-state change made in the same transaction as the transition,
|
|
153
|
+
* under the rules of `updateProviderState` (codec validation, the stamp
|
|
154
|
+
* rebound to the value, `unbound-credential` for a row no stamp of this
|
|
155
|
+
* store can bind it to); it requires `attribution`. The value and the
|
|
156
|
+
* enabled flag land together: no reader, and no crash at any write point,
|
|
157
|
+
* shows one without the other. Returning `DECLINE_TRANSITION` declines the
|
|
158
|
+
* whole call and writes nothing.
|
|
159
|
+
*/
|
|
160
|
+
providerState?: RowTransitionMutator;
|
|
161
|
+
}
|
|
162
|
+
export interface RowTransitionResult {
|
|
142
163
|
id: string;
|
|
143
|
-
|
|
164
|
+
/** The provider-state mutator declined: nothing was written. */
|
|
165
|
+
declined?: true;
|
|
166
|
+
/**
|
|
167
|
+
* Set when a provider-state mutator ran and did not decline: what it did
|
|
168
|
+
* to the value, as `updateProviderState` reports it.
|
|
169
|
+
*/
|
|
170
|
+
providerStateOutcome?: UpdateProviderStateResult['outcome'];
|
|
171
|
+
/** The provider state the row now holds, when a mutator ran and left one. */
|
|
172
|
+
providerState?: unknown;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Marks a row disabled with a reason. See `RowTransitionOptions` for the
|
|
176
|
+
* attributed form, which may change the provider state with it.
|
|
177
|
+
*/
|
|
178
|
+
export declare function disableRow(rt: StoreRuntime, id: string, reason: string, options?: RowTransitionOptions): Promise<RowTransitionResult>;
|
|
144
179
|
/**
|
|
145
180
|
* Clears a row's `enabled: false` and its `disabledReason` in one config
|
|
146
181
|
* write. An OAuth row whose recorded identity another enabled OAuth row holds
|
|
147
182
|
* stays disabled and the call refuses (`duplicate-identity`): the same rule
|
|
148
183
|
* that makes `add` store such a row disabled. Enabling a row that is already
|
|
149
|
-
* enabled writes nothing.
|
|
184
|
+
* enabled writes nothing. See `RowTransitionOptions` for the attributed
|
|
185
|
+
* form, which may change the provider state with it.
|
|
150
186
|
*/
|
|
151
|
-
export declare function enableRow(rt: StoreRuntime, id: string, options?:
|
|
152
|
-
id: string;
|
|
153
|
-
}>;
|
|
187
|
+
export declare function enableRow(rt: StoreRuntime, id: string, options?: RowTransitionOptions): Promise<RowTransitionResult>;
|
|
154
188
|
/**
|
|
155
189
|
* Deletes a row: its roster row and per-row entry (quota, epoch; the identity
|
|
156
190
|
* lives in the roster row) in one config write, then its credential and
|
package/dist/store/rows.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { PoolOperationError } from './errors.js';
|
|
2
3
|
import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
3
|
-
import { DUPLICATE_IDENTITY_REASON, disableIdentityDuplicates, disableIn, recordIdentityIn, } from './identity.js';
|
|
4
|
+
import { DUPLICATE_IDENTITY_REASON, disableIdentityDuplicates, disableIn, enableIn, recordIdentityIn, } from './identity.js';
|
|
4
5
|
import { notReadyError, readPool, runOperation, withTransaction, } from './mutate.js';
|
|
5
|
-
import { acceptProviderState, mergedProviderState, providerStateCoverage, replacementProviderState, } from './provider-state.js';
|
|
6
|
+
import { acceptProviderState, mergedProviderState, planProviderStateIn, providerStateCoverage, replacementProviderState, } from './provider-state.js';
|
|
6
7
|
import { readRow, refusal, requireBound, rowLockSpec, unknownRow, } from './runtime.js';
|
|
7
8
|
import { boundProviderStateDigest, CREDENTIAL_STAMP_KEY, credentialProblem, fingerprintOf, idProblem, isCredentialEpoch, isRecord, PROVIDER_STATE_KEY, rosterRowFor, rotationStamp, rowLockKey, stampFor, stateFieldsFor, storedCredential, } from './schema.js';
|
|
8
|
-
import { bindReplacement } from './torn.js';
|
|
9
|
+
import { applyTransition, bindReplacement, TRANSITION_STAMP_KEY, } from './torn.js';
|
|
9
10
|
/** Fields of a state entry that belong to the credential it replaces. */
|
|
10
11
|
const CREDENTIAL_STATE_FIELDS = [
|
|
11
12
|
'access',
|
|
@@ -420,72 +421,154 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
420
421
|
});
|
|
421
422
|
}
|
|
422
423
|
/**
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
424
|
+
* `disable` and `enable` in one place. Takes the row lock, then the extra
|
|
425
|
+
* locks, then the store locks, as every row write does, so it waits for a
|
|
426
|
+
* refresh of the row instead of landing during its provider call.
|
|
427
|
+
*
|
|
428
|
+
* With a provider-state mutator that changes the value, the transition is
|
|
429
|
+
* written as a replace is: the state file first, carrying the value and, in
|
|
430
|
+
* the stamp, the transition itself; then the config, flipping the row and
|
|
431
|
+
* recording the transition's mark (see `torn.ts`). A stop between the two
|
|
432
|
+
* leaves a row every reader shows transitioned beside its new value. When
|
|
433
|
+
* the config already says what the transition would write (an `enable` of
|
|
434
|
+
* an enabled row), the value alone is written, in one state write.
|
|
426
435
|
*/
|
|
427
|
-
|
|
428
|
-
assertNotInsideHook(
|
|
429
|
-
|
|
430
|
-
|
|
436
|
+
async function transitionRow(rt, operation, id, flag, options) {
|
|
437
|
+
assertNotInsideHook(operation);
|
|
438
|
+
const { ctx } = rt;
|
|
439
|
+
const codec = ctx.providerState;
|
|
440
|
+
const fence = options.attribution;
|
|
441
|
+
const mutator = options.providerState;
|
|
442
|
+
return runOperation(ctx, operation, id, options.onFailure, async (locks, progress) => {
|
|
443
|
+
if (fence !== undefined &&
|
|
444
|
+
!isCredentialEpoch(isRecord(fence) ? fence.credentialEpoch : undefined))
|
|
445
|
+
throw refusal(operation, id, 'invalid-input', 'the attribution must name a credential epoch that is a positive safe integer');
|
|
446
|
+
if (mutator !== undefined) {
|
|
447
|
+
if (typeof mutator !== 'function')
|
|
448
|
+
throw refusal(operation, id, 'invalid-input', 'the provider-state mutator must be a function');
|
|
449
|
+
// A provider-state value belongs to one credential, so a change to
|
|
450
|
+
// it must say which credential it was decided for.
|
|
451
|
+
if (fence === undefined)
|
|
452
|
+
throw refusal(operation, id, 'invalid-input', 'a provider-state change needs the attribution of the credential it is for');
|
|
453
|
+
if (!codec)
|
|
454
|
+
throw refusal(operation, id, 'invalid-input', 'the store was opened without a provider-state codec');
|
|
455
|
+
}
|
|
456
|
+
const { row: seen } = await readRow(rt, operation, id);
|
|
431
457
|
await locks.acquire(rowLockSpec(rt, seen));
|
|
432
458
|
for (const extra of options.extraLocks ?? [])
|
|
433
459
|
await locks.acquire(extra);
|
|
434
|
-
return withTransaction(
|
|
435
|
-
const
|
|
460
|
+
return withTransaction(ctx, locks, progress, { operation, rowId: id }, async (tx) => {
|
|
461
|
+
const loaded = tx.row(id);
|
|
462
|
+
const row = flag.enabled
|
|
463
|
+
? requireUsableRow('enable', id, loaded)
|
|
464
|
+
: loaded;
|
|
436
465
|
if (!row || !tx.rosterRow(id))
|
|
437
|
-
throw unknownRow(
|
|
466
|
+
throw unknownRow(operation, id);
|
|
438
467
|
if (rowLockKey(row) !== rowLockKey(seen))
|
|
439
|
-
throw keyChanged(
|
|
440
|
-
|
|
468
|
+
throw keyChanged(operation, id);
|
|
469
|
+
if (fence !== undefined) {
|
|
470
|
+
// An invalid entry has no epoch to compare the fence with.
|
|
471
|
+
if (row.invalid)
|
|
472
|
+
throw refusal(operation, id, 'invalid-row', `row ${id} failed validation`);
|
|
473
|
+
if ((row.credentialEpoch ?? 1) !== fence.credentialEpoch ||
|
|
474
|
+
row.identity !== fence.identity)
|
|
475
|
+
throw refusal(operation, id, 'attribution', `the ${operation} of ${id} was issued for a credential or account the row no longer holds`, true);
|
|
476
|
+
}
|
|
477
|
+
// An enable of a row that is already enabled has nothing to write
|
|
478
|
+
// to the config; a disable always rewrites it, as it always has.
|
|
479
|
+
const writesConfig = !flag.enabled || !row.enabled || row.disabledReason !== undefined;
|
|
480
|
+
if (flag.enabled && writesConfig && row.type === 'oauth') {
|
|
481
|
+
const holder = row.identity === undefined
|
|
482
|
+
? undefined
|
|
483
|
+
: tx
|
|
484
|
+
.rows()
|
|
485
|
+
.find((other) => other.id !== id &&
|
|
486
|
+
other.invalid === undefined &&
|
|
487
|
+
other.type === 'oauth' &&
|
|
488
|
+
other.enabled &&
|
|
489
|
+
other.identity === row.identity);
|
|
490
|
+
if (holder)
|
|
491
|
+
throw refusal('enable', id, 'duplicate-identity', `row ${holder.id} is enabled with the same identity as row ${id}`);
|
|
492
|
+
}
|
|
493
|
+
if (mutator === undefined || codec === undefined) {
|
|
494
|
+
if (!writesConfig)
|
|
495
|
+
return { id };
|
|
496
|
+
if (flag.enabled)
|
|
497
|
+
enableIn(tx, id);
|
|
498
|
+
else
|
|
499
|
+
disableIn(tx, id, flag.reason);
|
|
500
|
+
await tx.commitConfig();
|
|
501
|
+
return { id };
|
|
502
|
+
}
|
|
503
|
+
if (!row.credential)
|
|
504
|
+
throw refusal(operation, id, 'no-credential', `row ${id} holds no credential`);
|
|
505
|
+
// A strict store refuses here, but not for an attribution alone:
|
|
506
|
+
// disabling or enabling a row whose credential the store cannot
|
|
507
|
+
// prove is harmless (an unbound row is never a candidate), while
|
|
508
|
+
// writing a provider state would vouch for that credential.
|
|
509
|
+
requireBound(operation, row);
|
|
510
|
+
const plan = await planProviderStateIn(tx, codec, operation, row, mutator, true);
|
|
511
|
+
if (plan.kind === 'declined')
|
|
512
|
+
return { id, declined: true };
|
|
513
|
+
const result = {
|
|
514
|
+
id,
|
|
515
|
+
providerStateOutcome: plan.kind === 'unchanged'
|
|
516
|
+
? 'unchanged'
|
|
517
|
+
: plan.value === undefined
|
|
518
|
+
? 'cleared'
|
|
519
|
+
: 'updated',
|
|
520
|
+
...(plan.value !== undefined ? { providerState: plan.value } : {}),
|
|
521
|
+
};
|
|
522
|
+
if (plan.kind === 'unchanged') {
|
|
523
|
+
if (writesConfig) {
|
|
524
|
+
if (flag.enabled)
|
|
525
|
+
enableIn(tx, id);
|
|
526
|
+
else
|
|
527
|
+
disableIn(tx, id, flag.reason);
|
|
528
|
+
await tx.commitConfig();
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
if (!writesConfig) {
|
|
533
|
+
tx.setStateAccount(id, plan.account);
|
|
534
|
+
await tx.commitState();
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
const transition = flag.enabled
|
|
538
|
+
? { mark: randomUUID(), enabled: true }
|
|
539
|
+
: { mark: randomUUID(), enabled: false, reason: flag.reason };
|
|
540
|
+
const stamp = plan.account[CREDENTIAL_STAMP_KEY];
|
|
541
|
+
tx.setStateAccount(id, {
|
|
542
|
+
...plan.account,
|
|
543
|
+
[CREDENTIAL_STAMP_KEY]: {
|
|
544
|
+
...stamp,
|
|
545
|
+
[TRANSITION_STAMP_KEY]: transition,
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
await tx.commitState();
|
|
549
|
+
applyTransition(tx, id, transition);
|
|
441
550
|
await tx.commitConfig();
|
|
442
|
-
return
|
|
551
|
+
return result;
|
|
443
552
|
});
|
|
444
553
|
});
|
|
445
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Marks a row disabled with a reason. See `RowTransitionOptions` for the
|
|
557
|
+
* attributed form, which may change the provider state with it.
|
|
558
|
+
*/
|
|
559
|
+
export function disableRow(rt, id, reason, options = {}) {
|
|
560
|
+
return transitionRow(rt, 'disable', id, { enabled: false, reason }, options);
|
|
561
|
+
}
|
|
446
562
|
/**
|
|
447
563
|
* Clears a row's `enabled: false` and its `disabledReason` in one config
|
|
448
564
|
* write. An OAuth row whose recorded identity another enabled OAuth row holds
|
|
449
565
|
* stays disabled and the call refuses (`duplicate-identity`): the same rule
|
|
450
566
|
* that makes `add` store such a row disabled. Enabling a row that is already
|
|
451
|
-
* enabled writes nothing.
|
|
567
|
+
* enabled writes nothing. See `RowTransitionOptions` for the attributed
|
|
568
|
+
* form, which may change the provider state with it.
|
|
452
569
|
*/
|
|
453
|
-
export
|
|
454
|
-
|
|
455
|
-
return runOperation(rt.ctx, 'enable', id, options.onFailure, async (locks, progress) => {
|
|
456
|
-
const { row: seen } = await readRow(rt, 'enable', id);
|
|
457
|
-
await locks.acquire(rowLockSpec(rt, seen));
|
|
458
|
-
for (const extra of options.extraLocks ?? [])
|
|
459
|
-
await locks.acquire(extra);
|
|
460
|
-
return withTransaction(rt.ctx, locks, progress, { operation: 'enable', rowId: id }, async (tx) => {
|
|
461
|
-
const row = requireUsableRow('enable', id, tx.row(id));
|
|
462
|
-
if (rowLockKey(row) !== rowLockKey(seen))
|
|
463
|
-
throw keyChanged('enable', id);
|
|
464
|
-
if (row.enabled && row.disabledReason === undefined)
|
|
465
|
-
return { id };
|
|
466
|
-
if (row.type === 'oauth' && row.identity !== undefined) {
|
|
467
|
-
const holder = tx
|
|
468
|
-
.rows()
|
|
469
|
-
.find((other) => other.id !== id &&
|
|
470
|
-
other.invalid === undefined &&
|
|
471
|
-
other.type === 'oauth' &&
|
|
472
|
-
other.enabled &&
|
|
473
|
-
other.identity === row.identity);
|
|
474
|
-
if (holder)
|
|
475
|
-
throw refusal('enable', id, 'duplicate-identity', `row ${holder.id} is enabled with the same identity as row ${id}`);
|
|
476
|
-
}
|
|
477
|
-
const raw = tx.rosterRow(id);
|
|
478
|
-
raw.enabled = true;
|
|
479
|
-
const entry = tx.entry(id);
|
|
480
|
-
if (entry && 'disabledReason' in entry) {
|
|
481
|
-
const next = { ...entry };
|
|
482
|
-
delete next.disabledReason;
|
|
483
|
-
tx.setEntry(id, next);
|
|
484
|
-
}
|
|
485
|
-
await tx.commitConfig();
|
|
486
|
-
return { id };
|
|
487
|
-
});
|
|
488
|
-
});
|
|
570
|
+
export function enableRow(rt, id, options = {}) {
|
|
571
|
+
return transitionRow(rt, 'enable', id, { enabled: true }, options);
|
|
489
572
|
}
|
|
490
573
|
/**
|
|
491
574
|
* Deletes a row: its roster row and per-row entry (quota, epoch; the identity
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -168,9 +168,11 @@ export interface PoolRow {
|
|
|
168
168
|
* it belongs to, and the config still holds the replaced row. Also set when
|
|
169
169
|
* a write that gives a row its first identity (`recordIdentity`, or a
|
|
170
170
|
* `rotate` or refresh that learns one) stopped after stamping the identity
|
|
171
|
-
* and before recording it in the config
|
|
172
|
-
*
|
|
173
|
-
*
|
|
171
|
+
* and before recording it in the config, and when an attributed `disable`
|
|
172
|
+
* or `enable` that changed the provider state stopped after its state
|
|
173
|
+
* write and before flipping the row in the config. The row is shown as the
|
|
174
|
+
* write leaves it once completed, is never a candidate, and the next store
|
|
175
|
+
* write on it writes the config to match.
|
|
174
176
|
*/
|
|
175
177
|
torn?: true;
|
|
176
178
|
/**
|
package/dist/store/torn.d.ts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { type RowEditor } from './identity.js';
|
|
2
2
|
import { type CredentialBinding, type CredentialStamp, type PoolRow, type ProviderStateCodec, type QuotaCodec } from './schema.js';
|
|
3
|
+
/** Key, inside a credential stamp, of an attributed enable or disable. */
|
|
4
|
+
export declare const TRANSITION_STAMP_KEY = "transition";
|
|
5
|
+
/**
|
|
6
|
+
* Key, inside a per-row config entry, of the mark of the last transition the
|
|
7
|
+
* config carries out. Older readers ignore it.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TRANSITION_MARK_KEY = "transitionMark";
|
|
10
|
+
/**
|
|
11
|
+
* An attributed enable or disable as its state write records it: `mark` is
|
|
12
|
+
* unique to that write, `enabled` is the flag it sets, and `reason` the
|
|
13
|
+
* disabled reason (a disable's only).
|
|
14
|
+
*/
|
|
15
|
+
export interface StampedTransition {
|
|
16
|
+
mark: string;
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Carries a transition out in a config being edited: the row's enabled flag
|
|
22
|
+
* and reason as the transition says, and its mark recorded in the entry (a
|
|
23
|
+
* row without an entry gets one at epoch 1, as `disableIn` gives it). An
|
|
24
|
+
* enable then applies the duplicate-identity rule, as every write that
|
|
25
|
+
* enables an OAuth row with a known identity does: the earlier row in roster
|
|
26
|
+
* order keeps the identity.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyTransition(editor: RowEditor, id: string, transition: StampedTransition): void;
|
|
3
29
|
/** How a row left between the two writes of an operation is completed. */
|
|
4
30
|
export type TornCompletion = {
|
|
5
31
|
kind: 'replace';
|
|
@@ -9,6 +35,9 @@ export type TornCompletion = {
|
|
|
9
35
|
} | {
|
|
10
36
|
kind: 'identity';
|
|
11
37
|
identity: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'transition';
|
|
40
|
+
transition: StampedTransition;
|
|
12
41
|
};
|
|
13
42
|
/** Rows left between the two writes of an operation, by row id. */
|
|
14
43
|
export declare function tornStamps(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec, options?: {
|
package/dist/store/torn.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { disableIdentityDuplicates, recordIdentityIn, } from './identity.js';
|
|
1
|
+
import { disableIdentityDuplicates, disableIn, enableIn, recordIdentityIn, } from './identity.js';
|
|
2
2
|
import { buildRawRows, CREDENTIAL_STAMP_KEY, credentialDigest, dispatchDigest, entryIn, isRecord, parseStamp, rosterRowIn, setEntryIn, } from './schema.js';
|
|
3
3
|
/*
|
|
4
4
|
* A replace changes both files: the state file gets the new credential and
|
|
@@ -46,7 +46,69 @@ import { buildRawRows, CREDENTIAL_STAMP_KEY, credentialDigest, dispatchDigest, e
|
|
|
46
46
|
* exactly (digest and dispatch digest), that names an identity the config
|
|
47
47
|
* does not record; that is completed forward the same way, by recording the
|
|
48
48
|
* identity.
|
|
49
|
+
*
|
|
50
|
+
* An attributed `disable` or `enable` that also changes the provider state
|
|
51
|
+
* follows the same order. Its state write carries the new value and, in the
|
|
52
|
+
* stamp, the transition (`transition: {mark, enabled, reason?}`, see
|
|
53
|
+
* `StampedTransition`); its config write then flips the row and records the
|
|
54
|
+
* transition's mark in the row's entry (`transitionMark`). A stamp bound to
|
|
55
|
+
* the row (this credential, epoch and identity, the binding that also shows
|
|
56
|
+
* the provider state beside it) whose transition mark the entry does not
|
|
57
|
+
* record is such a write stopped between its two writes, and is completed
|
|
58
|
+
* forward the same way: readers are shown the row disabled or enabled as the
|
|
59
|
+
* transition says, beside the value written with it. Once the config records
|
|
60
|
+
* the mark the transition says nothing more, so a later `enable` or
|
|
61
|
+
* `disable` of the row is never undone by it; a credential write drops it
|
|
62
|
+
* with the rest of the old stamp.
|
|
63
|
+
*/
|
|
64
|
+
/** Key, inside a credential stamp, of an attributed enable or disable. */
|
|
65
|
+
export const TRANSITION_STAMP_KEY = 'transition';
|
|
66
|
+
/**
|
|
67
|
+
* Key, inside a per-row config entry, of the mark of the last transition the
|
|
68
|
+
* config carries out. Older readers ignore it.
|
|
49
69
|
*/
|
|
70
|
+
export const TRANSITION_MARK_KEY = 'transitionMark';
|
|
71
|
+
/** The well-formed transition in a raw stamp, or undefined. */
|
|
72
|
+
function stampedTransition(rawStamp) {
|
|
73
|
+
if (!isRecord(rawStamp))
|
|
74
|
+
return undefined;
|
|
75
|
+
const raw = rawStamp[TRANSITION_STAMP_KEY];
|
|
76
|
+
if (!isRecord(raw))
|
|
77
|
+
return undefined;
|
|
78
|
+
if (typeof raw.mark !== 'string' || raw.mark.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (raw.enabled === true)
|
|
81
|
+
return { mark: raw.mark, enabled: true };
|
|
82
|
+
if (raw.enabled === false && typeof raw.reason === 'string')
|
|
83
|
+
return { mark: raw.mark, enabled: false, reason: raw.reason };
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Carries a transition out in a config being edited: the row's enabled flag
|
|
88
|
+
* and reason as the transition says, and its mark recorded in the entry (a
|
|
89
|
+
* row without an entry gets one at epoch 1, as `disableIn` gives it). An
|
|
90
|
+
* enable then applies the duplicate-identity rule, as every write that
|
|
91
|
+
* enables an OAuth row with a known identity does: the earlier row in roster
|
|
92
|
+
* order keeps the identity.
|
|
93
|
+
*/
|
|
94
|
+
export function applyTransition(editor, id, transition) {
|
|
95
|
+
if (transition.enabled)
|
|
96
|
+
enableIn(editor, id);
|
|
97
|
+
else
|
|
98
|
+
disableIn(editor, id, transition.reason ?? '');
|
|
99
|
+
if (!editor.rosterRow(id))
|
|
100
|
+
return;
|
|
101
|
+
const entry = editor.entry(id) ?? {
|
|
102
|
+
credentialEpoch: 1,
|
|
103
|
+
needsFirstReading: true,
|
|
104
|
+
};
|
|
105
|
+
editor.setEntry(id, { ...entry, [TRANSITION_MARK_KEY]: transition.mark });
|
|
106
|
+
if (!transition.enabled)
|
|
107
|
+
return;
|
|
108
|
+
const row = editor.rows().find((candidate) => candidate.id === id);
|
|
109
|
+
if (row?.type === 'oauth' && row.identity !== undefined)
|
|
110
|
+
disableIdentityDuplicates(editor, row.identity);
|
|
111
|
+
}
|
|
50
112
|
/**
|
|
51
113
|
* The credential a torn replace leaves once its config write lands: the
|
|
52
114
|
* credential loaded beside the stamp, with the endpoint the stamp's binding
|
|
@@ -122,6 +184,13 @@ export function tornStamps(config, state, codec, options = {}) {
|
|
|
122
184
|
row.identity === undefined) {
|
|
123
185
|
torn.set(row.id, { kind: 'identity', identity: stamp.binding.identity });
|
|
124
186
|
}
|
|
187
|
+
else if (stamp.credentialEpoch === epoch &&
|
|
188
|
+
stamp.binding.identity === row.identity) {
|
|
189
|
+
const transition = stampedTransition(account[CREDENTIAL_STAMP_KEY]);
|
|
190
|
+
if (transition &&
|
|
191
|
+
entryIn(config, row.id)?.[TRANSITION_MARK_KEY] !== transition.mark)
|
|
192
|
+
torn.set(row.id, { kind: 'transition', transition });
|
|
193
|
+
}
|
|
125
194
|
}
|
|
126
195
|
return torn;
|
|
127
196
|
}
|
|
@@ -175,6 +244,8 @@ export function completeTornRows(config, state, codec, options = {}) {
|
|
|
175
244
|
for (const [id, completion] of completions) {
|
|
176
245
|
if (completion.kind === 'identity')
|
|
177
246
|
recordIdentityIn(editor, id, completion.identity);
|
|
247
|
+
else if (completion.kind === 'transition')
|
|
248
|
+
applyTransition(editor, id, completion.transition);
|
|
178
249
|
else if (completion.stamp.binding.identity !== undefined)
|
|
179
250
|
disableIdentityDuplicates(editor, completion.stamp.binding.identity);
|
|
180
251
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/common-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Shared code for the CortexKit auth plugins: account pool, quota and routing, commands and auth menu, OpenCode 2 hooks, Claustrum custody, and plumbing (loopback RPC, file locks, logger, sidebar state, TUI preferences and build).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|