@cortexkit/common-auth 0.10.0 → 0.11.1
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/README.md +16 -0
- package/dist/auth-menu/ansi.d.ts +4 -2
- package/dist/auth-menu/ansi.js +44 -13
- package/dist/auth-menu/index.d.ts +1 -1
- package/dist/auth-menu/index.js +1 -1
- package/dist/auth-menu/select.js +30 -24
- package/dist/store/index.d.ts +1 -1
- package/dist/store/pool.d.ts +6 -3
- package/dist/store/provider-state.js +3 -0
- package/dist/store/rows.d.ts +16 -2
- package/dist/store/rows.js +36 -13
- package/dist/store/schema.d.ts +6 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,4 +7,20 @@ Shared libraries for the CortexKit auth plugins for OpenCode and Pi: openai-auth
|
|
|
7
7
|
- [OpenCode 2 transport spike](research/opencode2-transport/REPORT.md): can a plugin own the transport on OpenCode 2?
|
|
8
8
|
- [OpenCode 2 native-driver spike](research/opencode2-thin/REPORT.md): multi-account support through hooks on OpenCode 2's built-in OpenAI driver.
|
|
9
9
|
|
|
10
|
+
## Mutation catalogue
|
|
11
|
+
|
|
12
|
+
A passing safety test proves little until it has been seen to fail. [`mutations.toml`](mutations.toml) is the checked-in catalogue: each row deliberately breaks one guard in `src/` (or a CI scan script) and names the Bun test that must fail because of it, so a guard cannot silently stop guarding. A row earns its place by guarding a silent, costly failure: lock exclusion, crash safety, credential loss or leakage, a single-use token used twice, misattributed quota, a wire contract, data loss. Style and cosmetics do not.
|
|
13
|
+
|
|
14
|
+
The pinned [`ck-mutate`](https://github.com/cortexkit/commons/tree/7d08e73722fa3e79bbcc2607753978ab768f1c6b/crates/cortexkit-mutate) runner replays it from a clean tree. It installs and builds first (fixtures import `dist/`), checks each named test passes, applies the break, rebuilds, requires the test to fail, and restores the source byte for byte:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
cargo install --locked --git https://github.com/cortexkit/commons --rev 7d08e73722fa3e79bbcc2607753978ab768f1c6b cortexkit-mutate
|
|
18
|
+
ck-mutate check
|
|
19
|
+
ck-mutate run --all
|
|
20
|
+
ck-mutate run --diff origin/main
|
|
21
|
+
ck-mutate run --only fs-lock-try-once-refuses-held-lock
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Every row must report CAUGHT. Replay with the Bun version CI pins (1.3.14): the row `rpc-declared-oversize-413-half-closes` guards the RPC server's workaround for Bun 1.3.14 reusing a connection the server refused with 413. Bun 1.4.2 never reuses it, so that row survives there. Rows are command rows: `{test}` is a Bun `-t` filter, a regular expression over the full test name (describe names and test name joined by spaces), so ids are anchored, escaped, and spell each space `\s`. CI replays the rows a pull request's diff selects, the full catalogue on every push to main, and the full catalogue with `--broad` nightly. Never `git checkout` a file while a replay runs: it removes the mutation and fakes a survivor.
|
|
25
|
+
|
|
10
26
|
Licensed under MIT; see [LICENSE](LICENSE).
|
package/dist/auth-menu/ansi.d.ts
CHANGED
|
@@ -12,8 +12,10 @@ export declare const ANSI: {
|
|
|
12
12
|
readonly dim: '\u001B[2m';
|
|
13
13
|
readonly reset: '\u001B[0m';
|
|
14
14
|
};
|
|
15
|
-
export type KeyAction = 'up' | 'down' | 'enter' | 'escape' | 'escape-start' | null;
|
|
16
|
-
/**
|
|
15
|
+
export type KeyAction = 'up' | 'down' | 'left' | 'right' | 'enter' | 'escape' | 'escape-start' | 'char' | null;
|
|
16
|
+
/** Tokenize complete keys in one read; incomplete escape sequences are ignored. */
|
|
17
|
+
export declare function parseKeys(data: Buffer | string): KeyAction[];
|
|
18
|
+
/** Preserve the single-key parser for callers that only need the first token. */
|
|
17
19
|
export declare function parseKey(data: Buffer | string): KeyAction;
|
|
18
20
|
/** Remove colour codes, leaving the text an operator sees. */
|
|
19
21
|
export declare function stripAnsi(input: string): string;
|
package/dist/auth-menu/ansi.js
CHANGED
|
@@ -12,20 +12,51 @@ export const ANSI = {
|
|
|
12
12
|
dim: '\x1b[2m',
|
|
13
13
|
reset: '\x1b[0m',
|
|
14
14
|
};
|
|
15
|
-
/**
|
|
16
|
-
export function
|
|
15
|
+
/** Tokenize complete keys in one read; incomplete escape sequences are ignored. */
|
|
16
|
+
export function parseKeys(data) {
|
|
17
17
|
const value = data.toString();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
const keys = [];
|
|
19
|
+
for (let index = 0; index < value.length;) {
|
|
20
|
+
const char = value[index];
|
|
21
|
+
if (char === '\x1b') {
|
|
22
|
+
const sequence = value.slice(index, index + 3);
|
|
23
|
+
const arrow = sequence[1] === '[' || sequence[1] === 'O' ? sequence[2] : undefined;
|
|
24
|
+
if (arrow && 'ABCD'.includes(arrow)) {
|
|
25
|
+
const action = { A: 'up', B: 'down', C: 'right', D: 'left' };
|
|
26
|
+
const parsed = action[arrow];
|
|
27
|
+
if (parsed)
|
|
28
|
+
keys.push(parsed);
|
|
29
|
+
index += 3;
|
|
30
|
+
}
|
|
31
|
+
else if (index + 1 === value.length) {
|
|
32
|
+
// Only a chunk consisting solely of ESC gets the legacy Escape timeout.
|
|
33
|
+
if (index === 0)
|
|
34
|
+
keys.push('escape-start');
|
|
35
|
+
index += 1;
|
|
36
|
+
}
|
|
37
|
+
else if ((value[index + 1] === '[' || value[index + 1] === 'O') &&
|
|
38
|
+
index + 2 >= value.length) {
|
|
39
|
+
// An incomplete escape prefix at the chunk boundary is not a key.
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
index += 1;
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (char === '\r' || char === '\n')
|
|
48
|
+
keys.push('enter');
|
|
49
|
+
else if (char === '\x03')
|
|
50
|
+
keys.push('escape');
|
|
51
|
+
else if (char && char >= ' ')
|
|
52
|
+
keys.push('char');
|
|
53
|
+
index += 1;
|
|
54
|
+
}
|
|
55
|
+
return keys;
|
|
56
|
+
}
|
|
57
|
+
/** Preserve the single-key parser for callers that only need the first token. */
|
|
58
|
+
export function parseKey(data) {
|
|
59
|
+
return parseKeys(data)[0] ?? null;
|
|
29
60
|
}
|
|
30
61
|
const ANSI_PATTERN = `${String.fromCharCode(27)}\\[[0-9;]*m`;
|
|
31
62
|
const ANSI_REGEX = new RegExp(ANSI_PATTERN, 'g');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { AccountMenuOptions } from './accounts.js';
|
|
2
2
|
export { accountMenuActions, addAccountAction, checkQuotasAction, deleteAllAction, listAccountsAction, MENU_DISABLE_REASON, poolHasCredential, quotaLines, reauthenticateAction, removeAccountAction, runAccountMenu, toggleAccountAction, } from './accounts.js';
|
|
3
3
|
export type { KeyAction } from './ansi.js';
|
|
4
|
-
export { ANSI, parseKey, stripAnsi, truncateAnsi } from './ansi.js';
|
|
4
|
+
export { ANSI, parseKey, parseKeys, stripAnsi, truncateAnsi } from './ansi.js';
|
|
5
5
|
export { confirm } from './confirm.js';
|
|
6
6
|
export type { DoctorActionOptions, DoctorCheck, DoctorFinding, DoctorRepair, DoctorReport, RepairOutcome, } from './doctor.js';
|
|
7
7
|
export { applyChosenRepairs, DOCTOR_CHECK_FAILED, doctorAction, formatDoctorReport, runDoctorChecks, } from './doctor.js';
|
package/dist/auth-menu/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// runtime, the OpenCode v1 `authorize` contract around it, and the account
|
|
3
3
|
// actions built over the `/store` pool.
|
|
4
4
|
export { accountMenuActions, addAccountAction, checkQuotasAction, deleteAllAction, listAccountsAction, MENU_DISABLE_REASON, poolHasCredential, quotaLines, reauthenticateAction, removeAccountAction, runAccountMenu, toggleAccountAction, } from './accounts.js';
|
|
5
|
-
export { ANSI, parseKey, stripAnsi, truncateAnsi } from './ansi.js';
|
|
5
|
+
export { ANSI, parseKey, parseKeys, stripAnsi, truncateAnsi } from './ansi.js';
|
|
6
6
|
export { confirm } from './confirm.js';
|
|
7
7
|
export { applyChosenRepairs, DOCTOR_CHECK_FAILED, doctorAction, formatDoctorReport, runDoctorChecks, } from './doctor.js';
|
|
8
8
|
export { openBrowserForMenu, runMenuLogin } from './login.js';
|
package/dist/auth-menu/select.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ANSI,
|
|
1
|
+
import { ANSI, parseKeys, truncateAnsi } from './ansi.js';
|
|
2
2
|
import { isInteractive } from './terminal.js';
|
|
3
3
|
/** How long a lone Escape byte waits for the rest of an arrow-key sequence. */
|
|
4
4
|
const ESCAPE_TIMEOUT_MS = 50;
|
|
@@ -109,30 +109,36 @@ export async function select(terminal, items, options) {
|
|
|
109
109
|
};
|
|
110
110
|
const onSignal = () => finish(null);
|
|
111
111
|
const onKey = (data) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
escapeTimeout = null;
|
|
115
|
-
}
|
|
116
|
-
switch (parseKey(data)) {
|
|
117
|
-
case 'up':
|
|
118
|
-
cursor = (cursor - 1 + items.length) % items.length;
|
|
119
|
-
render();
|
|
120
|
-
break;
|
|
121
|
-
case 'down':
|
|
122
|
-
cursor = (cursor + 1) % items.length;
|
|
123
|
-
render();
|
|
124
|
-
break;
|
|
125
|
-
case 'enter':
|
|
126
|
-
finish(items[cursor]?.value ?? null);
|
|
127
|
-
break;
|
|
128
|
-
case 'escape':
|
|
129
|
-
finish(null);
|
|
130
|
-
break;
|
|
131
|
-
case 'escape-start':
|
|
132
|
-
// A bare Escape byte is also the start of an arrow-key sequence
|
|
133
|
-
// that may arrive split across reads; only a lone one cancels.
|
|
134
|
-
escapeTimeout = setTimeout(() => finish(null), ESCAPE_TIMEOUT_MS);
|
|
112
|
+
for (const key of parseKeys(data)) {
|
|
113
|
+
if (cleaned)
|
|
135
114
|
break;
|
|
115
|
+
if (escapeTimeout) {
|
|
116
|
+
clearTimeout(escapeTimeout);
|
|
117
|
+
escapeTimeout = null;
|
|
118
|
+
}
|
|
119
|
+
switch (key) {
|
|
120
|
+
case 'up':
|
|
121
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
122
|
+
render();
|
|
123
|
+
break;
|
|
124
|
+
case 'down':
|
|
125
|
+
cursor = (cursor + 1) % items.length;
|
|
126
|
+
render();
|
|
127
|
+
break;
|
|
128
|
+
case 'enter':
|
|
129
|
+
finish(items[cursor]?.value ?? null);
|
|
130
|
+
break;
|
|
131
|
+
case 'escape':
|
|
132
|
+
finish(null);
|
|
133
|
+
break;
|
|
134
|
+
case 'escape-start':
|
|
135
|
+
// A bare Escape byte is also the start of an arrow-key sequence
|
|
136
|
+
// that may arrive split across reads; only a lone one cancels.
|
|
137
|
+
escapeTimeout = setTimeout(() => finish(null), ESCAPE_TIMEOUT_MS);
|
|
138
|
+
break;
|
|
139
|
+
default:
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
136
142
|
}
|
|
137
143
|
};
|
|
138
144
|
signals?.once('SIGINT', onSignal);
|
package/dist/store/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type { PullHook, PullRequest } from './pull.js';
|
|
|
12
12
|
export type { ProviderRefresh, ProviderRefreshResult, RefreshOptions, RefreshOutcome, } from './refresh.js';
|
|
13
13
|
export type { LockEvent, PoolLockOptions, PoolLockSpec, } from './refresh-lock.js';
|
|
14
14
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
15
|
-
export type { AddInput, AddResult, CredentialWriteInput, FailureHook, RemoveOptions, RemoveResult, RemoveView, ReorderOptions, ReorderResult, RowOperationOptions, RowToggleOptions, RowTransitionOptions, RowTransitionResult, } from './rows.js';
|
|
15
|
+
export type { AddInput, AddResult, CredentialWriteInput, FailureHook, RemoveOptions, RemoveResult, RemoveView, ReorderOptions, ReorderResult, RowOperationOptions, RowToggleOptions, RowTransitionOptions, RowTransitionResult, RowWriteOptions, } from './rows.js';
|
|
16
16
|
export type { PullReason } from './runtime.js';
|
|
17
17
|
export type { ApiKeyCredential, CredentialStampStatus, OAuthCredential, PoolCredential, PoolRow, ProviderStateCodec, ProviderStateDrop, ProviderStateReplacement, QuotaCodec, RotateCredential, StoredCredential, } from './schema.js';
|
|
18
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/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, type RowTransitionOptions, type RowTransitionResult } 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, type RowWriteOptions } 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 {
|
|
@@ -100,8 +100,10 @@ export interface PoolStore {
|
|
|
100
100
|
* Gives a row a new credential and a new credential epoch. Since 0.6.0 the
|
|
101
101
|
* row's provider state is whatever `ProviderStateCodec.onReplace` returns;
|
|
102
102
|
* without that hook it is `input.providerState`, else cleared.
|
|
103
|
+
* Optional `attribution` fences the write on the prior credential under the
|
|
104
|
+
* locks, before any write or replacement hook; see `RowWriteOptions`.
|
|
103
105
|
*/
|
|
104
|
-
replace(id: string, credential: PoolCredential, input?: CredentialWriteInput, options?:
|
|
106
|
+
replace(id: string, credential: PoolCredential, input?: CredentialWriteInput, options?: RowWriteOptions): Promise<{
|
|
105
107
|
id: string;
|
|
106
108
|
credential: StoredCredential;
|
|
107
109
|
credentialEpoch: number;
|
|
@@ -111,8 +113,9 @@ export interface PoolStore {
|
|
|
111
113
|
* endpoint. Since 0.4.1 an API key may leave out `baseURL` and `authHeader`
|
|
112
114
|
* to keep the row's, and one that gives another is refused
|
|
113
115
|
* (`endpoint-mismatch`) before writing: that is a `replace`.
|
|
116
|
+
* Takes the same optional attribution fence as `replace` (`RowWriteOptions`).
|
|
114
117
|
*/
|
|
115
|
-
rotate(id: string, credential: RotateCredential, input?: CredentialWriteInput, options?:
|
|
118
|
+
rotate(id: string, credential: RotateCredential, input?: CredentialWriteInput, options?: RowWriteOptions): Promise<{
|
|
116
119
|
id: string;
|
|
117
120
|
credential: StoredCredential;
|
|
118
121
|
}>;
|
|
@@ -53,6 +53,9 @@ export function replacementProviderState(codec, row, credentialEpoch, identity,
|
|
|
53
53
|
: structuredClone(row.providerState), {
|
|
54
54
|
id: row.id,
|
|
55
55
|
credentialEpoch,
|
|
56
|
+
...(row.identity !== undefined
|
|
57
|
+
? { previousIdentity: row.identity }
|
|
58
|
+
: {}),
|
|
56
59
|
...(identity !== undefined ? { identity } : {}),
|
|
57
60
|
...(incoming !== undefined
|
|
58
61
|
? { incoming: structuredClone(incoming) }
|
package/dist/store/rows.d.ts
CHANGED
|
@@ -20,6 +20,20 @@ export interface RowOperationOptions {
|
|
|
20
20
|
*/
|
|
21
21
|
extraLocks?: readonly PoolLockSpec[];
|
|
22
22
|
}
|
|
23
|
+
/** Options of `replace` and `rotate`. Without `attribution` a call behaves as before. */
|
|
24
|
+
export interface RowWriteOptions extends RowOperationOptions {
|
|
25
|
+
/**
|
|
26
|
+
* The credential epoch and recorded identity the caller read the row at
|
|
27
|
+
* when it decided on this write; an identity left out means the row had
|
|
28
|
+
* none. Compared exactly, under the row and store locks, before any write
|
|
29
|
+
* or replacement hook, including the completion of an interrupted replace.
|
|
30
|
+
* The call is refused (`attribution`, retryable, nothing written) once the
|
|
31
|
+
* row holds another epoch or identity, so a write decided on an older
|
|
32
|
+
* credential never overwrites the one that replaced it. `disable`, `enable`,
|
|
33
|
+
* `recordQuota` and `updateProviderState` take the same fence.
|
|
34
|
+
*/
|
|
35
|
+
attribution?: Attribution;
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* Options of `disable`, `enable` and `remove`. The provider-wide lock guards
|
|
25
39
|
* changes to the recorded identity a row lock is named by; none of these
|
|
@@ -127,12 +141,12 @@ export declare function rotateIn(rt: StoreRuntime, tx: Transaction, id: string,
|
|
|
127
141
|
transition?: StampedTransition;
|
|
128
142
|
}): Promise<StoredCredential>;
|
|
129
143
|
export declare function addRow(rt: StoreRuntime, input: AddInput, options?: RowOperationOptions): Promise<AddResult>;
|
|
130
|
-
export declare function replaceRow(rt: StoreRuntime, id: string, credential: PoolCredential, input?: CredentialWriteInput, options?:
|
|
144
|
+
export declare function replaceRow(rt: StoreRuntime, id: string, credential: PoolCredential, input?: CredentialWriteInput, options?: RowWriteOptions): Promise<{
|
|
131
145
|
id: string;
|
|
132
146
|
credential: StoredCredential;
|
|
133
147
|
credentialEpoch: number;
|
|
134
148
|
}>;
|
|
135
|
-
export declare function rotateRow(rt: StoreRuntime, id: string, credential: RotateCredential, input?: CredentialWriteInput, options?:
|
|
149
|
+
export declare function rotateRow(rt: StoreRuntime, id: string, credential: RotateCredential, input?: CredentialWriteInput, options?: RowWriteOptions): Promise<{
|
|
136
150
|
id: string;
|
|
137
151
|
credential: StoredCredential;
|
|
138
152
|
}>;
|
package/dist/store/rows.js
CHANGED
|
@@ -179,6 +179,22 @@ function requireUsableRow(operation, id, row, credential) {
|
|
|
179
179
|
throw refusal(operation, id, 'type-mismatch', `row ${id} holds a ${row.type} credential`);
|
|
180
180
|
return row;
|
|
181
181
|
}
|
|
182
|
+
function validateAttribution(operation, id, fence) {
|
|
183
|
+
if (fence !== undefined &&
|
|
184
|
+
!isCredentialEpoch(isRecord(fence) ? fence.credentialEpoch : undefined))
|
|
185
|
+
throw refusal(operation, id, 'invalid-input', 'the attribution must name a credential epoch that is a positive safe integer');
|
|
186
|
+
}
|
|
187
|
+
/** Exact recorded identity, including absence, is part of a credential fence. */
|
|
188
|
+
function assertRowAttribution(operation, id, row, fence) {
|
|
189
|
+
if (!row)
|
|
190
|
+
throw unknownRow(operation, id);
|
|
191
|
+
// A row that failed validation has no usable credential epoch to compare.
|
|
192
|
+
if (row.invalid)
|
|
193
|
+
throw refusal(operation, id, 'invalid-row', `row ${id} failed validation`);
|
|
194
|
+
if ((row.credentialEpoch ?? 1) !== fence.credentialEpoch ||
|
|
195
|
+
row.identity !== fence.identity)
|
|
196
|
+
throw refusal(operation, id, 'attribution', `the ${operation} of ${id} was issued for a credential or account the row no longer holds`, true);
|
|
197
|
+
}
|
|
182
198
|
/** The row is recorded for another account than the one given. */
|
|
183
199
|
function identityMismatch(operation, id) {
|
|
184
200
|
return refusal(operation, id, 'identity-mismatch', `row ${id} is recorded for another account; a credential of a different account is a replacement`);
|
|
@@ -318,6 +334,7 @@ export async function replaceRow(rt, id, credential, input = {}, options = {}) {
|
|
|
318
334
|
const { ctx } = rt;
|
|
319
335
|
const result = await runOperation(ctx, 'replace', id, options.onFailure, async (locks, progress) => {
|
|
320
336
|
checkInput('replace', id, credential);
|
|
337
|
+
validateAttribution('replace', id, options.attribution);
|
|
321
338
|
const incoming = input.providerState === undefined
|
|
322
339
|
? undefined
|
|
323
340
|
: acceptProviderState(ctx.providerState, 'replace', id, input.providerState);
|
|
@@ -328,6 +345,15 @@ export async function replaceRow(rt, id, credential, input = {}, options = {}) {
|
|
|
328
345
|
for (const extra of options.extraLocks ?? [])
|
|
329
346
|
await locks.acquire(extra);
|
|
330
347
|
return withTransaction(ctx, locks, progress, { operation: 'replace', rowId: id }, async (tx) => {
|
|
348
|
+
if (options.attribution !== undefined) {
|
|
349
|
+
// A replace interrupted between its two file writes is read as
|
|
350
|
+
// already done: tx.row shows its new epoch and identity. Compare
|
|
351
|
+
// the fence with that row before finishing the interrupted write
|
|
352
|
+
// on disk, so a stale caller does not even complete a replacement
|
|
353
|
+
// it was never issued for.
|
|
354
|
+
assertRowAttribution('replace', id, tx.row(id), options.attribution);
|
|
355
|
+
await tx.completeTorn();
|
|
356
|
+
}
|
|
331
357
|
const row = requireUsableRow('replace', id, tx.row(id), credential);
|
|
332
358
|
if (rowLockKey(row) !== rowLockKey(seen))
|
|
333
359
|
throw keyChanged('replace', id);
|
|
@@ -368,7 +394,7 @@ export async function replaceRow(rt, id, credential, input = {}, options = {}) {
|
|
|
368
394
|
});
|
|
369
395
|
await tx.commitConfig();
|
|
370
396
|
return { id, credential: stored, credentialEpoch };
|
|
371
|
-
});
|
|
397
|
+
}, { completeTorn: options.attribution === undefined });
|
|
372
398
|
});
|
|
373
399
|
if (credential.type === 'oauth')
|
|
374
400
|
rt.firePull(id, 'replace');
|
|
@@ -379,6 +405,7 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
379
405
|
const { ctx } = rt;
|
|
380
406
|
return runOperation(ctx, 'rotate', id, options.onFailure, async (locks, progress) => {
|
|
381
407
|
checkInput('rotate', id, credential);
|
|
408
|
+
validateAttribution('rotate', id, options.attribution);
|
|
382
409
|
const incoming = input.providerState === undefined
|
|
383
410
|
? undefined
|
|
384
411
|
: acceptProviderState(ctx.providerState, 'rotate', id, input.providerState);
|
|
@@ -389,6 +416,10 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
389
416
|
for (const extra of options.extraLocks ?? [])
|
|
390
417
|
await locks.acquire(extra);
|
|
391
418
|
return withTransaction(ctx, locks, progress, { operation: 'rotate', rowId: id }, async (tx) => {
|
|
419
|
+
if (options.attribution !== undefined) {
|
|
420
|
+
assertRowAttribution('rotate', id, tx.row(id), options.attribution);
|
|
421
|
+
await tx.completeTorn();
|
|
422
|
+
}
|
|
392
423
|
const row = requireUsableRow('rotate', id, tx.row(id), credential);
|
|
393
424
|
if (rowLockKey(row) !== rowLockKey(seen))
|
|
394
425
|
throw keyChanged('rotate', id);
|
|
@@ -428,7 +459,7 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
428
459
|
if (configChanged)
|
|
429
460
|
await tx.commitConfig();
|
|
430
461
|
return { id, credential: stored };
|
|
431
|
-
});
|
|
462
|
+
}, { completeTorn: options.attribution === undefined });
|
|
432
463
|
});
|
|
433
464
|
}
|
|
434
465
|
/**
|
|
@@ -451,9 +482,7 @@ async function transitionRow(rt, operation, id, flag, options) {
|
|
|
451
482
|
const fence = options.attribution;
|
|
452
483
|
const mutator = options.providerState;
|
|
453
484
|
return runOperation(ctx, operation, id, options.onFailure, async (locks, progress) => {
|
|
454
|
-
|
|
455
|
-
!isCredentialEpoch(isRecord(fence) ? fence.credentialEpoch : undefined))
|
|
456
|
-
throw refusal(operation, id, 'invalid-input', 'the attribution must name a credential epoch that is a positive safe integer');
|
|
485
|
+
validateAttribution(operation, id, fence);
|
|
457
486
|
if (mutator !== undefined) {
|
|
458
487
|
if (typeof mutator !== 'function')
|
|
459
488
|
throw refusal(operation, id, 'invalid-input', 'the provider-state mutator must be a function');
|
|
@@ -477,14 +506,8 @@ async function transitionRow(rt, operation, id, flag, options) {
|
|
|
477
506
|
throw unknownRow(operation, id);
|
|
478
507
|
if (rowLockKey(row) !== rowLockKey(seen))
|
|
479
508
|
throw keyChanged(operation, id);
|
|
480
|
-
if (fence !== undefined)
|
|
481
|
-
|
|
482
|
-
if (row.invalid)
|
|
483
|
-
throw refusal(operation, id, 'invalid-row', `row ${id} failed validation`);
|
|
484
|
-
if ((row.credentialEpoch ?? 1) !== fence.credentialEpoch ||
|
|
485
|
-
row.identity !== fence.identity)
|
|
486
|
-
throw refusal(operation, id, 'attribution', `the ${operation} of ${id} was issued for a credential or account the row no longer holds`, true);
|
|
487
|
-
}
|
|
509
|
+
if (fence !== undefined)
|
|
510
|
+
assertRowAttribution(operation, id, row, fence);
|
|
488
511
|
if (flag.enabled &&
|
|
489
512
|
row.disabledReason?.startsWith(IDENTITY_CONTRADICTED_REASON_PREFIX))
|
|
490
513
|
throw refusal('enable', id, 'identity-contradicted', `row ${id} needs an identity-validated credential replacement before it can be enabled`);
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -52,14 +52,16 @@ export interface QuotaCodec {
|
|
|
52
52
|
merge(stored: unknown | undefined, observation: unknown): unknown;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* What `ProviderStateCodec.onReplace` is told about a replacement: the row,
|
|
56
|
-
* the credential epoch the new credential starts,
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* What `ProviderStateCodec.onReplace` is told about a replacement: the row id,
|
|
56
|
+
* the credential epoch the new credential starts, `previousIdentity` (the
|
|
57
|
+
* identity the locked row recorded before the replace), `identity` (the
|
|
58
|
+
* identity the replace records), and the provider state the caller handed to
|
|
59
|
+
* `replace`, if any. Each identity is absent when none was recorded.
|
|
59
60
|
*/
|
|
60
61
|
export interface ProviderStateReplacement {
|
|
61
62
|
id: string;
|
|
62
63
|
credentialEpoch: number;
|
|
64
|
+
previousIdentity?: string;
|
|
63
65
|
identity?: string;
|
|
64
66
|
incoming?: unknown;
|
|
65
67
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/common-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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": {
|