@cogcoin/client 0.5.15 → 1.0.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.
Files changed (174) hide show
  1. package/README.md +80 -25
  2. package/dist/app-paths.d.ts +5 -6
  3. package/dist/app-paths.js +8 -16
  4. package/dist/art/balance.txt +10 -0
  5. package/dist/art/welcome.txt +16 -0
  6. package/dist/bitcoind/bootstrap/controller.d.ts +1 -0
  7. package/dist/bitcoind/bootstrap/controller.js +53 -1
  8. package/dist/bitcoind/client/follow-block-times.d.ts +1 -0
  9. package/dist/bitcoind/client/follow-block-times.js +1 -1
  10. package/dist/bitcoind/client/internal-types.d.ts +7 -3
  11. package/dist/bitcoind/client/managed-client.d.ts +4 -2
  12. package/dist/bitcoind/client/managed-client.js +14 -0
  13. package/dist/bitcoind/client/sync-engine.js +72 -11
  14. package/dist/bitcoind/hash-order.d.ts +4 -0
  15. package/dist/bitcoind/hash-order.js +13 -0
  16. package/dist/bitcoind/indexer-daemon-main.js +11 -3
  17. package/dist/bitcoind/normalize.js +3 -2
  18. package/dist/bitcoind/processing-start-height.d.ts +5 -0
  19. package/dist/bitcoind/processing-start-height.js +7 -0
  20. package/dist/bitcoind/progress/constants.d.ts +4 -0
  21. package/dist/bitcoind/progress/constants.js +4 -0
  22. package/dist/bitcoind/progress/controller.d.ts +2 -1
  23. package/dist/bitcoind/progress/controller.js +3 -3
  24. package/dist/bitcoind/progress/follow-scene.d.ts +6 -2
  25. package/dist/bitcoind/progress/follow-scene.js +29 -6
  26. package/dist/bitcoind/progress/formatting.d.ts +1 -0
  27. package/dist/bitcoind/progress/formatting.js +6 -0
  28. package/dist/bitcoind/progress/train-scene.js +37 -18
  29. package/dist/bitcoind/progress/tty-renderer.d.ts +6 -1
  30. package/dist/bitcoind/progress/tty-renderer.js +8 -4
  31. package/dist/bitcoind/rpc.d.ts +2 -1
  32. package/dist/bitcoind/rpc.js +3 -0
  33. package/dist/bitcoind/types.d.ts +6 -0
  34. package/dist/bytes.d.ts +1 -0
  35. package/dist/bytes.js +3 -0
  36. package/dist/cli/art.d.ts +2 -0
  37. package/dist/cli/art.js +37 -0
  38. package/dist/cli/commands/client-admin.d.ts +2 -0
  39. package/dist/cli/commands/client-admin.js +91 -0
  40. package/dist/cli/commands/follow.js +0 -2
  41. package/dist/cli/commands/mining-admin.js +6 -47
  42. package/dist/cli/commands/mining-read.js +11 -50
  43. package/dist/cli/commands/mining-runtime.js +142 -5
  44. package/dist/cli/commands/service-runtime.js +0 -2
  45. package/dist/cli/commands/status.js +8 -2
  46. package/dist/cli/commands/sync.js +49 -92
  47. package/dist/cli/commands/wallet-admin.js +142 -136
  48. package/dist/cli/commands/wallet-mutation.js +91 -79
  49. package/dist/cli/commands/wallet-read.js +15 -18
  50. package/dist/cli/context.js +5 -14
  51. package/dist/cli/mining-format.d.ts +0 -1
  52. package/dist/cli/mining-format.js +5 -37
  53. package/dist/cli/mining-json.d.ts +0 -18
  54. package/dist/cli/mining-json.js +0 -35
  55. package/dist/cli/mutation-command-groups.d.ts +1 -2
  56. package/dist/cli/mutation-command-groups.js +0 -5
  57. package/dist/cli/mutation-json.d.ts +24 -145
  58. package/dist/cli/mutation-json.js +30 -136
  59. package/dist/cli/mutation-resolved-json.d.ts +0 -7
  60. package/dist/cli/mutation-resolved-json.js +4 -10
  61. package/dist/cli/mutation-success.d.ts +2 -0
  62. package/dist/cli/mutation-success.js +11 -1
  63. package/dist/cli/mutation-text-format.js +1 -3
  64. package/dist/cli/output.d.ts +1 -1
  65. package/dist/cli/output.js +254 -231
  66. package/dist/cli/parse.d.ts +1 -1
  67. package/dist/cli/parse.js +93 -122
  68. package/dist/cli/preview-json.d.ts +17 -120
  69. package/dist/cli/preview-json.js +14 -97
  70. package/dist/cli/prompt.js +8 -13
  71. package/dist/cli/read-json.d.ts +15 -37
  72. package/dist/cli/read-json.js +44 -140
  73. package/dist/cli/runner.js +10 -13
  74. package/dist/cli/sync-progress.d.ts +6 -0
  75. package/dist/cli/sync-progress.js +91 -0
  76. package/dist/cli/types.d.ts +9 -17
  77. package/dist/cli/types.js +0 -2
  78. package/dist/cli/wallet-format.d.ts +1 -0
  79. package/dist/cli/wallet-format.js +208 -144
  80. package/dist/cli/workflow-hints.d.ts +3 -3
  81. package/dist/cli/workflow-hints.js +11 -8
  82. package/dist/client/default-client.d.ts +3 -1
  83. package/dist/client/default-client.js +45 -2
  84. package/dist/client/factory.js +1 -1
  85. package/dist/client/initialization.js +23 -0
  86. package/dist/client/persistence.js +5 -5
  87. package/dist/client/store-adapter.js +1 -0
  88. package/dist/sqlite/checkpoints.d.ts +1 -0
  89. package/dist/sqlite/checkpoints.js +7 -0
  90. package/dist/sqlite/store.js +14 -1
  91. package/dist/types.d.ts +1 -0
  92. package/dist/wallet/coin-control.d.ts +41 -12
  93. package/dist/wallet/coin-control.js +100 -428
  94. package/dist/wallet/descriptor-normalization.d.ts +1 -3
  95. package/dist/wallet/descriptor-normalization.js +0 -16
  96. package/dist/wallet/lifecycle.d.ts +7 -99
  97. package/dist/wallet/lifecycle.js +513 -968
  98. package/dist/wallet/managed-core-wallet.d.ts +13 -0
  99. package/dist/wallet/managed-core-wallet.js +20 -0
  100. package/dist/wallet/mining/constants.d.ts +5 -12
  101. package/dist/wallet/mining/constants.js +5 -12
  102. package/dist/wallet/mining/control.d.ts +1 -13
  103. package/dist/wallet/mining/control.js +45 -349
  104. package/dist/wallet/mining/index.d.ts +4 -5
  105. package/dist/wallet/mining/index.js +2 -3
  106. package/dist/wallet/mining/runner.d.ts +123 -13
  107. package/dist/wallet/mining/runner.js +899 -511
  108. package/dist/wallet/mining/runtime-artifacts.js +23 -3
  109. package/dist/wallet/mining/sentence-protocol.d.ts +44 -0
  110. package/dist/wallet/mining/sentence-protocol.js +123 -0
  111. package/dist/wallet/mining/sentences.d.ts +4 -8
  112. package/dist/wallet/mining/sentences.js +3 -52
  113. package/dist/wallet/mining/state.d.ts +11 -6
  114. package/dist/wallet/mining/state.js +7 -6
  115. package/dist/wallet/mining/types.d.ts +2 -30
  116. package/dist/wallet/mining/visualizer.d.ts +31 -3
  117. package/dist/wallet/mining/visualizer.js +135 -13
  118. package/dist/wallet/read/context.d.ts +0 -2
  119. package/dist/wallet/read/context.js +119 -140
  120. package/dist/wallet/read/filter.js +2 -11
  121. package/dist/wallet/read/index.d.ts +1 -1
  122. package/dist/wallet/read/project.js +24 -77
  123. package/dist/wallet/read/types.d.ts +10 -25
  124. package/dist/wallet/reset.d.ts +0 -1
  125. package/dist/wallet/reset.js +60 -138
  126. package/dist/wallet/root-resolution.d.ts +1 -5
  127. package/dist/wallet/root-resolution.js +0 -18
  128. package/dist/wallet/runtime.d.ts +0 -6
  129. package/dist/wallet/runtime.js +0 -8
  130. package/dist/wallet/state/client-password-agent.js +208 -0
  131. package/dist/wallet/state/client-password.d.ts +65 -0
  132. package/dist/wallet/state/client-password.js +952 -0
  133. package/dist/wallet/state/crypto.d.ts +1 -20
  134. package/dist/wallet/state/crypto.js +0 -63
  135. package/dist/wallet/state/provider.d.ts +23 -11
  136. package/dist/wallet/state/provider.js +248 -290
  137. package/dist/wallet/state/storage.d.ts +2 -2
  138. package/dist/wallet/state/storage.js +48 -16
  139. package/dist/wallet/tx/anchor.d.ts +3 -28
  140. package/dist/wallet/tx/anchor.js +349 -1250
  141. package/dist/wallet/tx/bitcoin-transfer.d.ts +35 -0
  142. package/dist/wallet/tx/bitcoin-transfer.js +200 -0
  143. package/dist/wallet/tx/cog.d.ts +5 -1
  144. package/dist/wallet/tx/cog.js +149 -185
  145. package/dist/wallet/tx/common.d.ts +61 -8
  146. package/dist/wallet/tx/common.js +266 -146
  147. package/dist/wallet/tx/domain-admin.d.ts +3 -1
  148. package/dist/wallet/tx/domain-admin.js +61 -99
  149. package/dist/wallet/tx/domain-market.d.ts +5 -1
  150. package/dist/wallet/tx/domain-market.js +221 -228
  151. package/dist/wallet/tx/field.d.ts +4 -10
  152. package/dist/wallet/tx/field.js +83 -924
  153. package/dist/wallet/tx/identity-selector.d.ts +9 -3
  154. package/dist/wallet/tx/identity-selector.js +17 -35
  155. package/dist/wallet/tx/index.d.ts +3 -1
  156. package/dist/wallet/tx/index.js +2 -1
  157. package/dist/wallet/tx/register.d.ts +3 -1
  158. package/dist/wallet/tx/register.js +62 -220
  159. package/dist/wallet/tx/reputation.d.ts +3 -1
  160. package/dist/wallet/tx/reputation.js +58 -95
  161. package/dist/wallet/types.d.ts +8 -122
  162. package/package.json +5 -5
  163. package/dist/wallet/archive.d.ts +0 -4
  164. package/dist/wallet/archive.js +0 -41
  165. package/dist/wallet/mining/hook-protocol.d.ts +0 -47
  166. package/dist/wallet/mining/hook-protocol.js +0 -161
  167. package/dist/wallet/mining/hook-runner.js +0 -52
  168. package/dist/wallet/mining/hooks.d.ts +0 -38
  169. package/dist/wallet/mining/hooks.js +0 -520
  170. package/dist/wallet/state/explicit-lock.d.ts +0 -4
  171. package/dist/wallet/state/explicit-lock.js +0 -19
  172. package/dist/wallet/state/session.d.ts +0 -12
  173. package/dist/wallet/state/session.js +0 -23
  174. /package/dist/wallet/{mining/hook-runner.d.ts → state/client-password-agent.d.ts} +0 -0
@@ -0,0 +1,952 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { mkdir, readFile, readdir, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import net from "node:net";
7
+ import { fileURLToPath } from "node:url";
8
+ import { argon2idAsync } from "@noble/hashes/argon2.js";
9
+ import { writeFileAtomic, writeJsonFileAtomic } from "../fs/atomic.js";
10
+ import { decryptBytesWithKey, encryptBytesWithKey } from "./crypto.js";
11
+ const CLIENT_PASSWORD_STATE_FORMAT = "cogcoin-client-password";
12
+ const CLIENT_PASSWORD_ROTATION_JOURNAL_FORMAT = "cogcoin-client-password-rotation";
13
+ const CLIENT_PASSWORD_VERIFIER_FORMAT = "cogcoin-client-password-verifier";
14
+ const LOCAL_SECRET_ENVELOPE_FORMAT = "cogcoin-local-wallet-secret";
15
+ const CLIENT_PASSWORD_VERIFIER_TEXT = "cogcoin-client-password-verifier-v1";
16
+ const CLIENT_PASSWORD_MANUAL_UNLOCK_SECONDS = 60;
17
+ export const CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS = 86_400;
18
+ const CLIENT_PASSWORD_DERIVED_KEY_BYTES = 32;
19
+ const CLIENT_PASSWORD_KDF = {
20
+ memoryKib: 65_536,
21
+ iterations: 3,
22
+ parallelism: 1,
23
+ };
24
+ function sanitizeSecretKeyId(keyId) {
25
+ return keyId.replace(/[^a-zA-Z0-9._-]+/g, "-");
26
+ }
27
+ export function resolveLocalSecretFilePath(directoryPath, keyId) {
28
+ return join(directoryPath, `${sanitizeSecretKeyId(keyId)}.secret`);
29
+ }
30
+ function resolveClientPasswordStatePath(directoryPath) {
31
+ return join(directoryPath, "client-password.json");
32
+ }
33
+ function resolveClientPasswordRotationJournalPath(directoryPath) {
34
+ return join(directoryPath, "client-password-rotation.json");
35
+ }
36
+ function resolveAgentEndpoint(platform, stateRoot) {
37
+ const hash = createHash("sha256").update(stateRoot).digest("hex").slice(0, 24);
38
+ if (platform === "win32") {
39
+ return `\\\\.\\pipe\\cogcoin-client-password-${hash}`;
40
+ }
41
+ return join(tmpdir(), `cogcoin-client-password-${hash}.sock`);
42
+ }
43
+ function isMissingFileError(error) {
44
+ return error instanceof Error
45
+ && "code" in error
46
+ && error.code === "ENOENT";
47
+ }
48
+ function createRuntimeError(code, cause) {
49
+ return cause === undefined ? new Error(code) : new Error(code, { cause });
50
+ }
51
+ function isClientPasswordStateV1(value) {
52
+ return value !== null
53
+ && typeof value === "object"
54
+ && value.format === CLIENT_PASSWORD_STATE_FORMAT
55
+ && value.version === 1
56
+ && typeof value.passwordHint === "string"
57
+ && value.kdf?.name === "argon2id"
58
+ && typeof value.verifier?.nonce === "string"
59
+ && typeof value.verifier?.tag === "string"
60
+ && typeof value.verifier?.ciphertext === "string";
61
+ }
62
+ function isWrappedSecretEnvelope(value) {
63
+ return value !== null
64
+ && typeof value === "object"
65
+ && value.format === LOCAL_SECRET_ENVELOPE_FORMAT
66
+ && value.version === 1
67
+ && value.cipher === "aes-256-gcm"
68
+ && value.wrappedBy === "client-password"
69
+ && typeof value.nonce === "string"
70
+ && typeof value.tag === "string"
71
+ && typeof value.ciphertext === "string";
72
+ }
73
+ function isClientPasswordRotationJournalV1(value) {
74
+ return value !== null
75
+ && typeof value === "object"
76
+ && value.format === CLIENT_PASSWORD_ROTATION_JOURNAL_FORMAT
77
+ && value.version === 1
78
+ && isClientPasswordStateV1(value.nextState)
79
+ && Array.isArray(value.secrets)
80
+ && (value.secrets).every((entry) => (entry !== null
81
+ && typeof entry === "object"
82
+ && typeof entry.keyId === "string"
83
+ && entry.keyId.trim().length > 0
84
+ && isWrappedSecretEnvelope(entry.envelope)));
85
+ }
86
+ async function readLocalSecretFile(path) {
87
+ try {
88
+ const raw = await readFile(path, "utf8");
89
+ const trimmed = raw.trim();
90
+ try {
91
+ const parsed = JSON.parse(trimmed);
92
+ if (isWrappedSecretEnvelope(parsed)) {
93
+ return {
94
+ state: "wrapped",
95
+ envelope: parsed,
96
+ };
97
+ }
98
+ }
99
+ catch {
100
+ // Legacy local secrets were raw base64 bytes.
101
+ }
102
+ return {
103
+ state: "raw",
104
+ secret: new Uint8Array(Buffer.from(trimmed, "base64")),
105
+ };
106
+ }
107
+ catch (error) {
108
+ if (isMissingFileError(error)) {
109
+ return { state: "missing" };
110
+ }
111
+ throw error;
112
+ }
113
+ }
114
+ async function loadClientPasswordStateOrNull(path) {
115
+ try {
116
+ const parsed = JSON.parse(await readFile(path, "utf8"));
117
+ if (!isClientPasswordStateV1(parsed)) {
118
+ return null;
119
+ }
120
+ return parsed;
121
+ }
122
+ catch (error) {
123
+ if (isMissingFileError(error)) {
124
+ return null;
125
+ }
126
+ return null;
127
+ }
128
+ }
129
+ async function loadClientPasswordRotationJournalOrNull(path) {
130
+ try {
131
+ const parsed = JSON.parse(await readFile(path, "utf8"));
132
+ if (!isClientPasswordRotationJournalV1(parsed)) {
133
+ return null;
134
+ }
135
+ return parsed;
136
+ }
137
+ catch (error) {
138
+ if (isMissingFileError(error)) {
139
+ return null;
140
+ }
141
+ return null;
142
+ }
143
+ }
144
+ async function derivePasswordKey(passwordBytes, saltBytes) {
145
+ return Buffer.from(await argon2idAsync(passwordBytes, saltBytes, {
146
+ m: CLIENT_PASSWORD_KDF.memoryKib,
147
+ t: CLIENT_PASSWORD_KDF.iterations,
148
+ p: CLIENT_PASSWORD_KDF.parallelism,
149
+ dkLen: CLIENT_PASSWORD_DERIVED_KEY_BYTES,
150
+ }));
151
+ }
152
+ function zeroizeBuffer(buffer) {
153
+ if (buffer != null) {
154
+ buffer.fill(0);
155
+ }
156
+ }
157
+ async function createClientPasswordState(options) {
158
+ const salt = randomBytes(16);
159
+ const derivedKey = await derivePasswordKey(options.passwordBytes, salt);
160
+ const verifier = encryptBytesWithKey(Buffer.from(CLIENT_PASSWORD_VERIFIER_TEXT, "utf8"), derivedKey, {
161
+ format: CLIENT_PASSWORD_VERIFIER_FORMAT,
162
+ wrappedBy: "client-password-verifier",
163
+ });
164
+ return {
165
+ state: {
166
+ format: CLIENT_PASSWORD_STATE_FORMAT,
167
+ version: 1,
168
+ passwordHint: options.passwordHint,
169
+ kdf: {
170
+ name: "argon2id",
171
+ memoryKib: CLIENT_PASSWORD_KDF.memoryKib,
172
+ iterations: CLIENT_PASSWORD_KDF.iterations,
173
+ parallelism: CLIENT_PASSWORD_KDF.parallelism,
174
+ salt: salt.toString("base64"),
175
+ },
176
+ verifier: {
177
+ cipher: "aes-256-gcm",
178
+ nonce: verifier.nonce,
179
+ tag: verifier.tag,
180
+ ciphertext: verifier.ciphertext,
181
+ },
182
+ },
183
+ derivedKey,
184
+ };
185
+ }
186
+ function createWrappedSecretEnvelope(secret, derivedKey) {
187
+ const envelope = encryptBytesWithKey(secret, derivedKey, {
188
+ format: LOCAL_SECRET_ENVELOPE_FORMAT,
189
+ wrappedBy: "client-password",
190
+ });
191
+ return {
192
+ format: LOCAL_SECRET_ENVELOPE_FORMAT,
193
+ version: 1,
194
+ cipher: "aes-256-gcm",
195
+ wrappedBy: "client-password",
196
+ nonce: envelope.nonce,
197
+ tag: envelope.tag,
198
+ ciphertext: envelope.ciphertext,
199
+ };
200
+ }
201
+ async function verifyPassword(options) {
202
+ const derivedKey = await derivePasswordKey(options.passwordBytes, Buffer.from(options.state.kdf.salt, "base64"));
203
+ try {
204
+ const plaintext = decryptBytesWithKey({
205
+ format: CLIENT_PASSWORD_VERIFIER_FORMAT,
206
+ version: 1,
207
+ cipher: "aes-256-gcm",
208
+ wrappedBy: "client-password-verifier",
209
+ nonce: options.state.verifier.nonce,
210
+ tag: options.state.verifier.tag,
211
+ ciphertext: options.state.verifier.ciphertext,
212
+ }, derivedKey);
213
+ if (plaintext.toString("utf8") !== CLIENT_PASSWORD_VERIFIER_TEXT) {
214
+ zeroizeBuffer(derivedKey);
215
+ return null;
216
+ }
217
+ return derivedKey;
218
+ }
219
+ catch {
220
+ zeroizeBuffer(derivedKey);
221
+ return null;
222
+ }
223
+ }
224
+ async function collectWalletStateRoots(stateRoot) {
225
+ const roots = [stateRoot];
226
+ const seedsRoot = join(stateRoot, "seeds");
227
+ try {
228
+ const entries = await readdir(seedsRoot, { withFileTypes: true });
229
+ for (const entry of entries) {
230
+ if (entry.isDirectory()) {
231
+ roots.push(join(seedsRoot, entry.name));
232
+ }
233
+ }
234
+ }
235
+ catch (error) {
236
+ if (!isMissingFileError(error)) {
237
+ throw error;
238
+ }
239
+ }
240
+ return roots;
241
+ }
242
+ async function readReferencedSecretIdsFromWalletStateRoot(walletStateRoot) {
243
+ const ids = new Set();
244
+ const candidatePaths = [
245
+ join(walletStateRoot, "wallet-state.enc"),
246
+ join(walletStateRoot, "wallet-state.enc.bak"),
247
+ join(walletStateRoot, "wallet-init-pending.enc"),
248
+ join(walletStateRoot, "wallet-init-pending.enc.bak"),
249
+ ];
250
+ for (const candidatePath of candidatePaths) {
251
+ try {
252
+ const parsed = JSON.parse(await readFile(candidatePath, "utf8"));
253
+ const keyId = parsed.secretProvider?.keyId?.trim() ?? "";
254
+ if (keyId.length > 0) {
255
+ ids.add(keyId);
256
+ }
257
+ }
258
+ catch (error) {
259
+ if (!isMissingFileError(error)) {
260
+ continue;
261
+ }
262
+ }
263
+ }
264
+ return ids;
265
+ }
266
+ async function collectReferencedSecretIds(stateRoot) {
267
+ const ids = new Set();
268
+ const roots = await collectWalletStateRoots(stateRoot);
269
+ for (const root of roots) {
270
+ const rootIds = await readReferencedSecretIdsFromWalletStateRoot(root);
271
+ for (const keyId of rootIds) {
272
+ ids.add(keyId);
273
+ }
274
+ }
275
+ return [...ids].sort((left, right) => left.localeCompare(right));
276
+ }
277
+ async function finalizePendingClientPasswordRotationIfNeeded(options) {
278
+ const journal = await loadClientPasswordRotationJournalOrNull(resolveClientPasswordRotationJournalPath(options.directoryPath));
279
+ if (journal === null) {
280
+ return;
281
+ }
282
+ await mkdir(options.directoryPath, { recursive: true, mode: 0o700 });
283
+ for (const secretEntry of journal.secrets) {
284
+ await writeJsonFileAtomic(resolveLocalSecretFilePath(options.directoryPath, secretEntry.keyId), secretEntry.envelope, { mode: 0o600 });
285
+ }
286
+ await writeJsonFileAtomic(resolveClientPasswordStatePath(options.directoryPath), journal.nextState, { mode: 0o600 });
287
+ await rm(resolveClientPasswordRotationJournalPath(options.directoryPath), { force: true });
288
+ }
289
+ async function legacyMacKeychainHasSecret(options, keyId) {
290
+ if (options.platform !== "darwin" || options.legacyMacKeychainReader == null) {
291
+ return false;
292
+ }
293
+ try {
294
+ await options.legacyMacKeychainReader.loadSecret(keyId);
295
+ return true;
296
+ }
297
+ catch {
298
+ return false;
299
+ }
300
+ }
301
+ async function inspectReadinessForKey(options, keyId) {
302
+ const local = await readLocalSecretFile(resolveLocalSecretFilePath(options.directoryPath, keyId));
303
+ const keychain = await legacyMacKeychainHasSecret(options, keyId);
304
+ return { local, keychain };
305
+ }
306
+ export async function inspectClientPasswordReadiness(options) {
307
+ await finalizePendingClientPasswordRotationIfNeeded(options);
308
+ const passwordState = await loadClientPasswordStateOrNull(resolveClientPasswordStatePath(options.directoryPath));
309
+ const keyIds = await collectReferencedSecretIds(options.stateRoot);
310
+ if (keyIds.length === 0) {
311
+ return passwordState === null ? "setup-required" : "ready";
312
+ }
313
+ for (const keyId of keyIds) {
314
+ const sourceState = await inspectReadinessForKey(options, keyId);
315
+ if (passwordState === null) {
316
+ if (sourceState.local.state === "raw" || sourceState.keychain) {
317
+ return "migration-required";
318
+ }
319
+ continue;
320
+ }
321
+ if (sourceState.local.state === "raw") {
322
+ return "migration-required";
323
+ }
324
+ if (sourceState.local.state === "missing" && sourceState.keychain) {
325
+ return "migration-required";
326
+ }
327
+ }
328
+ return passwordState === null ? "setup-required" : "ready";
329
+ }
330
+ function describeReadinessError(readiness) {
331
+ return readiness === "migration-required"
332
+ ? "wallet_client_password_migration_required"
333
+ : "wallet_client_password_setup_required";
334
+ }
335
+ async function openAgentConnection(endpoint) {
336
+ return await new Promise((resolve, reject) => {
337
+ const socket = net.createConnection(endpoint);
338
+ const cleanup = () => {
339
+ socket.off("connect", onConnect);
340
+ socket.off("error", onError);
341
+ };
342
+ const onConnect = () => {
343
+ cleanup();
344
+ resolve(socket);
345
+ };
346
+ const onError = (error) => {
347
+ cleanup();
348
+ reject(error);
349
+ };
350
+ socket.on("connect", onConnect);
351
+ socket.on("error", onError);
352
+ });
353
+ }
354
+ async function requestAgent(options, request) {
355
+ const endpoint = resolveAgentEndpoint(options.platform, options.stateRoot);
356
+ const socket = await openAgentConnection(endpoint);
357
+ return await new Promise((resolve, reject) => {
358
+ let received = "";
359
+ const cleanup = () => {
360
+ socket.off("data", onData);
361
+ socket.off("error", onError);
362
+ socket.off("end", onEnd);
363
+ socket.off("close", onClose);
364
+ };
365
+ const finish = (response) => {
366
+ cleanup();
367
+ socket.end();
368
+ resolve(response);
369
+ };
370
+ const fail = (error) => {
371
+ cleanup();
372
+ socket.destroy();
373
+ reject(error);
374
+ };
375
+ const onData = (chunk) => {
376
+ received += chunk.toString("utf8");
377
+ const newlineIndex = received.indexOf("\n");
378
+ if (newlineIndex === -1) {
379
+ return;
380
+ }
381
+ try {
382
+ finish(JSON.parse(received.slice(0, newlineIndex)));
383
+ }
384
+ catch (error) {
385
+ fail(error instanceof Error ? error : new Error(String(error)));
386
+ }
387
+ };
388
+ const onError = (error) => {
389
+ fail(error);
390
+ };
391
+ const onEnd = () => {
392
+ if (received.length === 0) {
393
+ fail(new Error("wallet_client_password_locked"));
394
+ }
395
+ };
396
+ const onClose = () => {
397
+ if (received.length === 0) {
398
+ fail(new Error("wallet_client_password_locked"));
399
+ }
400
+ };
401
+ socket.on("data", onData);
402
+ socket.on("error", onError);
403
+ socket.on("end", onEnd);
404
+ socket.on("close", onClose);
405
+ socket.write(`${JSON.stringify(request)}\n`);
406
+ });
407
+ }
408
+ async function requestAgentOrNull(options, request) {
409
+ try {
410
+ return await requestAgent(options, request);
411
+ }
412
+ catch (error) {
413
+ const message = error instanceof Error ? error.message : String(error);
414
+ if (message === "wallet_client_password_locked") {
415
+ return null;
416
+ }
417
+ const code = error instanceof Error && "code" in error
418
+ ? String(error.code ?? "")
419
+ : "";
420
+ if (code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE") {
421
+ if (options.platform !== "win32") {
422
+ await rm(resolveAgentEndpoint(options.platform, options.stateRoot), { force: true }).catch(() => undefined);
423
+ }
424
+ return null;
425
+ }
426
+ throw error;
427
+ }
428
+ }
429
+ export async function readClientPasswordSessionStatus(options) {
430
+ const response = await requestAgentOrNull(options, { command: "status" });
431
+ if (response === null || !response.ok) {
432
+ return {
433
+ unlocked: false,
434
+ unlockUntilUnixMs: null,
435
+ };
436
+ }
437
+ return {
438
+ unlocked: true,
439
+ unlockUntilUnixMs: response.unlockUntilUnixMs ?? null,
440
+ };
441
+ }
442
+ export async function lockClientPasswordSession(options) {
443
+ await requestAgentOrNull(options, { command: "lock" }).catch(() => null);
444
+ if (options.platform !== "win32") {
445
+ await rm(resolveAgentEndpoint(options.platform, options.stateRoot), { force: true }).catch(() => undefined);
446
+ }
447
+ return {
448
+ unlocked: false,
449
+ unlockUntilUnixMs: null,
450
+ };
451
+ }
452
+ async function waitForAgentReady(child) {
453
+ const stdout = child.stdout;
454
+ if (stdout == null) {
455
+ throw new Error("wallet_client_password_agent_start_failed");
456
+ }
457
+ await new Promise((resolve, reject) => {
458
+ let received = "";
459
+ const cleanup = () => {
460
+ stdout.off("data", onData);
461
+ child.off("exit", onExit);
462
+ child.off("error", onError);
463
+ };
464
+ const onData = (chunk) => {
465
+ received += chunk.toString("utf8");
466
+ const newlineIndex = received.indexOf("\n");
467
+ if (newlineIndex === -1) {
468
+ return;
469
+ }
470
+ cleanup();
471
+ if (received.slice(0, newlineIndex).trim() === "ready") {
472
+ resolve();
473
+ return;
474
+ }
475
+ reject(new Error("wallet_client_password_agent_start_failed"));
476
+ };
477
+ const onExit = () => {
478
+ cleanup();
479
+ reject(new Error("wallet_client_password_agent_start_failed"));
480
+ };
481
+ const onError = (error) => {
482
+ cleanup();
483
+ reject(error);
484
+ };
485
+ stdout.on("data", onData);
486
+ child.on("exit", onExit);
487
+ child.on("error", onError);
488
+ });
489
+ }
490
+ function releaseAgentBootstrapHandles(child) {
491
+ child.stdin?.destroy();
492
+ child.stdout?.destroy();
493
+ }
494
+ async function startClientPasswordSession(options) {
495
+ return await startClientPasswordSessionWithExpiry({
496
+ ...options,
497
+ unlockUntilUnixMs: Date.now() + (options.unlockDurationSeconds * 1_000),
498
+ });
499
+ }
500
+ async function startClientPasswordSessionWithExpiry(options) {
501
+ const unlockUntilUnixMs = options.unlockUntilUnixMs;
502
+ const endpoint = resolveAgentEndpoint(options.platform, options.stateRoot);
503
+ await lockClientPasswordSession(options).catch(() => undefined);
504
+ await mkdir(options.runtimeRoot, { recursive: true }).catch(() => undefined);
505
+ const child = spawn(process.execPath, [fileURLToPath(new URL("./client-password-agent.js", import.meta.url)), endpoint, String(unlockUntilUnixMs)], {
506
+ detached: true,
507
+ stdio: ["pipe", "pipe", "ignore"],
508
+ });
509
+ try {
510
+ child.stdin?.end(`${JSON.stringify({
511
+ derivedKeyBase64: options.derivedKey.toString("base64"),
512
+ })}\n`);
513
+ await waitForAgentReady(child);
514
+ }
515
+ catch (error) {
516
+ child.kill();
517
+ throw error;
518
+ }
519
+ finally {
520
+ releaseAgentBootstrapHandles(child);
521
+ zeroizeBuffer(options.derivedKey);
522
+ }
523
+ child.unref();
524
+ return {
525
+ unlocked: true,
526
+ unlockUntilUnixMs,
527
+ };
528
+ }
529
+ async function promptForHiddenValue(prompt, message) {
530
+ const value = prompt.promptHidden != null
531
+ ? await prompt.promptHidden(message)
532
+ : await prompt.prompt(message);
533
+ return value.trim();
534
+ }
535
+ async function promptForUnlockDuration(prompt) {
536
+ return await promptForUnlockDurationWithDefault(prompt, CLIENT_PASSWORD_MANUAL_UNLOCK_SECONDS);
537
+ }
538
+ async function promptForUnlockDurationWithDefault(prompt, defaultSeconds) {
539
+ while (true) {
540
+ const answer = (await prompt.prompt(`Unlock duration in seconds [${defaultSeconds}]: `)).trim();
541
+ if (answer === "") {
542
+ return defaultSeconds;
543
+ }
544
+ if (/^[1-9]\d*$/.test(answer)) {
545
+ return Number(answer);
546
+ }
547
+ prompt.writeLine("Enter a whole-number duration in seconds.");
548
+ }
549
+ }
550
+ function resolveRemainingUnlockSeconds(status) {
551
+ if (status.unlockUntilUnixMs === null) {
552
+ return CLIENT_PASSWORD_MANUAL_UNLOCK_SECONDS;
553
+ }
554
+ return Math.max(1, Math.ceil((status.unlockUntilUnixMs - Date.now()) / 1_000));
555
+ }
556
+ function resolvePostChangeUnlockUntilUnixMs(status) {
557
+ if (status.unlocked && status.unlockUntilUnixMs != null) {
558
+ return status.unlockUntilUnixMs;
559
+ }
560
+ return Date.now() + (CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS * 1_000);
561
+ }
562
+ async function refreshClientPasswordSession(options) {
563
+ const response = await requestAgentOrNull(options, {
564
+ command: "refresh",
565
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
566
+ });
567
+ if (response === null || !response.ok) {
568
+ return null;
569
+ }
570
+ return {
571
+ unlocked: true,
572
+ unlockUntilUnixMs: response.unlockUntilUnixMs ?? options.unlockUntilUnixMs,
573
+ };
574
+ }
575
+ async function unlockClientPasswordSessionWithPrompt(options) {
576
+ const derivedKey = await promptForVerifiedClientPassword({
577
+ ...options,
578
+ promptMessage: "Client password: ",
579
+ ttyErrorCode: "wallet_client_password_unlock_requires_tty",
580
+ });
581
+ const unlockDurationSeconds = await promptForUnlockDuration(options.prompt);
582
+ return await startClientPasswordSession({
583
+ ...options,
584
+ derivedKey,
585
+ unlockDurationSeconds,
586
+ });
587
+ }
588
+ async function promptForVerifiedClientPassword(options) {
589
+ const readiness = await inspectClientPasswordReadiness(options);
590
+ if (readiness !== "ready") {
591
+ throw new Error(describeReadinessError(readiness));
592
+ }
593
+ if (!options.prompt.isInteractive) {
594
+ throw new Error(options.ttyErrorCode);
595
+ }
596
+ const state = await loadClientPasswordStateOrNull(resolveClientPasswordStatePath(options.directoryPath));
597
+ if (state === null) {
598
+ throw new Error("wallet_client_password_setup_required");
599
+ }
600
+ let attempts = 0;
601
+ while (true) {
602
+ if (attempts >= 2 && state.passwordHint.trim().length > 0) {
603
+ options.prompt.writeLine(`Hint: ${state.passwordHint}`);
604
+ }
605
+ const passwordText = await promptForHiddenValue(options.prompt, options.promptMessage);
606
+ const passwordBytes = Buffer.from(passwordText, "utf8");
607
+ let derivedKey = null;
608
+ try {
609
+ derivedKey = await verifyPassword({
610
+ state,
611
+ passwordBytes,
612
+ });
613
+ }
614
+ finally {
615
+ zeroizeBuffer(passwordBytes);
616
+ }
617
+ if (derivedKey !== null) {
618
+ return derivedKey;
619
+ }
620
+ attempts += 1;
621
+ options.prompt.writeLine("Incorrect client password.");
622
+ }
623
+ }
624
+ async function writeWrappedSecret(options) {
625
+ const envelope = createWrappedSecretEnvelope(options.secret, options.derivedKey);
626
+ await writeJsonFileAtomic(options.path, envelope, { mode: 0o600 });
627
+ }
628
+ async function migrateReferencedSecrets(options) {
629
+ const keyIds = await collectReferencedSecretIds(options.stateRoot);
630
+ let migrated = false;
631
+ for (const keyId of keyIds) {
632
+ const localPath = resolveLocalSecretFilePath(options.directoryPath, keyId);
633
+ const localState = await readLocalSecretFile(localPath);
634
+ if (localState.state === "wrapped") {
635
+ continue;
636
+ }
637
+ if (localState.state === "raw") {
638
+ await writeWrappedSecret({
639
+ path: localPath,
640
+ secret: localState.secret,
641
+ derivedKey: options.derivedKey,
642
+ });
643
+ migrated = true;
644
+ continue;
645
+ }
646
+ if (options.platform === "darwin" && options.legacyMacKeychainReader != null) {
647
+ try {
648
+ const secret = await options.legacyMacKeychainReader.loadSecret(keyId);
649
+ await writeWrappedSecret({
650
+ path: localPath,
651
+ secret,
652
+ derivedKey: options.derivedKey,
653
+ });
654
+ migrated = true;
655
+ }
656
+ catch {
657
+ // Best-effort legacy migration only.
658
+ }
659
+ }
660
+ }
661
+ return migrated;
662
+ }
663
+ async function promptForNewPassword(prompt) {
664
+ if (!prompt.isInteractive) {
665
+ throw new Error("wallet_client_password_setup_requires_tty");
666
+ }
667
+ while (true) {
668
+ const first = await promptForHiddenValue(prompt, "Create client password: ");
669
+ const firstBytes = Buffer.from(first, "utf8");
670
+ if (firstBytes.length === 0) {
671
+ zeroizeBuffer(firstBytes);
672
+ prompt.writeLine("Client password cannot be blank.");
673
+ continue;
674
+ }
675
+ const second = await promptForHiddenValue(prompt, "Confirm client password: ");
676
+ const secondBytes = Buffer.from(second, "utf8");
677
+ if (!firstBytes.equals(secondBytes)) {
678
+ zeroizeBuffer(firstBytes);
679
+ zeroizeBuffer(secondBytes);
680
+ prompt.writeLine("Client password entries did not match.");
681
+ continue;
682
+ }
683
+ zeroizeBuffer(secondBytes);
684
+ let passwordHint = "";
685
+ while (passwordHint.length === 0) {
686
+ passwordHint = (await prompt.prompt("Password hint: ")).trim();
687
+ if (passwordHint.length === 0) {
688
+ prompt.writeLine("Password hint cannot be blank.");
689
+ }
690
+ }
691
+ return {
692
+ passwordBytes: firstBytes,
693
+ passwordHint,
694
+ };
695
+ }
696
+ }
697
+ export async function ensureClientPasswordConfigured(options) {
698
+ await finalizePendingClientPasswordRotationIfNeeded(options);
699
+ const readiness = await inspectClientPasswordReadiness(options);
700
+ if (readiness === "ready") {
701
+ return {
702
+ action: "already-configured",
703
+ session: await readClientPasswordSessionStatus(options),
704
+ };
705
+ }
706
+ const setup = await promptForNewPassword(options.prompt);
707
+ let derivedKey = null;
708
+ try {
709
+ const created = await createClientPasswordState({
710
+ passwordBytes: setup.passwordBytes,
711
+ passwordHint: setup.passwordHint,
712
+ });
713
+ derivedKey = created.derivedKey;
714
+ await mkdir(options.directoryPath, { recursive: true, mode: 0o700 });
715
+ await writeJsonFileAtomic(resolveClientPasswordStatePath(options.directoryPath), created.state, { mode: 0o600 });
716
+ const migrated = await migrateReferencedSecrets({
717
+ ...options,
718
+ derivedKey,
719
+ });
720
+ const session = await startClientPasswordSession({
721
+ ...options,
722
+ derivedKey,
723
+ unlockDurationSeconds: CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS,
724
+ });
725
+ derivedKey = null;
726
+ return {
727
+ action: migrated || readiness === "migration-required" ? "migrated" : "created",
728
+ session,
729
+ };
730
+ }
731
+ finally {
732
+ zeroizeBuffer(setup.passwordBytes);
733
+ zeroizeBuffer(derivedKey);
734
+ }
735
+ }
736
+ async function decryptWrappedSecretWithSession(options) {
737
+ let response = await requestAgentOrNull(options, {
738
+ command: "decrypt",
739
+ envelope: options.envelope,
740
+ });
741
+ if (response === null && options.prompt != null && options.prompt.isInteractive) {
742
+ await unlockClientPasswordSessionWithPrompt({
743
+ ...options,
744
+ prompt: options.prompt,
745
+ });
746
+ response = await requestAgentOrNull(options, {
747
+ command: "decrypt",
748
+ envelope: options.envelope,
749
+ });
750
+ }
751
+ if (response === null || !response.ok || response.secretBase64 == null) {
752
+ throw new Error("wallet_client_password_locked");
753
+ }
754
+ return new Uint8Array(Buffer.from(response.secretBase64, "base64"));
755
+ }
756
+ async function encryptWrappedSecretWithSession(options) {
757
+ let response = await requestAgentOrNull(options, {
758
+ command: "encrypt",
759
+ secretBase64: Buffer.from(options.secret).toString("base64"),
760
+ });
761
+ if (response === null && options.prompt != null && options.prompt.isInteractive) {
762
+ await unlockClientPasswordSessionWithPrompt({
763
+ ...options,
764
+ prompt: options.prompt,
765
+ });
766
+ response = await requestAgentOrNull(options, {
767
+ command: "encrypt",
768
+ secretBase64: Buffer.from(options.secret).toString("base64"),
769
+ });
770
+ }
771
+ if (response === null || !response.ok || response.envelope == null) {
772
+ throw new Error("wallet_client_password_locked");
773
+ }
774
+ return response.envelope;
775
+ }
776
+ export async function loadClientProtectedSecret(options) {
777
+ try {
778
+ await finalizePendingClientPasswordRotationIfNeeded(options);
779
+ const passwordState = await loadClientPasswordStateOrNull(resolveClientPasswordStatePath(options.directoryPath));
780
+ const localState = await readLocalSecretFile(resolveLocalSecretFilePath(options.directoryPath, options.keyId));
781
+ if (passwordState === null) {
782
+ if (localState.state === "raw" || await legacyMacKeychainHasSecret(options, options.keyId)) {
783
+ throw new Error("wallet_client_password_migration_required");
784
+ }
785
+ throw new Error("wallet_client_password_setup_required");
786
+ }
787
+ if (localState.state === "missing") {
788
+ if (await legacyMacKeychainHasSecret(options, options.keyId)) {
789
+ throw new Error("wallet_client_password_migration_required");
790
+ }
791
+ throw new Error(`wallet_secret_missing_${options.keyId}`);
792
+ }
793
+ if (localState.state === "raw") {
794
+ throw new Error("wallet_client_password_migration_required");
795
+ }
796
+ return await decryptWrappedSecretWithSession({
797
+ ...options,
798
+ envelope: localState.envelope,
799
+ });
800
+ }
801
+ catch (error) {
802
+ const message = error instanceof Error ? error.message : String(error);
803
+ if (message.startsWith("wallet_client_password_")
804
+ || message.startsWith("wallet_secret_missing_")) {
805
+ throw error;
806
+ }
807
+ throw createRuntimeError(options.runtimeErrorCode, error);
808
+ }
809
+ }
810
+ export async function storeClientProtectedSecret(options) {
811
+ try {
812
+ await finalizePendingClientPasswordRotationIfNeeded(options);
813
+ const passwordState = await loadClientPasswordStateOrNull(resolveClientPasswordStatePath(options.directoryPath));
814
+ if (passwordState === null) {
815
+ throw new Error("wallet_client_password_setup_required");
816
+ }
817
+ await mkdir(options.directoryPath, { recursive: true, mode: 0o700 });
818
+ const envelope = await encryptWrappedSecretWithSession(options);
819
+ await writeFileAtomic(resolveLocalSecretFilePath(options.directoryPath, options.keyId), `${JSON.stringify({
820
+ format: LOCAL_SECRET_ENVELOPE_FORMAT,
821
+ version: 1,
822
+ cipher: "aes-256-gcm",
823
+ wrappedBy: "client-password",
824
+ nonce: envelope.nonce,
825
+ tag: envelope.tag,
826
+ ciphertext: envelope.ciphertext,
827
+ }, null, 2)}\n`, { mode: 0o600 });
828
+ }
829
+ catch (error) {
830
+ const message = error instanceof Error ? error.message : String(error);
831
+ if (message.startsWith("wallet_client_password_")) {
832
+ throw error;
833
+ }
834
+ throw createRuntimeError(options.runtimeErrorCode, error);
835
+ }
836
+ }
837
+ export async function deleteClientProtectedSecret(options) {
838
+ await rm(resolveLocalSecretFilePath(options.directoryPath, options.keyId), { force: true }).catch(() => undefined);
839
+ }
840
+ export async function unlockClientPasswordSession(options) {
841
+ await finalizePendingClientPasswordRotationIfNeeded(options);
842
+ if (!options.prompt.isInteractive) {
843
+ throw new Error("wallet_client_password_unlock_requires_tty");
844
+ }
845
+ const currentStatus = await readClientPasswordSessionStatus(options);
846
+ if (currentStatus.unlocked) {
847
+ const unlockDurationSeconds = await promptForUnlockDurationWithDefault(options.prompt, resolveRemainingUnlockSeconds(currentStatus));
848
+ const refreshed = await refreshClientPasswordSession({
849
+ ...options,
850
+ unlockUntilUnixMs: Date.now() + (unlockDurationSeconds * 1_000),
851
+ });
852
+ if (refreshed !== null) {
853
+ return refreshed;
854
+ }
855
+ }
856
+ return await unlockClientPasswordSessionWithPrompt(options);
857
+ }
858
+ async function prepareClientPasswordRotation(options) {
859
+ const next = await createClientPasswordState({
860
+ passwordBytes: options.newPasswordBytes,
861
+ passwordHint: options.newPasswordHint,
862
+ });
863
+ const keyIds = await collectReferencedSecretIds(options.stateRoot);
864
+ const secrets = [];
865
+ try {
866
+ for (const keyId of keyIds) {
867
+ const localState = await readLocalSecretFile(resolveLocalSecretFilePath(options.directoryPath, keyId));
868
+ if (localState.state === "missing") {
869
+ throw new Error(`wallet_secret_missing_${keyId}`);
870
+ }
871
+ if (localState.state === "raw") {
872
+ throw new Error("wallet_client_password_migration_required");
873
+ }
874
+ const secret = decryptBytesWithKey(localState.envelope, options.currentDerivedKey);
875
+ try {
876
+ secrets.push({
877
+ keyId,
878
+ envelope: createWrappedSecretEnvelope(secret, next.derivedKey),
879
+ });
880
+ }
881
+ finally {
882
+ zeroizeBuffer(secret);
883
+ }
884
+ }
885
+ return {
886
+ journal: {
887
+ format: CLIENT_PASSWORD_ROTATION_JOURNAL_FORMAT,
888
+ version: 1,
889
+ nextState: next.state,
890
+ secrets,
891
+ },
892
+ newDerivedKey: next.derivedKey,
893
+ };
894
+ }
895
+ catch (error) {
896
+ zeroizeBuffer(next.derivedKey);
897
+ throw error;
898
+ }
899
+ }
900
+ export async function changeClientPassword(options) {
901
+ await finalizePendingClientPasswordRotationIfNeeded(options);
902
+ const previousSession = await readClientPasswordSessionStatus(options);
903
+ const currentDerivedKey = await promptForVerifiedClientPassword({
904
+ ...options,
905
+ promptMessage: "Current client password: ",
906
+ ttyErrorCode: "wallet_client_password_change_requires_tty",
907
+ });
908
+ const nextPassword = await promptForNewPassword(options.prompt);
909
+ let newDerivedKey = null;
910
+ try {
911
+ const prepared = await prepareClientPasswordRotation({
912
+ ...options,
913
+ currentDerivedKey,
914
+ newPasswordBytes: nextPassword.passwordBytes,
915
+ newPasswordHint: nextPassword.passwordHint,
916
+ });
917
+ newDerivedKey = prepared.newDerivedKey;
918
+ await mkdir(options.directoryPath, { recursive: true, mode: 0o700 });
919
+ await writeJsonFileAtomic(resolveClientPasswordRotationJournalPath(options.directoryPath), prepared.journal, { mode: 0o600 });
920
+ await finalizePendingClientPasswordRotationIfNeeded(options);
921
+ const session = await startClientPasswordSessionWithExpiry({
922
+ ...options,
923
+ derivedKey: newDerivedKey,
924
+ unlockUntilUnixMs: resolvePostChangeUnlockUntilUnixMs(previousSession),
925
+ });
926
+ newDerivedKey = null;
927
+ return session;
928
+ }
929
+ finally {
930
+ zeroizeBuffer(currentDerivedKey);
931
+ zeroizeBuffer(nextPassword.passwordBytes);
932
+ zeroizeBuffer(newDerivedKey);
933
+ }
934
+ }
935
+ export function createLegacyKeychainServiceName() {
936
+ return "org.cogcoin.wallet";
937
+ }
938
+ export function createAgentBootstrapState(options) {
939
+ return options;
940
+ }
941
+ export function describeClientPasswordLockedMessage() {
942
+ return "Wallet state exists but the client password is locked.";
943
+ }
944
+ export function describeClientPasswordSetupMessage() {
945
+ return "Wallet-local secret access is not configured yet. Run `cogcoin init` to create the client password.";
946
+ }
947
+ export function describeClientPasswordMigrationMessage() {
948
+ return "Wallet-local secret migration is still required. Run `cogcoin init` to migrate this client to password-protected local secrets.";
949
+ }
950
+ export function listLocalSecretFilesForTesting(options) {
951
+ return readdir(options.directoryPath).catch(() => []);
952
+ }