@bitkyc08/opencodex 2.59.0 → 2.60.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.
Files changed (194) hide show
  1. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  2. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +4 -1
  5. package/src/adapters/anthropic-image-codec.ts +16 -2
  6. package/src/adapters/anthropic-image-normalize.ts +49 -2
  7. package/src/adapters/anthropic.ts +4 -1
  8. package/src/adapters/base.ts +23 -0
  9. package/src/adapters/coding-agent/turn.ts +22 -2
  10. package/src/adapters/command-code.ts +50 -3
  11. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  12. package/src/adapters/cursor/discovery.ts +11 -8
  13. package/src/adapters/cursor/live-transport.ts +26 -9
  14. package/src/adapters/cursor/request-builder.ts +7 -1
  15. package/src/adapters/cursor/transport.ts +19 -0
  16. package/src/adapters/cursor.ts +3 -1
  17. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  18. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  19. package/src/adapters/devin.ts +50 -13
  20. package/src/adapters/google-antigravity-replay.ts +1 -1
  21. package/src/adapters/google-antigravity-wire.ts +29 -5
  22. package/src/adapters/google-http.ts +49 -10
  23. package/src/adapters/google-tool-schema.ts +595 -31
  24. package/src/adapters/google-wire-compiler.ts +93 -10
  25. package/src/adapters/google-wire-shape.ts +461 -0
  26. package/src/adapters/google.ts +37 -6
  27. package/src/adapters/openai-chat-images.ts +3 -1
  28. package/src/adapters/openai-chat.ts +5 -1
  29. package/src/adapters/openai-responses/image-gen.ts +8 -6
  30. package/src/adapters/openai-responses/passthrough.ts +15 -3
  31. package/src/adapters/openai-responses/reasoning.ts +7 -0
  32. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  33. package/src/bridge/sse.ts +3 -151
  34. package/src/cli/account-extended.ts +4 -4
  35. package/src/cli/dispatch.ts +3 -3
  36. package/src/cli/doctor.ts +28 -9
  37. package/src/cli/hub.ts +3 -2
  38. package/src/cli/index.ts +7 -2
  39. package/src/cli/opencode.ts +2 -2
  40. package/src/cli/provider.ts +13 -1
  41. package/src/client/machine-api.ts +2 -2
  42. package/src/client/machine-listener.ts +2 -2
  43. package/src/client/runtime.ts +26 -2
  44. package/src/codex/account-store.ts +65 -0
  45. package/src/codex/auth-api/account-list.ts +19 -11
  46. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  47. package/src/codex/catalog/gather-capture.ts +21 -2
  48. package/src/codex/catalog/model-hints.ts +29 -28
  49. package/src/codex/catalog/parsing.ts +7 -0
  50. package/src/codex/catalog/provider-models.ts +19 -2
  51. package/src/codex/catalog/retained-sync.ts +22 -26
  52. package/src/codex/catalog/routed-gather.ts +19 -0
  53. package/src/codex/context-compat.ts +5 -2
  54. package/src/codex/desired-state.ts +4 -1
  55. package/src/codex/history-job.ts +6 -6
  56. package/src/codex/history-provider.ts +20 -166
  57. package/src/codex/history-rollout-read.ts +174 -0
  58. package/src/codex/internal/catalog-writer.ts +33 -1
  59. package/src/codex/model-cache.ts +47 -0
  60. package/src/codex/model-entitlements.ts +29 -10
  61. package/src/codex/observed-model-denials.ts +101 -8
  62. package/src/codex/prompt-text-probe.ts +9 -6
  63. package/src/codex/routing.ts +7 -1
  64. package/src/codex/shim.ts +1 -1
  65. package/src/codex/subagent-model-fallback.ts +22 -4
  66. package/src/combos/failover.ts +3 -0
  67. package/src/config/admitted-identity.ts +222 -0
  68. package/src/config/diagnostics.ts +22 -1
  69. package/src/config/feature-flags.ts +5 -0
  70. package/src/config/load-degrade.ts +18 -0
  71. package/src/config/proxy-env.ts +8 -2
  72. package/src/config/schema/compaction-triggers.ts +11 -0
  73. package/src/config/schema/config-schema.ts +4 -0
  74. package/src/config/schema/leaf-validators.ts +12 -0
  75. package/src/config.ts +2 -2
  76. package/src/generated/compatibility-version.json +249 -169
  77. package/src/grok/reset-coupons.ts +38 -19
  78. package/src/images/loop.ts +6 -1
  79. package/src/integrations/aside-profile-context.ts +37 -3
  80. package/src/integrations/aside-profile-journal.ts +68 -3
  81. package/src/integrations/aside-profiles.ts +128 -3
  82. package/src/integrations/mutation-plan.ts +815 -0
  83. package/src/integrations/writer.ts +85 -99
  84. package/src/lab/live/transport.ts +4 -0
  85. package/src/lab/live/types.ts +5 -0
  86. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  87. package/src/lib/admin-secrets.ts +9 -1
  88. package/src/lib/debug-log-buffer.ts +6 -1
  89. package/src/lib/debug.ts +23 -0
  90. package/src/lib/errors.ts +79 -0
  91. package/src/lib/http-response-semantics.ts +57 -0
  92. package/src/lib/lab-live-pinned-sender.ts +26 -12
  93. package/src/lib/pinned-http.ts +142 -2
  94. package/src/lib/plain-data.ts +103 -0
  95. package/src/lib/process-control.ts +13 -5
  96. package/src/lib/provider-outbound.ts +50 -2
  97. package/src/lib/socks5-fetch.ts +136 -26
  98. package/src/lib/spend-ledger-owner.ts +364 -0
  99. package/src/lib/spend-reservation-ledger.ts +218 -27
  100. package/src/lib/windows-system-proxy.ts +16 -11
  101. package/src/oauth/callback-server.ts +4 -3
  102. package/src/oauth/generic-account-failover.ts +1 -0
  103. package/src/oauth/health.ts +12 -1
  104. package/src/oauth/index.ts +3 -107
  105. package/src/oauth/login-flow-state.ts +127 -0
  106. package/src/providers/derive.ts +34 -17
  107. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  108. package/src/providers/key-failover.ts +66 -18
  109. package/src/providers/model-rename-migration.ts +55 -1
  110. package/src/providers/model-rename-startup.ts +7 -5
  111. package/src/providers/openai-virtual-models.ts +42 -2
  112. package/src/providers/quota/antigravity.ts +22 -2
  113. package/src/providers/quota/vendor-probes-key.ts +1 -1
  114. package/src/providers/registry/entries-core.ts +39 -17
  115. package/src/providers/registry/entries-extended.ts +35 -1
  116. package/src/providers/registry/model-ids.ts +168 -0
  117. package/src/providers/registry/model-seeds.ts +9 -0
  118. package/src/providers/registry/types.ts +2 -0
  119. package/src/providers/resolved-model-policy-merge.ts +167 -0
  120. package/src/providers/resolved-model-policy.ts +406 -0
  121. package/src/providers/stale-vision-classification-migration.ts +137 -0
  122. package/src/responses/apply-patch-envelope.ts +0 -12
  123. package/src/responses/freeform-wrapper-scan.ts +279 -0
  124. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  125. package/src/responses/progressive-freeform-input.ts +130 -0
  126. package/src/responses/reasoning-envelope.ts +30 -0
  127. package/src/responses/state.ts +5 -12
  128. package/src/responses/tool-name-aliases.ts +15 -1
  129. package/src/router.ts +91 -115
  130. package/src/routing/compatibility/behavior.ts +9 -0
  131. package/src/routing/compatibility/subject.ts +16 -1
  132. package/src/server/adapter-resolve.ts +9 -0
  133. package/src/server/auth-cors.ts +3 -0
  134. package/src/server/chat-completions.ts +5 -2
  135. package/src/server/claude-messages.ts +6 -3
  136. package/src/server/effort-row.ts +11 -3
  137. package/src/server/grok-responses-control-frame.ts +160 -1
  138. package/src/server/index/serve-options.ts +86 -29
  139. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  140. package/src/server/index/websocket-handler.ts +6 -1
  141. package/src/server/index.ts +16 -13
  142. package/src/server/management/aside-profile-routes.ts +266 -7
  143. package/src/server/management/config-routes.ts +18 -2
  144. package/src/server/management/context.ts +3 -0
  145. package/src/server/management/integration-routes.ts +287 -5
  146. package/src/server/management/metrics-routes.ts +20 -0
  147. package/src/server/management/model-rows.ts +224 -12
  148. package/src/server/management/route-registry.ts +14 -0
  149. package/src/server/management/shared.ts +10 -3
  150. package/src/server/management/system-restart.ts +7 -2
  151. package/src/server/management/system-routes.ts +2 -0
  152. package/src/server/management/usage-aggregate-cache.ts +4 -0
  153. package/src/server/management-api.ts +2 -0
  154. package/src/server/management-auth.ts +15 -1
  155. package/src/server/readiness.ts +29 -10
  156. package/src/server/relay-eager.ts +24 -2
  157. package/src/server/relay.ts +119 -10
  158. package/src/server/request-log.ts +55 -2
  159. package/src/server/request-metrics.ts +236 -0
  160. package/src/server/responses/adapter-continuation.ts +3 -3
  161. package/src/server/responses/adapter-dispatch.ts +11 -6
  162. package/src/server/responses/compact.ts +32 -10
  163. package/src/server/responses/compaction-routing.ts +111 -0
  164. package/src/server/responses/core-codex-account.ts +8 -3
  165. package/src/server/responses/core-combo.ts +7 -7
  166. package/src/server/responses/core-normalize.ts +6 -12
  167. package/src/server/responses/core-opaque-recovery.ts +1 -0
  168. package/src/server/responses/core-options.ts +4 -0
  169. package/src/server/responses/encrypted-payload.ts +20 -2
  170. package/src/server/responses/passthrough-delivery.ts +31 -14
  171. package/src/server/responses/passthrough-dispatch.ts +36 -9
  172. package/src/server/responses/policy-fallback.ts +5 -13
  173. package/src/server/responses/request-prepare.ts +67 -17
  174. package/src/server/responses/request-send-budget.ts +5 -1
  175. package/src/server/responses/request-sidecar-auth.ts +1 -1
  176. package/src/server/responses/request-transport.ts +2 -2
  177. package/src/server/responses/run-turn-execution.ts +25 -3
  178. package/src/server/responses/sidecar-execution.ts +17 -2
  179. package/src/server/responses/ws-upstream.ts +14 -27
  180. package/src/server/responses-custom-tool-repair.ts +27 -54
  181. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  182. package/src/server/sse-payload-rewrite.ts +1 -1
  183. package/src/tray/windows-tray.ps1 +155 -3
  184. package/src/types/config.ts +11 -3
  185. package/src/types/provider.ts +18 -0
  186. package/src/types/request.ts +2 -0
  187. package/src/types/tools.ts +14 -0
  188. package/src/types.ts +1 -0
  189. package/src/vision/eligibility.ts +88 -9
  190. package/src/vision/plan.ts +34 -10
  191. package/src/web-search/executor.ts +41 -2
  192. package/src/web-search/loop.ts +6 -1
  193. package/gui/dist/assets/index-C5IebErG.js +0 -136
  194. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -0,0 +1,364 @@
1
+ /** Process-lifetime single-writer ownership for the shared spend journal. */
2
+ import { Database } from "bun:sqlite";
3
+ import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, realpathSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { getConfigDir } from "../config/paths";
6
+ import { recordOwnedConfigPath } from "./config-ownership";
7
+ import { assertNotRealHomeUnderTest } from "./test-home-guard";
8
+ import { hardenSecretDir, hardenSecretPath } from "./windows-secret-acl";
9
+
10
+ export const SPEND_LEDGER_OWNER_FILENAME = "spend-ledger-owner.sqlite";
11
+ export const SPEND_LEDGER_RESTART_PARENT_ENV = "OCX_SPEND_LEDGER_RESTART_PARENT_PID";
12
+ export const SPEND_LEDGER_RESTART_WAIT_MS = 5_000;
13
+ const OWNER_SIDECARS = ["-journal", "-wal", "-shm"] as const;
14
+
15
+ export type SpendLedgerOwnerErrorCode =
16
+ | "SPEND_LEDGER_OWNER_BUSY"
17
+ | "SPEND_LEDGER_OWNER_UNAVAILABLE"
18
+ | "SPEND_LEDGER_OWNER_HOME_CONFLICT"
19
+ | "SPEND_LEDGER_OWNER_NOT_HELD";
20
+
21
+ export class SpendLedgerOwnerError extends Error {
22
+ constructor(readonly code: SpendLedgerOwnerErrorCode, message: string, options?: ErrorOptions) {
23
+ super(message, options);
24
+ this.name = "SpendLedgerOwnerError";
25
+ }
26
+ }
27
+
28
+ export interface SpendLedgerOwnerLease {
29
+ release(): void;
30
+ }
31
+
32
+ interface ActiveOwner {
33
+ readonly home: string;
34
+ readonly database: Database;
35
+ references: number;
36
+ }
37
+
38
+ let activeOwner: ActiveOwner | null = null;
39
+ let boundLedgerHome: string | null = null;
40
+ /**
41
+ * Identity of the current ownership, not just of the home it covers.
42
+ *
43
+ * A home name alone cannot tell "still the lease I was built under" from "the same directory,
44
+ * owned again since". A ledger that only checked the home therefore came back to life across a
45
+ * release and a reacquire, writing totals it had accumulated before another writer owned the
46
+ * journal. Every acquisition that actually takes the lock mints a new value.
47
+ */
48
+ let ownerGeneration = 0;
49
+ const releaseHooks: Array<() => void> = [];
50
+
51
+ /**
52
+ * Run when the last lease on a state directory goes away.
53
+ *
54
+ * The ledger singleton is bound to the home it was built for, so it has to go when ownership
55
+ * does; otherwise a process that starts a second server against a different state directory
56
+ * either writes the old home's journal or is refused for a conflict it no longer has. The
57
+ * journal on disk is the durable record and construction replays it, so nothing is lost.
58
+ */
59
+ export function onSpendLedgerOwnerReleased(hook: () => void): void {
60
+ releaseHooks.push(hook);
61
+ }
62
+
63
+ function errorCode(error: unknown): unknown {
64
+ return error !== null && typeof error === "object" && "code" in error ? error.code : undefined;
65
+ }
66
+
67
+ function assertPrivateFile(path: string): void {
68
+ const stat = lstatSync(path);
69
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
70
+ throw new SpendLedgerOwnerError(
71
+ "SPEND_LEDGER_OWNER_UNAVAILABLE",
72
+ "Spend-ledger ownership could not be established safely.",
73
+ );
74
+ }
75
+ if (process.platform !== "win32" && stat.uid !== process.getuid!()) {
76
+ throw new SpendLedgerOwnerError(
77
+ "SPEND_LEDGER_OWNER_UNAVAILABLE",
78
+ "Spend-ledger ownership could not be established safely.",
79
+ );
80
+ }
81
+ }
82
+
83
+ function prepareOwnerPath(configDir: string): { home: string; path: string } {
84
+ const requested = resolve(configDir);
85
+ // First statement before mutation: tests must never acquire against the real user home.
86
+ assertNotRealHomeUnderTest(requested);
87
+ mkdirSync(requested, { recursive: true, mode: 0o700 });
88
+ const home = realpathSync.native(requested);
89
+ if (process.platform !== "win32") chmodSync(home, 0o700);
90
+ hardenSecretDir(home, { required: true });
91
+ const path = join(home, SPEND_LEDGER_OWNER_FILENAME);
92
+ // Registered while the directory is still EMPTY. Config ownership refuses to claim a
93
+ // directory that already has contents, so creating the database first meant a fresh home
94
+ // never got an ownership marker at all: the manifest was never written, and the database and
95
+ // its sidecars were left behind by a later uninstall because nothing recorded them.
96
+ recordOwnedConfigPath(home, path);
97
+ for (const suffix of OWNER_SIDECARS) recordOwnedConfigPath(home, `${path}${suffix}`);
98
+ try { closeSync(openSync(path, "wx", 0o600)); }
99
+ catch (error) { if (errorCode(error) !== "EEXIST") throw error; }
100
+ assertPrivateFile(path);
101
+ if (process.platform !== "win32") chmodSync(path, 0o600);
102
+ hardenSecretPath(path, { required: true });
103
+ assertPrivateFile(path);
104
+ return { home: process.platform === "win32" ? home.toLowerCase() : home, path };
105
+ }
106
+
107
+ function isBusy(error: unknown): boolean {
108
+ const code = errorCode(error);
109
+ const message = error instanceof Error ? error.message : "";
110
+ return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED"
111
+ || /database (?:is|table is) locked/i.test(message);
112
+ }
113
+
114
+ function stateDirectoryIdentity(configDir: string): string {
115
+ const requested = resolve(configDir);
116
+ assertNotRealHomeUnderTest(requested);
117
+ let canonical = requested;
118
+ try { canonical = realpathSync.native(requested); } catch { /* acquisition owns creation */ }
119
+ return process.platform === "win32" ? canonical.toLowerCase() : canonical;
120
+ }
121
+
122
+ function restartHandoffWaitMs(): number {
123
+ const markedParent = process.env[SPEND_LEDGER_RESTART_PARENT_ENV];
124
+ delete process.env[SPEND_LEDGER_RESTART_PARENT_ENV];
125
+ return markedParent === String(process.ppid) ? SPEND_LEDGER_RESTART_WAIT_MS : 0;
126
+ }
127
+
128
+ /** Mark only a parent-exit restart child for bounded lease acquisition. */
129
+ export function spendLedgerRestartEnvironment(
130
+ source: NodeJS.ProcessEnv,
131
+ parentPid?: number,
132
+ ): NodeJS.ProcessEnv {
133
+ const env = { ...source };
134
+ if (parentPid === undefined) delete env[SPEND_LEDGER_RESTART_PARENT_ENV];
135
+ else env[SPEND_LEDGER_RESTART_PARENT_ENV] = String(parentPid);
136
+ return env;
137
+ }
138
+
139
+ /**
140
+ * Hold one SQLite write transaction until the final in-process reference releases it.
141
+ * SQLite and the OS release a crashed process; no PID, timestamp, TTL or lock-file unlink
142
+ * can evict a live owner or mistake a reused process identity for this lease.
143
+ */
144
+ export function acquireSpendLedgerOwner(configDir = getConfigDir()): SpendLedgerOwnerLease {
145
+ const requestedHome = stateDirectoryIdentity(configDir);
146
+ const busyTimeout = restartHandoffWaitMs();
147
+ if (boundLedgerHome !== null && boundLedgerHome !== requestedHome) {
148
+ throw new SpendLedgerOwnerError(
149
+ "SPEND_LEDGER_OWNER_HOME_CONFLICT",
150
+ "This process already owns the spend ledger for a different state directory.",
151
+ );
152
+ }
153
+ if (activeOwner) {
154
+ if (activeOwner.home !== requestedHome) {
155
+ throw new SpendLedgerOwnerError(
156
+ "SPEND_LEDGER_OWNER_HOME_CONFLICT",
157
+ "This process already owns the spend ledger for a different state directory.",
158
+ );
159
+ }
160
+ activeOwner.references += 1;
161
+ return leaseFor(activeOwner);
162
+ }
163
+
164
+ let prepared: { home: string; path: string };
165
+ try {
166
+ prepared = prepareOwnerPath(configDir);
167
+ } catch (cause) {
168
+ if (cause instanceof SpendLedgerOwnerError) throw cause;
169
+ throw new SpendLedgerOwnerError(
170
+ "SPEND_LEDGER_OWNER_UNAVAILABLE",
171
+ "Spend-ledger ownership could not be established safely.",
172
+ { cause },
173
+ );
174
+ }
175
+
176
+ let database: Database | undefined;
177
+ try {
178
+ database = new Database(prepared.path, { create: true });
179
+ database.exec(`PRAGMA locking_mode = NORMAL; PRAGMA busy_timeout = ${busyTimeout}; BEGIN IMMEDIATE`);
180
+ } catch (cause) {
181
+ try { database?.close(); } catch { /* preserve acquisition failure */ }
182
+ if (isBusy(cause)) {
183
+ throw new SpendLedgerOwnerError(
184
+ "SPEND_LEDGER_OWNER_BUSY",
185
+ "Another OpenCodex process already owns this spend ledger. Use a separate OPENCODEX_HOME for an independent instance.",
186
+ );
187
+ }
188
+ throw new SpendLedgerOwnerError(
189
+ "SPEND_LEDGER_OWNER_UNAVAILABLE",
190
+ "Spend-ledger ownership could not be established safely.",
191
+ { cause },
192
+ );
193
+ }
194
+
195
+ activeOwner = { home: prepared.home, database, references: 1 };
196
+ ownerGeneration += 1;
197
+ return leaseFor(activeOwner);
198
+ }
199
+
200
+ function leaseFor(owner: ActiveOwner): SpendLedgerOwnerLease {
201
+ let released = false;
202
+ return Object.freeze({
203
+ release(): void {
204
+ if (released) return;
205
+ released = true;
206
+ if (activeOwner !== owner || owner.references < 1) return;
207
+ owner.references -= 1;
208
+ if (owner.references > 0) return;
209
+ activeOwner = null;
210
+ boundLedgerHome = null;
211
+ for (const hook of releaseHooks) {
212
+ try { hook(); } catch { /* a discard hook must not mask a release failure */ }
213
+ }
214
+ let failure: unknown;
215
+ try { owner.database.exec("ROLLBACK"); } catch (error) { failure = error; }
216
+ try { owner.database.close(); } catch (error) { failure ??= error; }
217
+ if (failure !== undefined) {
218
+ throw new SpendLedgerOwnerError(
219
+ "SPEND_LEDGER_OWNER_UNAVAILABLE",
220
+ "Spend-ledger ownership could not be released cleanly.",
221
+ { cause: failure },
222
+ );
223
+ }
224
+ },
225
+ });
226
+ }
227
+
228
+ /** The ownership a ledger was built under, to be handed back on every later use. */
229
+ export function currentSpendLedgerOwnerGeneration(): number {
230
+ return ownerGeneration;
231
+ }
232
+
233
+ /**
234
+ * Permission to touch one file under the owned state directory, for as long as this exact
235
+ * ownership lasts.
236
+ *
237
+ * A callback supplied by the caller cannot be this proof: the caller can pass one that does
238
+ * nothing, which is how a "required guard" still allowed an unowned write. A marker carried ON
239
+ * the object is not proof either, because an object spread copies it: `{ ...minted, path:
240
+ * elsewhere, assert() {} }` would look minted while pointing somewhere else. So the token
241
+ * carries nothing. The path and the ownership it was minted under live in a table only this
242
+ * module can read, keyed by the token's identity, and a copy is simply not that key.
243
+ */
244
+ export interface SpendLedgerStorage {
245
+ readonly __spendLedgerStorage: unique symbol;
246
+ }
247
+
248
+ const mintedStorage = new WeakMap<object, { readonly path: string; readonly generation: number; readonly home: string }>();
249
+
250
+ function trustedStorage(storage: SpendLedgerStorage): { readonly path: string; readonly generation: number; readonly home: string } {
251
+ const entry = typeof storage === "object" && storage !== null ? mintedStorage.get(storage) : undefined;
252
+ if (!entry) {
253
+ throw new SpendLedgerOwnerError(
254
+ "SPEND_LEDGER_OWNER_NOT_HELD",
255
+ "Spend-ledger ownership is required before the shared ledger can be used.",
256
+ );
257
+ }
258
+ return entry;
259
+ }
260
+
261
+ /**
262
+ * Mint storage permission for one file name directly under the owned state directory.
263
+ *
264
+ * The name is a file name, never a path: joining a caller-supplied path would let the caller
265
+ * choose the destination, which is the thing ownership is supposed to decide.
266
+ */
267
+ export function mintSpendLedgerStorage(fileName: string): SpendLedgerStorage {
268
+ if (fileName.includes("/") || fileName.includes("\\") || fileName === "." || fileName === "..") {
269
+ throw new SpendLedgerOwnerError(
270
+ "SPEND_LEDGER_OWNER_UNAVAILABLE",
271
+ "Spend-ledger storage could not be established safely.",
272
+ );
273
+ }
274
+ assertSpendLedgerOwnerHeld();
275
+ const owner = activeOwner;
276
+ if (!owner) {
277
+ throw new SpendLedgerOwnerError(
278
+ "SPEND_LEDGER_OWNER_NOT_HELD",
279
+ "Spend-ledger ownership is required before the shared ledger can be used.",
280
+ );
281
+ }
282
+ const generation = ownerGeneration;
283
+ const home = owner.home;
284
+ // The token is deliberately empty: everything trustworthy about it is held here, under its
285
+ // identity, where a spread cannot reach.
286
+ const token = Object.freeze({}) as unknown as SpendLedgerStorage;
287
+ mintedStorage.set(token, { path: join(home, fileName), generation, home });
288
+ return token;
289
+ }
290
+
291
+ /** The file this token stands for, as this module recorded it at mint time. */
292
+ export function spendLedgerStoragePath(storage: SpendLedgerStorage): string {
293
+ return trustedStorage(storage).path;
294
+ }
295
+
296
+ /**
297
+ * Refuse unless this exact token was minted here and its ownership is still current.
298
+ *
299
+ * Both halves matter: identity rules out a copy or a look-alike, and the generation rules out a
300
+ * token minted before the lock was dropped and taken again.
301
+ */
302
+ export function assertStorageOwned(storage: SpendLedgerStorage): void {
303
+ const entry = trustedStorage(storage);
304
+ assertSpendLedgerOwnerGeneration(entry.generation, entry.home);
305
+ }
306
+
307
+ /**
308
+ * Refuse a handle built under an ownership that has since ended.
309
+ *
310
+ * The state directory may be the same one; what matters is that the lock was dropped and taken
311
+ * again in between, because another process could have written the journal while it was free.
312
+ */
313
+ export function assertSpendLedgerOwnerGeneration(generation: number, configDir = getConfigDir()): void {
314
+ assertSpendLedgerOwnerHeld(configDir);
315
+ if (generation !== ownerGeneration) {
316
+ throw new SpendLedgerOwnerError(
317
+ "SPEND_LEDGER_OWNER_NOT_HELD",
318
+ "Spend-ledger ownership is required before the shared ledger can be used.",
319
+ );
320
+ }
321
+ }
322
+
323
+ /** The singleton journal may be touched only while its matching state directory is owned. */
324
+ export function assertSpendLedgerOwnerHeld(configDir = getConfigDir()): void {
325
+ let home: string;
326
+ try {
327
+ const canonical = realpathSync.native(resolve(configDir));
328
+ home = process.platform === "win32" ? canonical.toLowerCase() : canonical;
329
+ } catch (cause) {
330
+ throw new SpendLedgerOwnerError(
331
+ "SPEND_LEDGER_OWNER_NOT_HELD",
332
+ "Spend-ledger ownership is required before the shared ledger can be used.",
333
+ { cause },
334
+ );
335
+ }
336
+ if (!activeOwner || activeOwner.home !== home || activeOwner.references < 1) {
337
+ throw new SpendLedgerOwnerError(
338
+ "SPEND_LEDGER_OWNER_NOT_HELD",
339
+ "Spend-ledger ownership is required before the shared ledger can be used.",
340
+ );
341
+ }
342
+ }
343
+
344
+ /** Permanently bind the live process-wide singleton to its first constructed home. */
345
+ export function bindSpendLedgerOwnerHome(configDir = getConfigDir()): void {
346
+ assertSpendLedgerOwnerHeld(configDir);
347
+ const home = stateDirectoryIdentity(configDir);
348
+ if (boundLedgerHome !== null && boundLedgerHome !== home) {
349
+ throw new SpendLedgerOwnerError(
350
+ "SPEND_LEDGER_OWNER_HOME_CONFLICT",
351
+ "This process already owns the spend ledger for a different state directory.",
352
+ );
353
+ }
354
+ boundLedgerHome = home;
355
+ }
356
+
357
+ export function spendLedgerOwnerSnapshot(): { readonly ownership: "held" | "unheld" } {
358
+ return { ownership: activeOwner ? "held" : "unheld" };
359
+ }
360
+
361
+ /** Test seam paired with discarding the process-wide ledger singleton. */
362
+ export function resetSpendLedgerOwnerBindingForTest(): void {
363
+ boundLedgerHome = null;
364
+ }