@cogcoin/client 1.1.6 → 1.1.7

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