@cogcoin/client 1.1.5 → 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 (132) hide show
  1. package/README.md +2 -2
  2. package/dist/bitcoind/indexer-daemon.d.ts +3 -7
  3. package/dist/bitcoind/indexer-daemon.js +39 -204
  4. package/dist/bitcoind/managed-runtime/bitcoind-policy.d.ts +16 -0
  5. package/dist/bitcoind/managed-runtime/bitcoind-policy.js +177 -0
  6. package/dist/bitcoind/managed-runtime/bitcoind-runtime.d.ts +20 -0
  7. package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +74 -0
  8. package/dist/bitcoind/managed-runtime/bitcoind-status.d.ts +11 -0
  9. package/dist/bitcoind/managed-runtime/bitcoind-status.js +44 -0
  10. package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
  11. package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
  12. package/dist/bitcoind/managed-runtime/indexer-runtime.d.ts +15 -0
  13. package/dist/bitcoind/managed-runtime/indexer-runtime.js +82 -0
  14. package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
  15. package/dist/bitcoind/managed-runtime/status.js +59 -0
  16. package/dist/bitcoind/managed-runtime/types.d.ts +77 -0
  17. package/dist/bitcoind/node.d.ts +2 -2
  18. package/dist/bitcoind/node.js +2 -2
  19. package/dist/bitcoind/rpc.d.ts +2 -1
  20. package/dist/bitcoind/rpc.js +53 -3
  21. package/dist/bitcoind/service.d.ts +2 -7
  22. package/dist/bitcoind/service.js +79 -207
  23. package/dist/cli/command-registry.d.ts +1 -1
  24. package/dist/cli/command-registry.js +2 -64
  25. package/dist/cli/commands/client-admin.js +3 -18
  26. package/dist/cli/commands/mining-runtime.js +4 -60
  27. package/dist/cli/commands/wallet-admin.js +6 -6
  28. package/dist/cli/context.js +1 -3
  29. package/dist/cli/mining-json.d.ts +1 -22
  30. package/dist/cli/mining-json.js +0 -23
  31. package/dist/cli/output.js +16 -2
  32. package/dist/cli/parse.js +0 -2
  33. package/dist/cli/preview-json.d.ts +1 -22
  34. package/dist/cli/preview-json.js +0 -19
  35. package/dist/cli/types.d.ts +1 -3
  36. package/dist/cli/wallet-format.js +1 -1
  37. package/dist/cli/workflow-hints.d.ts +1 -2
  38. package/dist/cli/workflow-hints.js +5 -8
  39. package/dist/wallet/lifecycle/access.d.ts +5 -0
  40. package/dist/wallet/lifecycle/access.js +79 -0
  41. package/dist/wallet/lifecycle/context.d.ts +26 -0
  42. package/dist/wallet/lifecycle/context.js +57 -0
  43. package/dist/wallet/lifecycle/managed-core.d.ts +1 -9
  44. package/dist/wallet/lifecycle/managed-core.js +3 -63
  45. package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
  46. package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
  47. package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
  48. package/dist/wallet/lifecycle/repair-indexer.js +117 -0
  49. package/dist/wallet/lifecycle/repair-mining.d.ts +1 -5
  50. package/dist/wallet/lifecycle/repair-mining.js +5 -39
  51. package/dist/wallet/lifecycle/repair.d.ts +2 -4
  52. package/dist/wallet/lifecycle/repair.js +74 -318
  53. package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
  54. package/dist/wallet/lifecycle/setup-prompts.js +88 -0
  55. package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
  56. package/dist/wallet/lifecycle/setup-state.js +159 -0
  57. package/dist/wallet/lifecycle/setup.d.ts +3 -4
  58. package/dist/wallet/lifecycle/setup.js +47 -351
  59. package/dist/wallet/lifecycle/types.d.ts +33 -5
  60. package/dist/wallet/managed-core-wallet.d.ts +2 -0
  61. package/dist/wallet/managed-core-wallet.js +27 -1
  62. package/dist/wallet/mining/candidate.d.ts +1 -0
  63. package/dist/wallet/mining/candidate.js +38 -6
  64. package/dist/wallet/mining/competitiveness.d.ts +1 -0
  65. package/dist/wallet/mining/competitiveness.js +6 -0
  66. package/dist/wallet/mining/cycle.d.ts +2 -0
  67. package/dist/wallet/mining/cycle.js +14 -4
  68. package/dist/wallet/mining/engine-types.d.ts +1 -0
  69. package/dist/wallet/mining/index.d.ts +1 -1
  70. package/dist/wallet/mining/index.js +1 -1
  71. package/dist/wallet/mining/publish.d.ts +3 -0
  72. package/dist/wallet/mining/publish.js +78 -6
  73. package/dist/wallet/mining/runner.d.ts +0 -32
  74. package/dist/wallet/mining/runner.js +59 -104
  75. package/dist/wallet/mining/stop.d.ts +7 -0
  76. package/dist/wallet/mining/stop.js +23 -0
  77. package/dist/wallet/mining/supervisor.d.ts +2 -36
  78. package/dist/wallet/mining/supervisor.js +139 -246
  79. package/dist/wallet/read/context.d.ts +1 -5
  80. package/dist/wallet/read/context.js +20 -379
  81. package/dist/wallet/read/managed-services.d.ts +33 -0
  82. package/dist/wallet/read/managed-services.js +222 -0
  83. package/dist/wallet/state/client-password/bootstrap.d.ts +2 -0
  84. package/dist/wallet/state/client-password/bootstrap.js +3 -0
  85. package/dist/wallet/state/client-password/context.d.ts +10 -0
  86. package/dist/wallet/state/client-password/context.js +46 -0
  87. package/dist/wallet/state/client-password/crypto.d.ts +34 -0
  88. package/dist/wallet/state/client-password/crypto.js +117 -0
  89. package/dist/wallet/state/client-password/files.d.ts +10 -0
  90. package/dist/wallet/state/client-password/files.js +109 -0
  91. package/dist/wallet/state/client-password/legacy-cleanup.d.ts +11 -0
  92. package/dist/wallet/state/client-password/legacy-cleanup.js +338 -0
  93. package/dist/wallet/state/client-password/messages.d.ts +3 -0
  94. package/dist/wallet/state/client-password/messages.js +9 -0
  95. package/dist/wallet/state/client-password/migration.d.ts +4 -0
  96. package/dist/wallet/state/client-password/migration.js +32 -0
  97. package/dist/wallet/state/client-password/prompts.d.ts +12 -0
  98. package/dist/wallet/state/client-password/prompts.js +79 -0
  99. package/dist/wallet/state/client-password/protected-secrets.d.ts +13 -0
  100. package/dist/wallet/state/client-password/protected-secrets.js +90 -0
  101. package/dist/wallet/state/client-password/readiness.d.ts +4 -0
  102. package/dist/wallet/state/client-password/readiness.js +48 -0
  103. package/dist/wallet/state/client-password/references.d.ts +1 -0
  104. package/dist/wallet/state/client-password/references.js +56 -0
  105. package/dist/wallet/state/client-password/rotation.d.ts +6 -0
  106. package/dist/wallet/state/client-password/rotation.js +98 -0
  107. package/dist/wallet/state/client-password/session-policy.d.ts +6 -0
  108. package/dist/wallet/state/client-password/session-policy.js +28 -0
  109. package/dist/wallet/state/client-password/session.d.ts +19 -0
  110. package/dist/wallet/state/client-password/session.js +170 -0
  111. package/dist/wallet/state/client-password/setup.d.ts +8 -0
  112. package/dist/wallet/state/client-password/setup.js +49 -0
  113. package/dist/wallet/state/client-password/types.d.ts +82 -0
  114. package/dist/wallet/state/client-password/types.js +5 -0
  115. package/dist/wallet/state/client-password.d.ts +7 -38
  116. package/dist/wallet/state/client-password.js +52 -937
  117. package/dist/wallet/tx/anchor.js +123 -216
  118. package/dist/wallet/tx/cog.js +294 -489
  119. package/dist/wallet/tx/common.d.ts +2 -0
  120. package/dist/wallet/tx/common.js +2 -0
  121. package/dist/wallet/tx/domain-admin.js +111 -220
  122. package/dist/wallet/tx/domain-market.js +401 -681
  123. package/dist/wallet/tx/executor.d.ts +176 -0
  124. package/dist/wallet/tx/executor.js +302 -0
  125. package/dist/wallet/tx/field.js +109 -215
  126. package/dist/wallet/tx/register.js +158 -269
  127. package/dist/wallet/tx/reputation.js +120 -227
  128. package/package.json +1 -1
  129. package/dist/wallet/mining/worker-main.js +0 -17
  130. package/dist/wallet/state/client-password-agent.d.ts +0 -1
  131. package/dist/wallet/state/client-password-agent.js +0 -211
  132. /package/dist/{wallet/mining/worker-main.d.ts → bitcoind/managed-runtime/types.js} +0 -0
@@ -0,0 +1,338 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { access, readdir, rm, rmdir } from "node:fs/promises";
4
+ import net from "node:net";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { promisify } from "node:util";
8
+ const execFileAsync = promisify(execFile);
9
+ const LEGACY_AGENT_MARKER = "client-password-agent.js";
10
+ const LEGACY_AGENT_TIMEOUT_MS = 500;
11
+ const LEGACY_AGENT_STOP_TIMEOUT_MS = 5_000;
12
+ const LEGACY_AGENT_STOP_POLL_MS = 100;
13
+ const LEGACY_SOCKET_REMOVAL_WAIT_MS = 500;
14
+ const LEGACY_SOCKET_REMOVAL_POLL_MS = 25;
15
+ const inFlightCleanupByStateRoot = new Map();
16
+ function escapeRegex(value) {
17
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ }
19
+ function isWindowsHostPlatform(platform) {
20
+ return platform === "win32";
21
+ }
22
+ function isLegacyStatusResponse(value) {
23
+ if (value === null || typeof value !== "object" || value.ok !== true) {
24
+ return false;
25
+ }
26
+ const unlockUntilUnixMs = value.unlockUntilUnixMs;
27
+ return unlockUntilUnixMs === undefined
28
+ || unlockUntilUnixMs === null
29
+ || Number.isFinite(unlockUntilUnixMs);
30
+ }
31
+ function isLegacyLockResponse(value) {
32
+ return value !== null
33
+ && typeof value === "object"
34
+ && value.ok === true;
35
+ }
36
+ async function pathExists(path) {
37
+ try {
38
+ await access(path);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function isExactCommandArgument(command, argument) {
46
+ return new RegExp(`(^|\\s|["'])${escapeRegex(argument)}(?=$|\\s|["'])`).test(command);
47
+ }
48
+ function isLegacyAgentCommand(command, endpoint) {
49
+ return command.includes(LEGACY_AGENT_MARKER)
50
+ && isExactCommandArgument(command, endpoint);
51
+ }
52
+ async function sendLegacyAgentRequest(options) {
53
+ return await new Promise((resolve) => {
54
+ const socket = net.createConnection(options.endpoint);
55
+ const timeoutMs = options.timeoutMs ?? LEGACY_AGENT_TIMEOUT_MS;
56
+ let settled = false;
57
+ let received = "";
58
+ const cleanup = () => {
59
+ clearTimeout(timer);
60
+ socket.off("connect", onConnect);
61
+ socket.off("data", onData);
62
+ socket.off("error", onError);
63
+ socket.off("end", onEnd);
64
+ socket.off("close", onClose);
65
+ };
66
+ const finish = (result) => {
67
+ if (settled) {
68
+ return;
69
+ }
70
+ settled = true;
71
+ cleanup();
72
+ socket.destroy();
73
+ resolve(result);
74
+ };
75
+ const timer = setTimeout(() => {
76
+ finish({ kind: "invalid" });
77
+ }, timeoutMs);
78
+ timer.unref();
79
+ const onConnect = () => {
80
+ socket.write(`${JSON.stringify(options.request)}\n`);
81
+ };
82
+ const onData = (chunk) => {
83
+ received += chunk.toString("utf8");
84
+ const newlineIndex = received.indexOf("\n");
85
+ if (newlineIndex === -1) {
86
+ return;
87
+ }
88
+ try {
89
+ finish({
90
+ kind: "ok",
91
+ response: JSON.parse(received.slice(0, newlineIndex)),
92
+ });
93
+ }
94
+ catch {
95
+ finish({ kind: "invalid" });
96
+ }
97
+ };
98
+ const onError = (error) => {
99
+ const code = error instanceof Error && "code" in error
100
+ ? String(error.code ?? "")
101
+ : "";
102
+ if (code === "ENOENT") {
103
+ finish({ kind: "missing" });
104
+ return;
105
+ }
106
+ if (!isWindowsHostPlatform(options.hostPlatform)
107
+ && (code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE")) {
108
+ finish({ kind: "stale" });
109
+ return;
110
+ }
111
+ finish({ kind: "invalid" });
112
+ };
113
+ const onEnd = () => {
114
+ if (received.length === 0) {
115
+ finish({ kind: "invalid" });
116
+ }
117
+ };
118
+ const onClose = () => {
119
+ if (received.length === 0) {
120
+ finish({ kind: "invalid" });
121
+ }
122
+ };
123
+ socket.on("connect", onConnect);
124
+ socket.on("data", onData);
125
+ socket.on("error", onError);
126
+ socket.on("end", onEnd);
127
+ socket.on("close", onClose);
128
+ });
129
+ }
130
+ async function waitForLegacySocketCleanup(endpoint) {
131
+ const deadline = Date.now() + LEGACY_SOCKET_REMOVAL_WAIT_MS;
132
+ while (Date.now() < deadline) {
133
+ if (!await pathExists(endpoint)) {
134
+ return;
135
+ }
136
+ await new Promise((resolve) => setTimeout(resolve, LEGACY_SOCKET_REMOVAL_POLL_MS));
137
+ }
138
+ const probe = await sendLegacyAgentRequest({
139
+ endpoint,
140
+ request: { command: "status" },
141
+ hostPlatform: process.platform,
142
+ timeoutMs: LEGACY_AGENT_TIMEOUT_MS,
143
+ });
144
+ if (probe.kind === "missing" || probe.kind === "stale") {
145
+ await rm(endpoint, { force: true }).catch(() => undefined);
146
+ }
147
+ }
148
+ async function cleanupLegacyAgentEndpoint(options) {
149
+ const endpoint = resolveLegacyClientPasswordAgentEndpointForTesting(options.stateRoot, options.hostPlatform);
150
+ const status = await sendLegacyAgentRequest({
151
+ endpoint,
152
+ request: { command: "status" },
153
+ hostPlatform: options.hostPlatform,
154
+ });
155
+ if (status.kind === "missing") {
156
+ return;
157
+ }
158
+ if (status.kind === "stale") {
159
+ if (!isWindowsHostPlatform(options.hostPlatform)) {
160
+ await rm(endpoint, { force: true }).catch(() => undefined);
161
+ }
162
+ return;
163
+ }
164
+ if (status.kind !== "ok" || !isLegacyStatusResponse(status.response)) {
165
+ return;
166
+ }
167
+ const lock = await sendLegacyAgentRequest({
168
+ endpoint,
169
+ request: { command: "lock" },
170
+ hostPlatform: options.hostPlatform,
171
+ });
172
+ if (lock.kind === "ok" && isLegacyLockResponse(lock.response) && !isWindowsHostPlatform(options.hostPlatform)) {
173
+ await waitForLegacySocketCleanup(endpoint).catch(() => undefined);
174
+ }
175
+ }
176
+ export function resolveLegacyClientPasswordAgentEndpointForTesting(stateRoot, hostPlatform = process.platform) {
177
+ const hash = createHash("sha256").update(stateRoot).digest("hex").slice(0, 24);
178
+ if (isWindowsHostPlatform(hostPlatform)) {
179
+ return `\\\\.\\pipe\\cogcoin-client-password-${hash}`;
180
+ }
181
+ return join(tmpdir(), `cogcoin-client-password-${hash}.sock`);
182
+ }
183
+ export function extractLegacyClientPasswordAgentProcessIdsForTesting(options) {
184
+ const matches = new Set();
185
+ if (isWindowsHostPlatform(options.hostPlatform)) {
186
+ const trimmed = options.stdout.trim();
187
+ if (trimmed.length === 0 || trimmed === "null") {
188
+ return [];
189
+ }
190
+ const parsed = JSON.parse(trimmed);
191
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
192
+ for (const entry of entries) {
193
+ const processId = typeof entry.ProcessId === "number"
194
+ ? entry.ProcessId
195
+ : typeof entry.processId === "number"
196
+ ? entry.processId
197
+ : null;
198
+ const commandLine = typeof entry.CommandLine === "string"
199
+ ? entry.CommandLine
200
+ : typeof entry.commandLine === "string"
201
+ ? entry.commandLine
202
+ : "";
203
+ if (processId !== null && isLegacyAgentCommand(commandLine, options.endpoint)) {
204
+ matches.add(processId);
205
+ }
206
+ }
207
+ return [...matches];
208
+ }
209
+ for (const line of options.stdout.split(/\r?\n/)) {
210
+ const match = line.match(/^\s*(\d+)\s+(.*)$/);
211
+ if (match === null) {
212
+ continue;
213
+ }
214
+ const processId = Number(match[1]);
215
+ const command = match[2] ?? "";
216
+ if (Number.isInteger(processId) && isLegacyAgentCommand(command, options.endpoint)) {
217
+ matches.add(processId);
218
+ }
219
+ }
220
+ return [...matches];
221
+ }
222
+ async function listLegacyAgentProcessIds(options) {
223
+ if (isWindowsHostPlatform(options.hostPlatform)) {
224
+ const { stdout } = await execFileAsync("powershell.exe", [
225
+ "-NoProfile",
226
+ "-Command",
227
+ "Get-CimInstance Win32_Process | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress",
228
+ ]);
229
+ return extractLegacyClientPasswordAgentProcessIdsForTesting({
230
+ endpoint: options.endpoint,
231
+ hostPlatform: options.hostPlatform,
232
+ stdout,
233
+ });
234
+ }
235
+ const { stdout } = await execFileAsync("ps", ["-axo", "pid=,command="]);
236
+ return extractLegacyClientPasswordAgentProcessIdsForTesting({
237
+ endpoint: options.endpoint,
238
+ hostPlatform: options.hostPlatform,
239
+ stdout,
240
+ });
241
+ }
242
+ async function isProcessAlive(pid) {
243
+ try {
244
+ process.kill(pid, 0);
245
+ return true;
246
+ }
247
+ catch (error) {
248
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
249
+ return false;
250
+ }
251
+ return true;
252
+ }
253
+ }
254
+ async function waitForProcessExit(pid) {
255
+ const deadline = Date.now() + LEGACY_AGENT_STOP_TIMEOUT_MS;
256
+ while (Date.now() < deadline) {
257
+ if (!await isProcessAlive(pid)) {
258
+ return;
259
+ }
260
+ await new Promise((resolve) => setTimeout(resolve, LEGACY_AGENT_STOP_POLL_MS));
261
+ }
262
+ }
263
+ async function stopLegacyAgentProcess(pid) {
264
+ if (pid === process.pid || !await isProcessAlive(pid)) {
265
+ return;
266
+ }
267
+ try {
268
+ process.kill(pid, "SIGTERM");
269
+ }
270
+ catch (error) {
271
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
272
+ throw error;
273
+ }
274
+ }
275
+ try {
276
+ await waitForProcessExit(pid);
277
+ return;
278
+ }
279
+ catch {
280
+ try {
281
+ process.kill(pid, "SIGKILL");
282
+ }
283
+ catch (error) {
284
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
285
+ throw error;
286
+ }
287
+ }
288
+ }
289
+ await waitForProcessExit(pid).catch(() => undefined);
290
+ }
291
+ async function cleanupLegacyAgentProcesses(options) {
292
+ const endpoint = resolveLegacyClientPasswordAgentEndpointForTesting(options.stateRoot, options.hostPlatform);
293
+ const processIds = await listLegacyAgentProcessIds({
294
+ endpoint,
295
+ hostPlatform: options.hostPlatform,
296
+ });
297
+ for (const pid of processIds) {
298
+ await stopLegacyAgentProcess(pid).catch(() => undefined);
299
+ }
300
+ }
301
+ async function pruneLegacyRuntimeLeak(stateRoot) {
302
+ const legacyRuntimeRoot = join(stateRoot, ".client-runtime");
303
+ const entries = await readdir(legacyRuntimeRoot).catch(() => null);
304
+ if (entries === null || entries.length > 0) {
305
+ return;
306
+ }
307
+ await rmdir(legacyRuntimeRoot).catch(() => undefined);
308
+ }
309
+ async function runDefaultCleanupPass(context) {
310
+ const hostPlatform = process.platform;
311
+ await cleanupLegacyAgentEndpoint({
312
+ stateRoot: context.stateRoot,
313
+ hostPlatform,
314
+ }).catch(() => undefined);
315
+ await cleanupLegacyAgentProcesses({
316
+ stateRoot: context.stateRoot,
317
+ hostPlatform,
318
+ }).catch(() => undefined);
319
+ await pruneLegacyRuntimeLeak(context.stateRoot).catch(() => undefined);
320
+ }
321
+ export async function cleanupLegacyClientPasswordArtifactsResolved(context, deps = {}) {
322
+ const cacheKey = context.stateRoot;
323
+ const inFlight = inFlightCleanupByStateRoot.get(cacheKey);
324
+ if (inFlight !== undefined) {
325
+ await inFlight;
326
+ return;
327
+ }
328
+ const cleanupPromise = (deps.runCleanupPass ?? runDefaultCleanupPass)(context).catch(() => undefined);
329
+ inFlightCleanupByStateRoot.set(cacheKey, cleanupPromise);
330
+ try {
331
+ await cleanupPromise;
332
+ }
333
+ finally {
334
+ if (inFlightCleanupByStateRoot.get(cacheKey) === cleanupPromise) {
335
+ inFlightCleanupByStateRoot.delete(cacheKey);
336
+ }
337
+ }
338
+ }
@@ -0,0 +1,3 @@
1
+ export declare function describeClientPasswordLockedMessage(): string;
2
+ export declare function describeClientPasswordSetupMessage(): string;
3
+ export declare function describeClientPasswordMigrationMessage(): string;
@@ -0,0 +1,9 @@
1
+ export function describeClientPasswordLockedMessage() {
2
+ return "Wallet state exists but the client password is locked.";
3
+ }
4
+ export function describeClientPasswordSetupMessage() {
5
+ return "Wallet-local secret access is not configured yet. Run `cogcoin init` to create the client password.";
6
+ }
7
+ export function describeClientPasswordMigrationMessage() {
8
+ return "Wallet-local secret migration is still required. Run `cogcoin init` to migrate this client to password-protected local secrets.";
9
+ }
@@ -0,0 +1,4 @@
1
+ import type { ClientPasswordResolvedContext } from "./types.js";
2
+ export declare function migrateReferencedSecrets(options: ClientPasswordResolvedContext & {
3
+ derivedKey: Uint8Array;
4
+ }): Promise<boolean>;
@@ -0,0 +1,32 @@
1
+ import { resolveLocalSecretFilePath } from "./context.js";
2
+ import { createWrappedSecretEnvelope } from "./crypto.js";
3
+ import { readLocalSecretFile, writeWrappedSecretEnvelope } from "./files.js";
4
+ import { collectReferencedSecretIds } from "./references.js";
5
+ import { legacyMacKeychainHasSecret } from "./readiness.js";
6
+ export async function migrateReferencedSecrets(options) {
7
+ const keyIds = await collectReferencedSecretIds(options.stateRoot);
8
+ let migrated = false;
9
+ for (const keyId of keyIds) {
10
+ const localPath = resolveLocalSecretFilePath(options.directoryPath, keyId);
11
+ const localState = await readLocalSecretFile(localPath);
12
+ if (localState.state === "wrapped") {
13
+ continue;
14
+ }
15
+ if (localState.state === "raw") {
16
+ await writeWrappedSecretEnvelope(localPath, createWrappedSecretEnvelope(localState.secret, options.derivedKey));
17
+ migrated = true;
18
+ continue;
19
+ }
20
+ if (await legacyMacKeychainHasSecret(options, keyId)) {
21
+ try {
22
+ const secret = await options.legacyMacKeychainReader.loadSecret(keyId);
23
+ await writeWrappedSecretEnvelope(localPath, createWrappedSecretEnvelope(secret, options.derivedKey));
24
+ migrated = true;
25
+ }
26
+ catch {
27
+ // Best-effort legacy migration only.
28
+ }
29
+ }
30
+ }
31
+ return migrated;
32
+ }
@@ -0,0 +1,12 @@
1
+ import type { ClientPasswordPrompt, ClientPasswordResolvedContext } from "./types.js";
2
+ export declare function promptForHiddenValue(prompt: ClientPasswordPrompt, message: string): Promise<string>;
3
+ export declare function promptForVerifiedClientPassword(options: {
4
+ context: ClientPasswordResolvedContext;
5
+ prompt: ClientPasswordPrompt;
6
+ promptMessage: string;
7
+ ttyErrorCode: string;
8
+ }): Promise<Buffer>;
9
+ export declare function promptForNewPassword(prompt: ClientPasswordPrompt): Promise<{
10
+ passwordBytes: Buffer;
11
+ passwordHint: string;
12
+ }>;
@@ -0,0 +1,79 @@
1
+ import { loadClientPasswordStateOrNull } from "./files.js";
2
+ import { verifyPassword, zeroizeBuffer, } from "./crypto.js";
3
+ import { describeReadinessError, inspectClientPasswordReadinessResolved } from "./readiness.js";
4
+ export async function promptForHiddenValue(prompt, message) {
5
+ const value = prompt.promptHidden != null
6
+ ? await prompt.promptHidden(message)
7
+ : await prompt.prompt(message);
8
+ return value.trim();
9
+ }
10
+ export async function promptForVerifiedClientPassword(options) {
11
+ const readiness = await inspectClientPasswordReadinessResolved(options.context);
12
+ if (readiness !== "ready") {
13
+ throw new Error(describeReadinessError(readiness));
14
+ }
15
+ if (!options.prompt.isInteractive) {
16
+ throw new Error(options.ttyErrorCode);
17
+ }
18
+ const state = await loadClientPasswordStateOrNull(options.context.passwordStatePath);
19
+ if (state === null) {
20
+ throw new Error("wallet_client_password_setup_required");
21
+ }
22
+ let attempts = 0;
23
+ while (true) {
24
+ if (attempts >= 2 && state.passwordHint.trim().length > 0) {
25
+ options.prompt.writeLine(`Hint: ${state.passwordHint}`);
26
+ }
27
+ const passwordText = await promptForHiddenValue(options.prompt, options.promptMessage);
28
+ const passwordBytes = Buffer.from(passwordText, "utf8");
29
+ let derivedKey = null;
30
+ try {
31
+ derivedKey = await verifyPassword({
32
+ state,
33
+ passwordBytes,
34
+ });
35
+ }
36
+ finally {
37
+ zeroizeBuffer(passwordBytes);
38
+ }
39
+ if (derivedKey !== null) {
40
+ return derivedKey;
41
+ }
42
+ attempts += 1;
43
+ options.prompt.writeLine("Incorrect client password.");
44
+ }
45
+ }
46
+ export async function promptForNewPassword(prompt) {
47
+ if (!prompt.isInteractive) {
48
+ throw new Error("wallet_client_password_setup_requires_tty");
49
+ }
50
+ while (true) {
51
+ const first = await promptForHiddenValue(prompt, "Create client password: ");
52
+ const firstBytes = Buffer.from(first, "utf8");
53
+ if (firstBytes.length === 0) {
54
+ zeroizeBuffer(firstBytes);
55
+ prompt.writeLine("Client password cannot be blank.");
56
+ continue;
57
+ }
58
+ const second = await promptForHiddenValue(prompt, "Confirm client password: ");
59
+ const secondBytes = Buffer.from(second, "utf8");
60
+ if (!firstBytes.equals(secondBytes)) {
61
+ zeroizeBuffer(firstBytes);
62
+ zeroizeBuffer(secondBytes);
63
+ prompt.writeLine("Client password entries did not match.");
64
+ continue;
65
+ }
66
+ zeroizeBuffer(secondBytes);
67
+ let passwordHint = "";
68
+ while (passwordHint.length === 0) {
69
+ passwordHint = (await prompt.prompt("Password hint: ")).trim();
70
+ if (passwordHint.length === 0) {
71
+ prompt.writeLine("Password hint cannot be blank.");
72
+ }
73
+ }
74
+ return {
75
+ passwordBytes: firstBytes,
76
+ passwordHint,
77
+ };
78
+ }
79
+ }
@@ -0,0 +1,13 @@
1
+ import type { ClientPasswordPrompt, ClientPasswordResolvedContext } from "./types.js";
2
+ export declare function loadClientProtectedSecretResolved(options: ClientPasswordResolvedContext & {
3
+ keyId: string;
4
+ prompt?: ClientPasswordPrompt;
5
+ }): Promise<Uint8Array>;
6
+ export declare function storeClientProtectedSecretResolved(options: ClientPasswordResolvedContext & {
7
+ keyId: string;
8
+ secret: Uint8Array;
9
+ prompt?: ClientPasswordPrompt;
10
+ }): Promise<void>;
11
+ export declare function deleteClientProtectedSecretResolved(options: ClientPasswordResolvedContext & {
12
+ keyId: string;
13
+ }): Promise<void>;
@@ -0,0 +1,90 @@
1
+ import { mkdir, rm } from "node:fs/promises";
2
+ import { createRuntimeError, resolveLocalSecretFilePath } from "./context.js";
3
+ import { loadClientPasswordStateOrNull, readLocalSecretFile, writeWrappedSecretEnvelope, } from "./files.js";
4
+ import { legacyMacKeychainHasSecret } from "./readiness.js";
5
+ import { finalizePendingClientPasswordRotationIfNeeded } from "./rotation.js";
6
+ import { decryptClientProtectedSecretWithSessionResolved, encryptClientProtectedSecretWithSessionResolved, unlockClientPasswordSessionResolved, } from "./session.js";
7
+ async function decryptWrappedSecretWithSessionResolved(options) {
8
+ let secret = decryptClientProtectedSecretWithSessionResolved(options, options.envelope);
9
+ if (secret === null && options.prompt != null && options.prompt.isInteractive) {
10
+ await unlockClientPasswordSessionResolved({
11
+ context: options,
12
+ prompt: options.prompt,
13
+ });
14
+ secret = decryptClientProtectedSecretWithSessionResolved(options, options.envelope);
15
+ }
16
+ if (secret === null) {
17
+ throw new Error("wallet_client_password_locked");
18
+ }
19
+ return secret;
20
+ }
21
+ async function encryptWrappedSecretWithSessionResolved(options) {
22
+ let envelope = encryptClientProtectedSecretWithSessionResolved(options, options.secret);
23
+ if (envelope === null && options.prompt != null && options.prompt.isInteractive) {
24
+ await unlockClientPasswordSessionResolved({
25
+ context: options,
26
+ prompt: options.prompt,
27
+ });
28
+ envelope = encryptClientProtectedSecretWithSessionResolved(options, options.secret);
29
+ }
30
+ if (envelope === null) {
31
+ throw new Error("wallet_client_password_locked");
32
+ }
33
+ return envelope;
34
+ }
35
+ export async function loadClientProtectedSecretResolved(options) {
36
+ try {
37
+ await finalizePendingClientPasswordRotationIfNeeded(options);
38
+ const passwordState = await loadClientPasswordStateOrNull(options.passwordStatePath);
39
+ const localState = await readLocalSecretFile(resolveLocalSecretFilePath(options.directoryPath, options.keyId));
40
+ if (passwordState === null) {
41
+ if (localState.state === "raw" || await legacyMacKeychainHasSecret(options, options.keyId)) {
42
+ throw new Error("wallet_client_password_migration_required");
43
+ }
44
+ throw new Error("wallet_client_password_setup_required");
45
+ }
46
+ if (localState.state === "missing") {
47
+ if (await legacyMacKeychainHasSecret(options, options.keyId)) {
48
+ throw new Error("wallet_client_password_migration_required");
49
+ }
50
+ throw new Error(`wallet_secret_missing_${options.keyId}`);
51
+ }
52
+ if (localState.state === "raw") {
53
+ throw new Error("wallet_client_password_migration_required");
54
+ }
55
+ return await decryptWrappedSecretWithSessionResolved({
56
+ ...options,
57
+ envelope: localState.envelope,
58
+ });
59
+ }
60
+ catch (error) {
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ if (message.startsWith("wallet_client_password_")
63
+ || message.startsWith("wallet_secret_missing_")) {
64
+ throw error;
65
+ }
66
+ throw createRuntimeError(options.runtimeErrorCode, error);
67
+ }
68
+ }
69
+ export async function storeClientProtectedSecretResolved(options) {
70
+ try {
71
+ await finalizePendingClientPasswordRotationIfNeeded(options);
72
+ const passwordState = await loadClientPasswordStateOrNull(options.passwordStatePath);
73
+ if (passwordState === null) {
74
+ throw new Error("wallet_client_password_setup_required");
75
+ }
76
+ await mkdir(options.directoryPath, { recursive: true, mode: 0o700 });
77
+ const envelope = await encryptWrappedSecretWithSessionResolved(options);
78
+ await writeWrappedSecretEnvelope(resolveLocalSecretFilePath(options.directoryPath, options.keyId), envelope);
79
+ }
80
+ catch (error) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ if (message.startsWith("wallet_client_password_")) {
83
+ throw error;
84
+ }
85
+ throw createRuntimeError(options.runtimeErrorCode, error);
86
+ }
87
+ }
88
+ export async function deleteClientProtectedSecretResolved(options) {
89
+ await rm(resolveLocalSecretFilePath(options.directoryPath, options.keyId), { force: true }).catch(() => undefined);
90
+ }
@@ -0,0 +1,4 @@
1
+ import type { ClientPasswordReadiness, ClientPasswordResolvedContext } from "./types.js";
2
+ export declare function legacyMacKeychainHasSecret(context: ClientPasswordResolvedContext, keyId: string): Promise<boolean>;
3
+ export declare function inspectClientPasswordReadinessResolved(context: ClientPasswordResolvedContext): Promise<ClientPasswordReadiness>;
4
+ export declare function describeReadinessError(readiness: ClientPasswordReadiness): string;
@@ -0,0 +1,48 @@
1
+ import { resolveLocalSecretFilePath } from "./context.js";
2
+ import { readLocalSecretFile, loadClientPasswordStateOrNull } from "./files.js";
3
+ import { collectReferencedSecretIds } from "./references.js";
4
+ export async function legacyMacKeychainHasSecret(context, keyId) {
5
+ if (context.platform !== "darwin" || context.legacyMacKeychainReader == null) {
6
+ return false;
7
+ }
8
+ try {
9
+ await context.legacyMacKeychainReader.loadSecret(keyId);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ async function inspectReadinessForKey(context, keyId) {
17
+ const local = await readLocalSecretFile(resolveLocalSecretFilePath(context.directoryPath, keyId));
18
+ const keychain = await legacyMacKeychainHasSecret(context, keyId);
19
+ return { local, keychain };
20
+ }
21
+ export async function inspectClientPasswordReadinessResolved(context) {
22
+ const passwordState = await loadClientPasswordStateOrNull(context.passwordStatePath);
23
+ const keyIds = await collectReferencedSecretIds(context.stateRoot);
24
+ if (keyIds.length === 0) {
25
+ return passwordState === null ? "setup-required" : "ready";
26
+ }
27
+ for (const keyId of keyIds) {
28
+ const sourceState = await inspectReadinessForKey(context, keyId);
29
+ if (passwordState === null) {
30
+ if (sourceState.local.state === "raw" || sourceState.keychain) {
31
+ return "migration-required";
32
+ }
33
+ continue;
34
+ }
35
+ if (sourceState.local.state === "raw") {
36
+ return "migration-required";
37
+ }
38
+ if (sourceState.local.state === "missing" && sourceState.keychain) {
39
+ return "migration-required";
40
+ }
41
+ }
42
+ return passwordState === null ? "setup-required" : "ready";
43
+ }
44
+ export function describeReadinessError(readiness) {
45
+ return readiness === "migration-required"
46
+ ? "wallet_client_password_migration_required"
47
+ : "wallet_client_password_setup_required";
48
+ }
@@ -0,0 +1 @@
1
+ export declare function collectReferencedSecretIds(stateRoot: string): Promise<string[]>;