@cortexkit/opencode-openai-auth 0.11.0 → 1.0.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/README.md +15 -26
- package/dist/index-766sfrh5.js +1 -0
- package/dist/index-8kj7sa1p.js +12 -0
- package/dist/index-8rqfbhfw.js +221 -0
- package/dist/index-b3q3j39x.js +2 -0
- package/dist/index-bmd7x5aq.js +2 -0
- package/dist/index-eqh53k8v.js +2 -0
- package/dist/{index-sdt2d2ra.js → index-hy9030jh.js} +1 -1
- package/dist/index-jyr8y8k4.js +7 -0
- package/dist/index-rv08jmd4.js +2 -0
- package/dist/index.d.ts +578 -145
- package/dist/index.js +1 -110
- package/dist/pool-lifecycle-DbDmgabd.d.ts +2430 -0
- package/dist/port-file-Do3lQIEk.d.ts +29 -0
- package/dist/protocol-BOBdkqqm.d.ts +198 -0
- package/dist/rpc/port-file.d.ts +4 -17
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +2 -2
- package/dist/rpc/protocol.js +1 -0
- package/dist/rpc/rpc-client.d.ts +5 -4
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.d.ts +2 -0
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state-D6cUKgkm.d.ts +385 -0
- package/dist/sidebar-state.d.ts +2 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/token-util-wdza526f.js +1 -0
- package/dist/{token-9823y0pc.js → token-ypk61xj0.js} +1 -1
- package/dist/tui-preferences.js +5 -2
- package/dist/tui.d.ts +9 -4
- package/dist/v2/server.d.ts +54 -0
- package/dist/v2/server.js +4 -0
- package/package.json +21 -9
- package/src/tui/entry.mjs +26 -25
- package/src/tui-compiled/raw/logger.js +24 -0
- package/src/tui-compiled/raw/rpc/protocol.js +3 -0
- package/src/tui-compiled/raw/rpc/rpc-client.js +7 -0
- package/src/tui-compiled/raw/rpc/rpc-dir.js +27 -0
- package/src/tui-compiled/raw/selector.js +21 -0
- package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +138 -0
- package/src/tui-compiled/raw/shared/04a6357627de5ed0-errors.js +38 -0
- package/src/tui-compiled/raw/shared/053cc17b5419261e-codec.js +12 -0
- package/src/tui-compiled/raw/shared/0766d78c6f481f18-watcher.js +157 -0
- package/src/tui-compiled/raw/shared/08eef45626a94992-reserved.js +13 -0
- package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +134 -0
- package/src/tui-compiled/raw/shared/3c2d4169d00f907e-tui-support.js +5 -0
- package/src/tui-compiled/raw/shared/3e7d45a77c6ad4d5-index.js +4 -0
- package/src/tui-compiled/raw/shared/415792e85d80819e-capture-sink.js +13 -0
- package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +28 -0
- package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
- package/src/tui-compiled/raw/shared/458b934e92490331-mutate.js +500 -0
- package/src/tui-compiled/raw/shared/45bf5343bbbf9add-admission.js +142 -0
- package/src/tui-compiled/raw/shared/46f3dff1266160e8-client.js +2 -0
- package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +48 -0
- package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +162 -0
- package/src/tui-compiled/raw/shared/54eba94fd64c8c3c-index.js +7 -0
- package/src/tui-compiled/raw/shared/7211cfec06a298bc-atomic-write.js +80 -0
- package/src/tui-compiled/raw/shared/754d6017f8e419cf-builtins.js +534 -0
- package/src/tui-compiled/raw/shared/75bd8c3bd63b315f-identity.js +79 -0
- package/src/tui-compiled/raw/shared/7a91bb630e6f4804-index.js +1 -0
- package/src/tui-compiled/raw/shared/826342a4b6a8b89b-model.js +16 -0
- package/src/tui-compiled/raw/shared/83068c0dbe8a6f13-publication.js +304 -0
- package/src/tui-compiled/raw/shared/836fc615de6afbd9-settings.js +120 -0
- package/src/tui-compiled/raw/shared/83bc3ba9d0fe2761-runtime.js +68 -0
- package/src/tui-compiled/raw/shared/8a18abe4fa2977c8-menu.js +300 -0
- package/src/tui-compiled/raw/shared/901e149c5a0dbbff-sticky.js +339 -0
- package/src/tui-compiled/raw/shared/9020c407ce64f672-ordered.js +58 -0
- package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +105 -0
- package/src/tui-compiled/raw/shared/94820e324f2b4c09-seam.js +301 -0
- package/src/tui-compiled/raw/shared/97484d9bb98c705b-provider-state.js +226 -0
- package/src/tui-compiled/raw/shared/9968e3cade6969a1-index.js +2 -0
- package/src/tui-compiled/raw/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
- package/src/tui-compiled/raw/shared/9d3fc9e37456504e-refresh.js +225 -0
- package/src/tui-compiled/raw/shared/9deae3c4da4fd63b-hooks.js +34 -0
- package/src/tui-compiled/raw/shared/a09b6a533c11858f-tui-preferences.js +77 -0
- package/src/tui-compiled/raw/shared/a62a27a204266acd-attribution.js +60 -0
- package/src/tui-compiled/raw/shared/a910277ac43a8626-pull.js +143 -0
- package/src/tui-compiled/raw/shared/b85c5301fddcb98a-paths.js +5 -0
- package/src/tui-compiled/raw/shared/c14988621dc60a75-port-file.js +217 -0
- package/src/tui-compiled/raw/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
- package/src/tui-compiled/raw/shared/c6b176708c446d6d-pins.js +34 -0
- package/src/tui-compiled/raw/shared/c7d252bedee01600-pi.js +206 -0
- package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +120 -0
- package/src/tui-compiled/raw/shared/cc9e0702a752cea7-index.js +3 -0
- package/src/tui-compiled/raw/shared/cd94316a888b0a8e-schema.js +754 -0
- package/src/tui-compiled/raw/shared/d319517e3ea6ad70-redact.js +83 -0
- package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +5 -0
- package/src/tui-compiled/raw/shared/d669d72c40e9e116-logger.js +19 -0
- package/src/tui-compiled/raw/shared/d7dcba96fea1685c-engine.js +246 -0
- package/src/tui-compiled/raw/shared/dbef697d1df1cd84-index.js +5 -0
- package/src/tui-compiled/raw/shared/e4acc4cb713f300c-protocol.js +3 -0
- package/src/tui-compiled/raw/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
- package/src/tui-compiled/raw/shared/ec971142548df90e-index.js +6 -0
- package/src/tui-compiled/raw/shared/f37fcefc7aedbf25-error.js +11 -0
- package/src/tui-compiled/raw/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
- package/src/tui-compiled/raw/shared/fd45e82f2f9c0a0d-torn.js +294 -0
- package/src/tui-compiled/raw/shared/fe02abeadc535f77-rows.js +1258 -0
- package/src/tui-compiled/raw/sidebar-state.js +960 -0
- package/src/tui-compiled/raw/tui/command-dialogs.tsx +424 -0
- package/src/tui-compiled/raw/tui-preferences.js +135 -0
- package/src/tui-compiled/raw/tui.tsx +135 -143
- package/src/tui-compiled/runtime/logger.js +24 -0
- package/src/tui-compiled/runtime/rpc/protocol.js +3 -0
- package/src/tui-compiled/runtime/rpc/rpc-client.js +7 -0
- package/src/tui-compiled/runtime/rpc/rpc-dir.js +27 -0
- package/src/tui-compiled/runtime/selector.js +21 -0
- package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +138 -0
- package/src/tui-compiled/runtime/shared/04a6357627de5ed0-errors.js +38 -0
- package/src/tui-compiled/runtime/shared/053cc17b5419261e-codec.js +12 -0
- package/src/tui-compiled/runtime/shared/0766d78c6f481f18-watcher.js +157 -0
- package/src/tui-compiled/runtime/shared/08eef45626a94992-reserved.js +13 -0
- package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +134 -0
- package/src/tui-compiled/runtime/shared/3c2d4169d00f907e-tui-support.js +5 -0
- package/src/tui-compiled/runtime/shared/3e7d45a77c6ad4d5-index.js +4 -0
- package/src/tui-compiled/runtime/shared/415792e85d80819e-capture-sink.js +13 -0
- package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +28 -0
- package/src/tui-compiled/runtime/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
- package/src/tui-compiled/runtime/shared/458b934e92490331-mutate.js +500 -0
- package/src/tui-compiled/runtime/shared/45bf5343bbbf9add-admission.js +142 -0
- package/src/tui-compiled/runtime/shared/46f3dff1266160e8-client.js +2 -0
- package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +48 -0
- package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +162 -0
- package/src/tui-compiled/runtime/shared/54eba94fd64c8c3c-index.js +7 -0
- package/src/tui-compiled/runtime/shared/7211cfec06a298bc-atomic-write.js +80 -0
- package/src/tui-compiled/runtime/shared/754d6017f8e419cf-builtins.js +534 -0
- package/src/tui-compiled/runtime/shared/75bd8c3bd63b315f-identity.js +79 -0
- package/src/tui-compiled/runtime/shared/7a91bb630e6f4804-index.js +1 -0
- package/src/tui-compiled/runtime/shared/826342a4b6a8b89b-model.js +16 -0
- package/src/tui-compiled/runtime/shared/83068c0dbe8a6f13-publication.js +304 -0
- package/src/tui-compiled/runtime/shared/836fc615de6afbd9-settings.js +120 -0
- package/src/tui-compiled/runtime/shared/83bc3ba9d0fe2761-runtime.js +68 -0
- package/src/tui-compiled/runtime/shared/8a18abe4fa2977c8-menu.js +300 -0
- package/src/tui-compiled/runtime/shared/901e149c5a0dbbff-sticky.js +339 -0
- package/src/tui-compiled/runtime/shared/9020c407ce64f672-ordered.js +58 -0
- package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +105 -0
- package/src/tui-compiled/runtime/shared/94820e324f2b4c09-seam.js +301 -0
- package/src/tui-compiled/runtime/shared/97484d9bb98c705b-provider-state.js +226 -0
- package/src/tui-compiled/runtime/shared/9968e3cade6969a1-index.js +2 -0
- package/src/tui-compiled/runtime/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
- package/src/tui-compiled/runtime/shared/9d3fc9e37456504e-refresh.js +225 -0
- package/src/tui-compiled/runtime/shared/9deae3c4da4fd63b-hooks.js +34 -0
- package/src/tui-compiled/runtime/shared/a09b6a533c11858f-tui-preferences.js +77 -0
- package/src/tui-compiled/runtime/shared/a62a27a204266acd-attribution.js +60 -0
- package/src/tui-compiled/runtime/shared/a910277ac43a8626-pull.js +143 -0
- package/src/tui-compiled/runtime/shared/b85c5301fddcb98a-paths.js +5 -0
- package/src/tui-compiled/runtime/shared/c14988621dc60a75-port-file.js +217 -0
- package/src/tui-compiled/runtime/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
- package/src/tui-compiled/runtime/shared/c6b176708c446d6d-pins.js +34 -0
- package/src/tui-compiled/runtime/shared/c7d252bedee01600-pi.js +206 -0
- package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +120 -0
- package/src/tui-compiled/runtime/shared/cc9e0702a752cea7-index.js +3 -0
- package/src/tui-compiled/runtime/shared/cd94316a888b0a8e-schema.js +754 -0
- package/src/tui-compiled/runtime/shared/d319517e3ea6ad70-redact.js +83 -0
- package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +5 -0
- package/src/tui-compiled/runtime/shared/d669d72c40e9e116-logger.js +19 -0
- package/src/tui-compiled/runtime/shared/d7dcba96fea1685c-engine.js +246 -0
- package/src/tui-compiled/runtime/shared/dbef697d1df1cd84-index.js +5 -0
- package/src/tui-compiled/runtime/shared/e4acc4cb713f300c-protocol.js +3 -0
- package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
- package/src/tui-compiled/runtime/shared/ec971142548df90e-index.js +6 -0
- package/src/tui-compiled/runtime/shared/f37fcefc7aedbf25-error.js +11 -0
- package/src/tui-compiled/runtime/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
- package/src/tui-compiled/runtime/shared/fd45e82f2f9c0a0d-torn.js +294 -0
- package/src/tui-compiled/runtime/shared/fe02abeadc535f77-rows.js +1258 -0
- package/src/tui-compiled/runtime/sidebar-state.js +960 -0
- package/src/tui-compiled/runtime/tui/command-dialogs.js +378 -0
- package/src/tui-compiled/runtime/tui-preferences.js +135 -0
- package/src/tui-compiled/runtime/{tui.tsx → tui.js} +288 -316
- package/dist/index-4effjmr6.js +0 -2
- package/dist/index-9ehhb051.js +0 -2
- package/dist/index-g3hsdjh4.js +0 -2
- package/dist/index-hh52v8sb.js +0 -2
- package/dist/index-jwn4r3sw.js +0 -1
- package/dist/index-sq36b120.js +0 -211
- package/dist/internal-BTndPmW2.d.ts +0 -914
- package/dist/quota-normalize-08hpp51v.js +0 -1
- package/dist/sidebar-state-CoKYKcLn.d.ts +0 -220
- package/dist/token-util-ypgz7rge.js +0 -1
- package/src/tui-compiled/raw/core-account-paths.ts +0 -114
- package/src/tui-compiled/raw/core-custody-state.ts +0 -243
- package/src/tui-compiled/raw/logger.ts +0 -53
- package/src/tui-compiled/raw/rpc-port-file.ts +0 -177
- package/src/tui-compiled/raw/rpc-protocol.ts +0 -12
- package/src/tui-compiled/raw/rpc-rpc-client.ts +0 -82
- package/src/tui-compiled/raw/rpc-rpc-dir.ts +0 -46
- package/src/tui-compiled/raw/sidebar-state.ts +0 -1659
- package/src/tui-compiled/raw/tui-command-dialogs.tsx +0 -1096
- package/src/tui-compiled/raw/tui-preferences.ts +0 -328
- package/src/tui-compiled/runtime/core-account-paths.ts +0 -114
- package/src/tui-compiled/runtime/core-custody-state.ts +0 -243
- package/src/tui-compiled/runtime/logger.ts +0 -53
- package/src/tui-compiled/runtime/rpc-port-file.ts +0 -177
- package/src/tui-compiled/runtime/rpc-protocol.ts +0 -12
- package/src/tui-compiled/runtime/rpc-rpc-client.ts +0 -82
- package/src/tui-compiled/runtime/rpc-rpc-dir.ts +0 -46
- package/src/tui-compiled/runtime/sidebar-state.ts +0 -1659
- package/src/tui-compiled/runtime/tui-command-dialogs.tsx +0 -855
- package/src/tui-compiled/runtime/tui-preferences.ts +0 -328
- package/src/tui-compiled/shared/internal.ts +0 -6
- package/src/tui-compiled/shared/logger.ts +0 -264
- package/src/tui-compiled/shared/paths.ts +0 -33
- package/src/tui-compiled/shared/refresh-file-lock.ts +0 -384
- package/src/tui-compiled/shared/util/error.ts +0 -9
- package/src/tui-compiled/shared/util/open-url.ts +0 -30
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { assertNotInsideHook, runInsideHook } from "./9deae3c4da4fd63b-hooks.js";
|
|
2
|
+
import { runOperation, withTransaction } from "./458b934e92490331-mutate.js";
|
|
3
|
+
import { readRow, refusal, requireBound, rowLockSpec, unknownRow, } from "./83bc3ba9d0fe2761-runtime.js";
|
|
4
|
+
import { boundProviderStateDigest, CREDENTIAL_STAMP_KEY, credentialDigest, isCredentialEpoch, isRecord, PROVIDER_STATE_KEY, parseStamp, providerStateDigest, rowLockKey, } from "./cd94316a888b0a8e-schema.js";
|
|
5
|
+
/**
|
|
6
|
+
* A provider state as the store will store it: a JSON round trip of it (so
|
|
7
|
+
* its digest is the same once read back from disk), accepted by the codec.
|
|
8
|
+
* Refused before anything is written when the store has no codec, the value
|
|
9
|
+
* is not JSON, or the codec rejects it.
|
|
10
|
+
*/
|
|
11
|
+
export function acceptProviderState(codec, operation, id, value, what = 'the provider state') {
|
|
12
|
+
if (!codec)
|
|
13
|
+
throw refusal(operation, id, 'invalid-input', 'the store was opened without a provider-state codec');
|
|
14
|
+
let normalized;
|
|
15
|
+
try {
|
|
16
|
+
const text = JSON.stringify(value);
|
|
17
|
+
normalized = text === undefined ? undefined : JSON.parse(text);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
normalized = undefined;
|
|
21
|
+
}
|
|
22
|
+
if (normalized === undefined)
|
|
23
|
+
throw refusal(operation, id, 'invalid-provider-state', `${what} cannot be stored as JSON`);
|
|
24
|
+
if (!codec.validate(normalized))
|
|
25
|
+
throw refusal(operation, id, 'invalid-provider-state', `the provider-state codec rejected ${what}`);
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The write for a value a credential write brings (`add` of a secret the
|
|
30
|
+
* pool holds, `rotate`, a refresh): merged with the row's value on disk by
|
|
31
|
+
* the codec's `merge` when both exist, else the incoming value as is.
|
|
32
|
+
*/
|
|
33
|
+
export function mergedProviderState(codec, operation, id, onDisk, incoming) {
|
|
34
|
+
if (onDisk === undefined || !codec?.merge)
|
|
35
|
+
return { kind: 'set', value: incoming };
|
|
36
|
+
return {
|
|
37
|
+
kind: 'set',
|
|
38
|
+
value: acceptProviderState(codec, operation, id, codec.merge(structuredClone(onDisk), structuredClone(incoming)), 'the merged provider state'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The provider state a replace leaves on the row, decided before anything is
|
|
43
|
+
* written: whatever the codec's `onReplace` returns (undefined clears it), or
|
|
44
|
+
* without that hook the value handed to `replace`, else nothing. The old
|
|
45
|
+
* credential's value is never kept by default: it describes the account the
|
|
46
|
+
* replaced credential belonged to.
|
|
47
|
+
*/
|
|
48
|
+
export function replacementProviderState(codec, row, credentialEpoch, identity, incoming) {
|
|
49
|
+
const hook = codec?.onReplace;
|
|
50
|
+
const next = hook
|
|
51
|
+
? hook(row.providerState === undefined
|
|
52
|
+
? undefined
|
|
53
|
+
: structuredClone(row.providerState), {
|
|
54
|
+
id: row.id,
|
|
55
|
+
credentialEpoch,
|
|
56
|
+
...(row.identity !== undefined
|
|
57
|
+
? { previousIdentity: row.identity }
|
|
58
|
+
: {}),
|
|
59
|
+
...(identity !== undefined ? { identity } : {}),
|
|
60
|
+
...(incoming !== undefined
|
|
61
|
+
? { incoming: structuredClone(incoming) }
|
|
62
|
+
: {}),
|
|
63
|
+
})
|
|
64
|
+
: incoming;
|
|
65
|
+
if (next === undefined)
|
|
66
|
+
return { kind: 'clear' };
|
|
67
|
+
if (!hook)
|
|
68
|
+
return { kind: 'set', value: next };
|
|
69
|
+
return {
|
|
70
|
+
kind: 'set',
|
|
71
|
+
value: acceptProviderState(codec, 'replace', row.id, next, 'the provider state onReplace returned'),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The provider-state digest the stamp of a credential write carries, and the
|
|
76
|
+
* state-file fields it changes. `set` binds the new value; `keep` carries the
|
|
77
|
+
* old stamp's digest forward only when that stamp bound it to this row as it
|
|
78
|
+
* stood before the write (same credential lineage, the epoch being written,
|
|
79
|
+
* the identity recorded before the write); `clear` binds nothing.
|
|
80
|
+
*/
|
|
81
|
+
export function providerStateCoverage(codec, write, prior, priorRow, credentialEpoch) {
|
|
82
|
+
if (write.kind === 'set')
|
|
83
|
+
return providerStateDigest(codec, write.value);
|
|
84
|
+
if (write.kind === 'clear')
|
|
85
|
+
return undefined;
|
|
86
|
+
return boundProviderStateDigest(prior, priorRow?.credential, credentialEpoch, priorRow?.identity);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Changes a row's provider state without touching its credential, in one
|
|
90
|
+
* state-file write under the row lock, the caller's extra locks and the
|
|
91
|
+
* store locks. `fence` is what the caller read the row at: the write is
|
|
92
|
+
* refused (`attribution`, retryable) when the row has since moved to another
|
|
93
|
+
* credential epoch or recorded identity, and (`unknown-row`) once it is
|
|
94
|
+
* removed, so a writer that read the row before a replace or a removal never
|
|
95
|
+
* lands its value on the new credential or brings a removed row's state back.
|
|
96
|
+
*
|
|
97
|
+
* The value is bound by the stamp already beside the credential. When its
|
|
98
|
+
* credential-bound part is unchanged the stamp is left byte for byte as it
|
|
99
|
+
* is; otherwise only the stamp's provider-state digest changes, so the
|
|
100
|
+
* credential's stamp status never moves. With `requireCredentialStamps`, an
|
|
101
|
+
* unbound row refuses (`unbound-credential`) as every other strict path
|
|
102
|
+
* does. Without it, a row whose stamp was not written by this store with this
|
|
103
|
+
* credential at the row's epoch and identity refuses the same way: no stamp
|
|
104
|
+
* could bind the value, so no reader would ever show it. A `rotate` or
|
|
105
|
+
* `replace` stamps such a row.
|
|
106
|
+
*/
|
|
107
|
+
export async function updateProviderStateRow(rt, id, fence, mutator, options = {}) {
|
|
108
|
+
assertNotInsideHook('updateProviderState');
|
|
109
|
+
const { ctx } = rt;
|
|
110
|
+
const operation = 'updateProviderState';
|
|
111
|
+
return runOperation(ctx, operation, id, options.onFailure, async (locks, progress) => {
|
|
112
|
+
const codec = ctx.providerState;
|
|
113
|
+
if (!codec)
|
|
114
|
+
throw refusal(operation, id, 'invalid-input', 'the store was opened without a provider-state codec');
|
|
115
|
+
if (typeof mutator !== 'function')
|
|
116
|
+
throw refusal(operation, id, 'invalid-input', 'a mutator is required');
|
|
117
|
+
const captured = isRecord(fence) ? fence.credentialEpoch : undefined;
|
|
118
|
+
if (!isCredentialEpoch(captured))
|
|
119
|
+
throw refusal(operation, id, 'invalid-input', 'the credential epoch the provider state was read at is required');
|
|
120
|
+
const { row: seen } = await readRow(rt, operation, id);
|
|
121
|
+
await locks.acquire(rowLockSpec(rt, seen));
|
|
122
|
+
for (const extra of options.extraLocks ?? [])
|
|
123
|
+
await locks.acquire(extra);
|
|
124
|
+
return withTransaction(ctx, locks, progress, { operation, rowId: id }, async (tx) => {
|
|
125
|
+
const row = tx.row(id);
|
|
126
|
+
if (!row)
|
|
127
|
+
throw unknownRow(operation, id);
|
|
128
|
+
if (rowLockKey(row) !== rowLockKey(seen))
|
|
129
|
+
throw refusal(operation, id, 'row-key-changed', `row ${id}'s wire identity changed while its lock was being taken`, true);
|
|
130
|
+
if (row.invalid)
|
|
131
|
+
throw refusal(operation, id, 'invalid-row', `row ${id} failed validation`);
|
|
132
|
+
if (!row.credential)
|
|
133
|
+
throw refusal(operation, id, 'no-credential', `row ${id} holds no credential`);
|
|
134
|
+
requireBound(operation, row);
|
|
135
|
+
const epoch = row.credentialEpoch ?? 1;
|
|
136
|
+
if (epoch !== captured || row.identity !== fence.identity)
|
|
137
|
+
throw refusal(operation, id, 'attribution', `the provider state for ${id} was read for a credential or account the row no longer holds`, true);
|
|
138
|
+
const plan = await planProviderStateIn(tx, codec, operation, row, mutator);
|
|
139
|
+
if (plan.kind !== 'changed')
|
|
140
|
+
return {
|
|
141
|
+
id,
|
|
142
|
+
outcome: 'unchanged',
|
|
143
|
+
...(plan.kind === 'unchanged' && plan.value !== undefined
|
|
144
|
+
? { providerState: plan.value }
|
|
145
|
+
: {}),
|
|
146
|
+
};
|
|
147
|
+
tx.setStateAccount(id, plan.account);
|
|
148
|
+
await tx.commitState();
|
|
149
|
+
return plan.value === undefined
|
|
150
|
+
? { id, outcome: 'cleared' }
|
|
151
|
+
: { id, outcome: 'updated', providerState: plan.value };
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Returned by the provider-state mutator of an attributed `disable` or
|
|
157
|
+
* `enable` to decline the whole transition: nothing is written, neither the
|
|
158
|
+
* provider state nor the row's enabled flag, and the call resolves with
|
|
159
|
+
* `declined: true`. A mutator declines when the state it is shown is newer
|
|
160
|
+
* than what its caller saw, such as an eligibility recorded after the
|
|
161
|
+
* request whose refusal is being acted on. It is a value of its own because
|
|
162
|
+
* `undefined` already means "clear the provider state". `Symbol.for` keeps it
|
|
163
|
+
* equal across two copies of this module loaded in one process.
|
|
164
|
+
*/
|
|
165
|
+
export const DECLINE_TRANSITION = Symbol.for('@cortexkit/common-auth/store/decline-transition');
|
|
166
|
+
/**
|
|
167
|
+
* Runs a provider-state mutator for a row loaded under every lock and
|
|
168
|
+
* already checked by the caller (present, valid, inside its attribution
|
|
169
|
+
* fence), and plans the write. Refuses (`no-credential`) a row holding no
|
|
170
|
+
* credential, and (`unbound-credential`) one whose credential carries no
|
|
171
|
+
* stamp of this store at the row's epoch and identity: no stamp could bind
|
|
172
|
+
* the value, so no reader would ever show it. `DECLINE_TRANSITION` is
|
|
173
|
+
* honoured only when `declinable` is set; elsewhere it is not JSON and is
|
|
174
|
+
* refused as such.
|
|
175
|
+
*/
|
|
176
|
+
export async function planProviderStateIn(tx, codec, operation, row, mutator, declinable = false) {
|
|
177
|
+
const id = row.id;
|
|
178
|
+
const credential = row.credential;
|
|
179
|
+
if (!credential)
|
|
180
|
+
throw refusal(operation, id, 'no-credential', `row ${id} holds no credential`);
|
|
181
|
+
const epoch = row.credentialEpoch ?? 1;
|
|
182
|
+
const account = tx.stateAccount(id);
|
|
183
|
+
const rawStamp = account?.[CREDENTIAL_STAMP_KEY];
|
|
184
|
+
const stamp = parseStamp(rawStamp);
|
|
185
|
+
if (!isRecord(rawStamp) ||
|
|
186
|
+
!stamp?.binding ||
|
|
187
|
+
stamp.digest !== credentialDigest(credential) ||
|
|
188
|
+
stamp.credentialEpoch !== epoch ||
|
|
189
|
+
stamp.binding.identity !== row.identity)
|
|
190
|
+
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`);
|
|
191
|
+
const current = row.providerState === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: structuredClone(row.providerState);
|
|
194
|
+
const returned = await runInsideHook(operation, () => mutator(current, row));
|
|
195
|
+
if (declinable && returned === DECLINE_TRANSITION)
|
|
196
|
+
return { kind: 'declined' };
|
|
197
|
+
const next = returned === undefined
|
|
198
|
+
? undefined
|
|
199
|
+
: acceptProviderState(codec, operation, id, returned, 'the provider state the mutator returned');
|
|
200
|
+
const held = account !== undefined && Object.hasOwn(account, PROVIDER_STATE_KEY);
|
|
201
|
+
if (next === undefined
|
|
202
|
+
? !held
|
|
203
|
+
: row.providerState !== undefined &&
|
|
204
|
+
JSON.stringify(row.providerState) === JSON.stringify(next))
|
|
205
|
+
return { kind: 'unchanged', ...(next !== undefined ? { value: next } : {}) };
|
|
206
|
+
const nextAccount = { ...account };
|
|
207
|
+
if (next === undefined) {
|
|
208
|
+
delete nextAccount[PROVIDER_STATE_KEY];
|
|
209
|
+
const nextStamp = { ...rawStamp };
|
|
210
|
+
delete nextStamp.providerState;
|
|
211
|
+
nextAccount[CREDENTIAL_STAMP_KEY] = nextStamp;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
nextAccount[PROVIDER_STATE_KEY] = next;
|
|
215
|
+
const digest = providerStateDigest(codec, next);
|
|
216
|
+
// A change confined to the part the codec does not bind to the
|
|
217
|
+
// credential leaves the stamp exactly as it was.
|
|
218
|
+
if (stamp.providerState !== digest)
|
|
219
|
+
nextAccount[CREDENTIAL_STAMP_KEY] = { ...rawStamp, providerState: digest };
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
kind: 'changed',
|
|
223
|
+
...(next !== undefined ? { value: next } : {}),
|
|
224
|
+
account: nextAccount,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const WRITER_LOCK_CONSTANTS = Object.freeze({
|
|
2
|
+
sidebar: Object.freeze({
|
|
3
|
+
name: 'sidebar-write',
|
|
4
|
+
ttlMs: 10_000,
|
|
5
|
+
timeoutMs: 15_000,
|
|
6
|
+
renew: true,
|
|
7
|
+
}),
|
|
8
|
+
preferences: Object.freeze({
|
|
9
|
+
name: 'preferences',
|
|
10
|
+
ttlMs: 10_000,
|
|
11
|
+
timeoutMs: 2_000,
|
|
12
|
+
renew: true,
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { PoolOperationError } from "./04a6357627de5ed0-errors.js";
|
|
3
|
+
import { assertNotInsideHook, runInsideHook } from "./9deae3c4da4fd63b-hooks.js";
|
|
4
|
+
import { IDENTITY_CONTRADICTED_REASON_PREFIX, recordIdentityIn, } from "./75bd8c3bd63b315f-identity.js";
|
|
5
|
+
import { runOperation, withTransaction } from "./458b934e92490331-mutate.js";
|
|
6
|
+
import { acceptProviderState, mergedProviderState } from "./97484d9bb98c705b-provider-state.js";
|
|
7
|
+
import { assertNotReserved } from "./08eef45626a94992-reserved.js";
|
|
8
|
+
import { rotateIn } from "./fe02abeadc535f77-rows.js";
|
|
9
|
+
import { readRow, refusal, requireBound, rowLockSpec, } from "./83bc3ba9d0fe2761-runtime.js";
|
|
10
|
+
import { rotationStamp, rotationStampUntrusted, rowLockKey, } from "./cd94316a888b0a8e-schema.js";
|
|
11
|
+
import { applyTransition } from "./fd45e82f2f9c0a0d-torn.js";
|
|
12
|
+
function requireRefreshable(id, row) {
|
|
13
|
+
assertNotReserved('refresh', id, row?.staged);
|
|
14
|
+
if (!row)
|
|
15
|
+
throw refusal('refresh', id, 'unknown-row', `no row ${id} in the pool`);
|
|
16
|
+
if (row.invalid)
|
|
17
|
+
throw refusal('refresh', id, 'invalid-row', `row ${id} is invalid`);
|
|
18
|
+
if (row.type !== 'oauth' || row.credential?.type !== 'oauth')
|
|
19
|
+
throw refusal('refresh', id, 'no-credential', `row ${id} holds no OAuth credential`);
|
|
20
|
+
if (!row.enabled)
|
|
21
|
+
throw refusal('refresh', id, 'row-disabled', `row ${id} is disabled`);
|
|
22
|
+
return row;
|
|
23
|
+
}
|
|
24
|
+
function stampAhead(id) {
|
|
25
|
+
return refusal('refresh', id, 'refresh-stamp-ahead', `row ${id}'s stored refresh stamp is ahead of the clock; a rotation now could not be stamped newer within the trust bound`, true);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Refreshes one OAuth row. Locks are taken in the fixed order row lock,
|
|
29
|
+
* provider-wide lock, extra locks, and the store locks only around the
|
|
30
|
+
* capture and the commit; ownership of every held lease is asserted after
|
|
31
|
+
* each wait, immediately before the provider call and before each write.
|
|
32
|
+
*/
|
|
33
|
+
export async function refreshRow(rt, id, provider, options = {}) {
|
|
34
|
+
assertNotInsideHook('refresh');
|
|
35
|
+
const { ctx } = rt;
|
|
36
|
+
return runOperation(ctx, 'refresh', id, options.onFailure, async (locks, progress) => {
|
|
37
|
+
const { row: seen } = await readRow(rt, 'refresh', id);
|
|
38
|
+
requireRefreshable(id, seen);
|
|
39
|
+
const early = await options.refuse?.(seen);
|
|
40
|
+
if (early !== undefined)
|
|
41
|
+
return { status: 'refused', rowId: id, reason: early };
|
|
42
|
+
let key = rowLockKey(seen);
|
|
43
|
+
let captured;
|
|
44
|
+
for (let attempt = 0;; attempt++) {
|
|
45
|
+
await locks.acquire(rowLockSpec(rt, { id, identity: key === id ? undefined : key }));
|
|
46
|
+
await locks.acquire(options.providerLock ?? rt.providerLock);
|
|
47
|
+
for (const extra of options.extraLocks ?? [])
|
|
48
|
+
await locks.acquire(extra);
|
|
49
|
+
// The capture may write the config once, to give a row its per-row
|
|
50
|
+
// entry. That write is setup, not the rotation, so it is counted apart
|
|
51
|
+
// and never makes a later failure report `after-first-write`.
|
|
52
|
+
const captureProgress = { writes: 0 };
|
|
53
|
+
const read = await withTransaction(ctx, locks, captureProgress, { operation: 'refresh', rowId: id }, async (tx) => {
|
|
54
|
+
let row = requireRefreshable(id, tx.row(id));
|
|
55
|
+
requireBound('refresh', row);
|
|
56
|
+
if (!row.hasEntry) {
|
|
57
|
+
tx.setEntry(id, { credentialEpoch: 1, needsFirstReading: true });
|
|
58
|
+
await tx.commitConfig();
|
|
59
|
+
row = requireRefreshable(id, tx.row(id));
|
|
60
|
+
}
|
|
61
|
+
if (rowLockKey(row) !== key)
|
|
62
|
+
return { keyNow: rowLockKey(row) };
|
|
63
|
+
const reason = await options.refuse?.(row);
|
|
64
|
+
if (reason !== undefined)
|
|
65
|
+
return { refused: reason };
|
|
66
|
+
return {
|
|
67
|
+
row,
|
|
68
|
+
credential: row.credential,
|
|
69
|
+
credentialEpoch: row.credentialEpoch,
|
|
70
|
+
identity: row.identity,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
if ('refused' in read)
|
|
74
|
+
return { status: 'refused', rowId: id, reason: read.refused };
|
|
75
|
+
if ('keyNow' in read) {
|
|
76
|
+
await locks.releaseAll();
|
|
77
|
+
if (attempt >= 1)
|
|
78
|
+
throw refusal('refresh', id, 'row-key-changed', `row ${id}'s wire identity changed twice while its lock was held`, true);
|
|
79
|
+
key = read.keyNow;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
captured = read;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (rotationStampUntrusted(captured.credential.lastRefreshedAt, ctx.now()))
|
|
86
|
+
throw stampAhead(id);
|
|
87
|
+
await ctx.hold?.('refresh-before-provider', id);
|
|
88
|
+
await locks.assertAll();
|
|
89
|
+
let result;
|
|
90
|
+
try {
|
|
91
|
+
result = await provider(captured.credential, captured.row);
|
|
92
|
+
}
|
|
93
|
+
catch (cause) {
|
|
94
|
+
throw new PoolOperationError({
|
|
95
|
+
operation: 'refresh',
|
|
96
|
+
rowId: id,
|
|
97
|
+
phase: 'before-first-write',
|
|
98
|
+
retryable: true,
|
|
99
|
+
kind: 'provider',
|
|
100
|
+
message: 'the provider refresh failed',
|
|
101
|
+
cause,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (typeof result?.refresh !== 'string' || !result.refresh.trim())
|
|
105
|
+
throw refusal('refresh', id, 'provider', 'the provider returned no refresh token', true);
|
|
106
|
+
const incoming = result.providerState === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: acceptProviderState(ctx.providerState, 'refresh', id, result.providerState, 'the provider state the provider returned');
|
|
109
|
+
const commit = await withTransaction(ctx, locks, progress, { operation: 'refresh', rowId: id }, async (tx) => {
|
|
110
|
+
const current = tx.row(id);
|
|
111
|
+
const entry = tx.entry(id);
|
|
112
|
+
if (!current ||
|
|
113
|
+
current.invalid ||
|
|
114
|
+
!entry ||
|
|
115
|
+
entry.credentialEpoch !== captured.credentialEpoch ||
|
|
116
|
+
current.identity !== captured.identity)
|
|
117
|
+
throw new PoolOperationError({
|
|
118
|
+
operation: 'refresh',
|
|
119
|
+
rowId: id,
|
|
120
|
+
phase: 'before-first-write',
|
|
121
|
+
retryable: true,
|
|
122
|
+
kind: 'attribution',
|
|
123
|
+
message: `row ${id} changed credential while its refresh was in flight; the rotation is discarded`,
|
|
124
|
+
});
|
|
125
|
+
// The epoch fence above does not see a credential another writer
|
|
126
|
+
// swapped in under the same epoch; the stamp does, and committing
|
|
127
|
+
// the rotation would stamp the swapped row as bound.
|
|
128
|
+
requireBound('refresh', current);
|
|
129
|
+
const reason = await options.refuse?.(current);
|
|
130
|
+
if (reason !== undefined)
|
|
131
|
+
return { refused: reason };
|
|
132
|
+
const now = ctx.now();
|
|
133
|
+
const prior = current.credential?.type === 'oauth'
|
|
134
|
+
? current.credential.lastRefreshedAt
|
|
135
|
+
: undefined;
|
|
136
|
+
if (rotationStampUntrusted(prior, now))
|
|
137
|
+
throw stampAhead(id);
|
|
138
|
+
const credential = {
|
|
139
|
+
type: 'oauth',
|
|
140
|
+
access: result.access,
|
|
141
|
+
refresh: result.refresh,
|
|
142
|
+
expires: result.expires,
|
|
143
|
+
};
|
|
144
|
+
const contradiction = current.identity !== undefined &&
|
|
145
|
+
result.identity !== undefined &&
|
|
146
|
+
current.identity !== result.identity
|
|
147
|
+
? {
|
|
148
|
+
expectedIdentity: current.identity,
|
|
149
|
+
returnedIdentity: result.identity,
|
|
150
|
+
}
|
|
151
|
+
: undefined;
|
|
152
|
+
const transition = contradiction
|
|
153
|
+
? {
|
|
154
|
+
mark: randomUUID(),
|
|
155
|
+
enabled: false,
|
|
156
|
+
reason: `${IDENTITY_CONTRADICTED_REASON_PREFIX}${JSON.stringify(contradiction)}`,
|
|
157
|
+
}
|
|
158
|
+
: undefined;
|
|
159
|
+
const learnt = current.identity === undefined && result.identity
|
|
160
|
+
? result.identity
|
|
161
|
+
: undefined;
|
|
162
|
+
// The rotated credential's stamp names a learnt identity before the
|
|
163
|
+
// config records it, so a crash between the two writes is completed
|
|
164
|
+
// forward rather than leaving an identity no stamp proves.
|
|
165
|
+
const stored = await rotateIn(rt, tx, id, credential, {
|
|
166
|
+
stamp: rotationStamp(prior, now),
|
|
167
|
+
// Write state first with the disable marker, then config below.
|
|
168
|
+
// Once the successor is durable, recovery projects the disable even
|
|
169
|
+
// if config has not landed. A crash before the first rename still
|
|
170
|
+
// loses an in-memory provider reply, as in any ordinary refresh.
|
|
171
|
+
transition,
|
|
172
|
+
identity: learnt,
|
|
173
|
+
...(incoming !== undefined
|
|
174
|
+
? {
|
|
175
|
+
providerState: mergedProviderState(ctx.providerState, 'refresh', id, current.providerState, incoming),
|
|
176
|
+
}
|
|
177
|
+
: {}),
|
|
178
|
+
});
|
|
179
|
+
if (transition !== undefined) {
|
|
180
|
+
applyTransition(tx, id, transition);
|
|
181
|
+
await tx.commitConfig();
|
|
182
|
+
}
|
|
183
|
+
let identity = current.identity;
|
|
184
|
+
if (learnt !== undefined) {
|
|
185
|
+
recordIdentityIn(tx, id, learnt);
|
|
186
|
+
identity = learnt;
|
|
187
|
+
await tx.commitConfig();
|
|
188
|
+
}
|
|
189
|
+
return { stored, identity, contradiction, refused: undefined };
|
|
190
|
+
});
|
|
191
|
+
if (commit.refused !== undefined)
|
|
192
|
+
return { status: 'refused', rowId: id, reason: commit.refused };
|
|
193
|
+
if (commit.contradiction !== undefined)
|
|
194
|
+
return {
|
|
195
|
+
status: 'identity-contradicted',
|
|
196
|
+
rowId: id,
|
|
197
|
+
...commit.contradiction,
|
|
198
|
+
credential: commit.stored,
|
|
199
|
+
};
|
|
200
|
+
if (options.onPersisted) {
|
|
201
|
+
try {
|
|
202
|
+
const persisted = options.onPersisted;
|
|
203
|
+
await runInsideHook('refresh', () => persisted(id, commit.stored));
|
|
204
|
+
}
|
|
205
|
+
catch (cause) {
|
|
206
|
+
throw new PoolOperationError({
|
|
207
|
+
operation: 'refresh',
|
|
208
|
+
rowId: id,
|
|
209
|
+
phase: 'after-first-write',
|
|
210
|
+
retryable: false,
|
|
211
|
+
kind: 'after-persist-hook',
|
|
212
|
+
committed: commit.stored,
|
|
213
|
+
message: 'the after-persist hook threw; the rotation stays committed',
|
|
214
|
+
cause,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
status: 'rotated',
|
|
220
|
+
rowId: id,
|
|
221
|
+
credential: commit.stored,
|
|
222
|
+
...(commit.identity !== undefined ? { identity: commit.identity } : {}),
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { PoolReentryError } from "./04a6357627de5ed0-errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Set while a hook of a lock-holding operation runs. Async context is
|
|
5
|
+
* inherited by every timer, promise and continuation created inside the hook,
|
|
6
|
+
* so work a hook schedules is inside the guarded region too.
|
|
7
|
+
*/
|
|
8
|
+
const insideHook = new AsyncLocalStorage();
|
|
9
|
+
/** Refuses a row operation or refresh called from inside a hook. */
|
|
10
|
+
export function assertNotInsideHook(operation) {
|
|
11
|
+
if (insideHook.getStore() !== undefined)
|
|
12
|
+
throw new PoolReentryError(operation);
|
|
13
|
+
}
|
|
14
|
+
export function runInsideHook(operation, fn) {
|
|
15
|
+
return insideHook.run(operation, async () => await fn());
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs a failure hook. A hook that throws never replaces the failure it was
|
|
19
|
+
* handed: its exception is logged and discarded.
|
|
20
|
+
*/
|
|
21
|
+
export async function callFailureHook(operation, hook, rowId, error, logger) {
|
|
22
|
+
if (!hook)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
await runInsideHook(operation, () => hook(rowId, error));
|
|
26
|
+
}
|
|
27
|
+
catch (hookError) {
|
|
28
|
+
logger?.warn('store failure hook threw; the original failure stands', {
|
|
29
|
+
operation,
|
|
30
|
+
rowId,
|
|
31
|
+
error: hookError instanceof Error ? hookError.message : String(hookError),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
3
|
+
import { writeJsonAtomic } from "./7211cfec06a298bc-atomic-write.js";
|
|
4
|
+
import { WRITER_LOCK_CONSTANTS } from "./9b4da614c45f9cd0-lock-constants.js";
|
|
5
|
+
import { withLock } from "./4825e03209d6ae3e-with-lock.js";
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
/** Malformed or missing user-edited files are treated as an empty root. */
|
|
10
|
+
export async function readTuiPreferencesFile(file) {
|
|
11
|
+
try {
|
|
12
|
+
const errors = [];
|
|
13
|
+
const root = parse(await readFile(file, 'utf8'), errors, {
|
|
14
|
+
allowTrailingComma: true,
|
|
15
|
+
});
|
|
16
|
+
return errors.length === 0 && isRecord(root) ? root : {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function readTuiPreferences(options) {
|
|
23
|
+
const root = await readTuiPreferencesFile(options.file);
|
|
24
|
+
const entry = Object.hasOwn(root, options.pluginKey)
|
|
25
|
+
? root[options.pluginKey]
|
|
26
|
+
: undefined;
|
|
27
|
+
return options.schema(entry, options.defaults);
|
|
28
|
+
}
|
|
29
|
+
const TEMPLATE = `// Shared preferences for TUI plugins.
|
|
30
|
+
// Plugins update individual keys in place and preserve comments.
|
|
31
|
+
{}
|
|
32
|
+
`;
|
|
33
|
+
/** Each instance serializes updates; the file lock also excludes other processes. */
|
|
34
|
+
export function createTuiPreferenceWriter(options) {
|
|
35
|
+
const { file, pluginKey, beforeCommit } = options;
|
|
36
|
+
const lockOptions = {
|
|
37
|
+
...WRITER_LOCK_CONSTANTS.preferences,
|
|
38
|
+
timeoutMs: options.timeoutMs ?? WRITER_LOCK_CONSTANTS.preferences.timeoutMs,
|
|
39
|
+
};
|
|
40
|
+
let chain = Promise.resolve();
|
|
41
|
+
async function writePreference(path, value) {
|
|
42
|
+
await withLock(file, lockOptions, async (lock) => {
|
|
43
|
+
let text;
|
|
44
|
+
try {
|
|
45
|
+
text = await readFile(file, 'utf8');
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code !== 'ENOENT')
|
|
49
|
+
throw error;
|
|
50
|
+
text = '';
|
|
51
|
+
}
|
|
52
|
+
if (text.trim() === '')
|
|
53
|
+
text = TEMPLATE;
|
|
54
|
+
const next = applyEdits(text, modify(text, [pluginKey, ...path], value, {
|
|
55
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
56
|
+
}));
|
|
57
|
+
await writeJsonAtomic(file, next, {
|
|
58
|
+
serialize: (value) => String(value),
|
|
59
|
+
beforeRename: async () => {
|
|
60
|
+
await beforeCommit?.();
|
|
61
|
+
await lock.assertOwned();
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
queueTuiPreferenceUpdate(path, value) {
|
|
68
|
+
// Snapshot inputs so queued callers cannot change an update before it runs.
|
|
69
|
+
const savedPath = [...path];
|
|
70
|
+
const savedValue = structuredClone(value);
|
|
71
|
+
const update = chain.then(() => writePreference(savedPath, savedValue));
|
|
72
|
+
// Recover only the stored tail; the caller still sees the original rejection.
|
|
73
|
+
chain = update.catch(() => { });
|
|
74
|
+
return update;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { PoolOperationError } from "./04a6357627de5ed0-errors.js";
|
|
2
|
+
import { toFailure, withTransaction } from "./458b934e92490331-mutate.js";
|
|
3
|
+
import { LockStack } from "./43fc9e6b994519c7-refresh-lock.js";
|
|
4
|
+
import { refusal, requireBound, unknownRow, } from "./83bc3ba9d0fe2761-runtime.js";
|
|
5
|
+
import { isCredentialEpoch } from "./cd94316a888b0a8e-schema.js";
|
|
6
|
+
/**
|
|
7
|
+
* Merges a quota observation into a row's stored map under the store locks,
|
|
8
|
+
* after attribution passes. Needs no row lock, so it is permitted from
|
|
9
|
+
* inside hooks. Clears needs-first-reading on success.
|
|
10
|
+
*/
|
|
11
|
+
export async function recordQuota(rt, id, attribution, observation) {
|
|
12
|
+
const { ctx } = rt;
|
|
13
|
+
const locks = new LockStack(ctx.lockDefaults, ctx.lockEnv);
|
|
14
|
+
const progress = { writes: 0 };
|
|
15
|
+
try {
|
|
16
|
+
if (!isCredentialEpoch(attribution?.credentialEpoch))
|
|
17
|
+
throw refusal('pull', id, 'invalid-input', 'the credential epoch the reading was issued for must be a positive safe integer');
|
|
18
|
+
await withTransaction(ctx, locks, progress, { operation: 'pull', rowId: id }, async (tx) => {
|
|
19
|
+
tx.assertNotStaged(id);
|
|
20
|
+
const row = tx.row(id);
|
|
21
|
+
if (!row)
|
|
22
|
+
throw unknownRow('pull', id);
|
|
23
|
+
if (row.invalid)
|
|
24
|
+
throw refusal('pull', id, 'invalid-row', `row ${id} is invalid`);
|
|
25
|
+
// A reading belongs to one (epoch, identity, credential). A row
|
|
26
|
+
// holding no credential, or torn between the writes of a replace,
|
|
27
|
+
// has no such triple on disk, so no reading is recorded for it.
|
|
28
|
+
if (!row.credential)
|
|
29
|
+
throw refusal('pull', id, 'no-credential', `row ${id} holds no credential`);
|
|
30
|
+
requireBound('pull', row);
|
|
31
|
+
const entry = tx.entry(id);
|
|
32
|
+
if (row.torn ||
|
|
33
|
+
!entry ||
|
|
34
|
+
entry.credentialEpoch !== attribution.credentialEpoch ||
|
|
35
|
+
row.identity !== attribution.identity)
|
|
36
|
+
throw new PoolOperationError({
|
|
37
|
+
operation: 'pull',
|
|
38
|
+
rowId: id,
|
|
39
|
+
phase: 'pull',
|
|
40
|
+
retryable: true,
|
|
41
|
+
kind: 'attribution',
|
|
42
|
+
message: `quota for ${id} was issued for a credential the row no longer holds`,
|
|
43
|
+
});
|
|
44
|
+
const merged = ctx.codec.merge(entry.quota, observation);
|
|
45
|
+
if (!ctx.codec.validate(merged))
|
|
46
|
+
throw refusal('pull', id, 'invalid-quota', 'the quota codec rejected the merged map');
|
|
47
|
+
tx.setEntry(id, { ...entry, quota: merged, needsFirstReading: false });
|
|
48
|
+
await tx.commitConfig();
|
|
49
|
+
},
|
|
50
|
+
// Recording a reading never completes a torn row: the fence above
|
|
51
|
+
// refuses it, and the row's own next write completes it.
|
|
52
|
+
{ completeTorn: false });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw toFailure(error, 'pull', id, progress);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await locks.releaseAll();
|
|
59
|
+
}
|
|
60
|
+
}
|