@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.
- package/README.md +2 -2
- package/dist/bitcoind/indexer-daemon.d.ts +3 -7
- package/dist/bitcoind/indexer-daemon.js +39 -204
- package/dist/bitcoind/managed-runtime/bitcoind-policy.d.ts +16 -0
- package/dist/bitcoind/managed-runtime/bitcoind-policy.js +177 -0
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.d.ts +20 -0
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +74 -0
- package/dist/bitcoind/managed-runtime/bitcoind-status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/bitcoind-status.js +44 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
- package/dist/bitcoind/managed-runtime/indexer-runtime.d.ts +15 -0
- package/dist/bitcoind/managed-runtime/indexer-runtime.js +82 -0
- package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/status.js +59 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +77 -0
- package/dist/bitcoind/node.d.ts +2 -2
- package/dist/bitcoind/node.js +2 -2
- package/dist/bitcoind/rpc.d.ts +2 -1
- package/dist/bitcoind/rpc.js +53 -3
- package/dist/bitcoind/service.d.ts +2 -7
- package/dist/bitcoind/service.js +79 -207
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +2 -64
- package/dist/cli/commands/client-admin.js +3 -18
- package/dist/cli/commands/mining-runtime.js +4 -60
- package/dist/cli/commands/wallet-admin.js +6 -6
- package/dist/cli/context.js +1 -3
- package/dist/cli/mining-json.d.ts +1 -22
- package/dist/cli/mining-json.js +0 -23
- package/dist/cli/output.js +16 -2
- package/dist/cli/parse.js +0 -2
- package/dist/cli/preview-json.d.ts +1 -22
- package/dist/cli/preview-json.js +0 -19
- package/dist/cli/types.d.ts +1 -3
- package/dist/cli/wallet-format.js +1 -1
- package/dist/cli/workflow-hints.d.ts +1 -2
- package/dist/cli/workflow-hints.js +5 -8
- package/dist/wallet/lifecycle/access.d.ts +5 -0
- package/dist/wallet/lifecycle/access.js +79 -0
- package/dist/wallet/lifecycle/context.d.ts +26 -0
- package/dist/wallet/lifecycle/context.js +57 -0
- package/dist/wallet/lifecycle/managed-core.d.ts +1 -9
- package/dist/wallet/lifecycle/managed-core.js +3 -63
- package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
- package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
- package/dist/wallet/lifecycle/repair-indexer.js +117 -0
- package/dist/wallet/lifecycle/repair-mining.d.ts +1 -5
- package/dist/wallet/lifecycle/repair-mining.js +5 -39
- package/dist/wallet/lifecycle/repair.d.ts +2 -4
- package/dist/wallet/lifecycle/repair.js +74 -318
- package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
- package/dist/wallet/lifecycle/setup-prompts.js +88 -0
- package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
- package/dist/wallet/lifecycle/setup-state.js +159 -0
- package/dist/wallet/lifecycle/setup.d.ts +3 -4
- package/dist/wallet/lifecycle/setup.js +47 -351
- package/dist/wallet/lifecycle/types.d.ts +33 -5
- package/dist/wallet/managed-core-wallet.d.ts +2 -0
- package/dist/wallet/managed-core-wallet.js +27 -1
- package/dist/wallet/mining/candidate.d.ts +1 -0
- package/dist/wallet/mining/candidate.js +38 -6
- package/dist/wallet/mining/competitiveness.d.ts +1 -0
- package/dist/wallet/mining/competitiveness.js +6 -0
- package/dist/wallet/mining/cycle.d.ts +2 -0
- package/dist/wallet/mining/cycle.js +14 -4
- package/dist/wallet/mining/engine-types.d.ts +1 -0
- package/dist/wallet/mining/index.d.ts +1 -1
- package/dist/wallet/mining/index.js +1 -1
- package/dist/wallet/mining/publish.d.ts +3 -0
- package/dist/wallet/mining/publish.js +78 -6
- package/dist/wallet/mining/runner.d.ts +0 -32
- package/dist/wallet/mining/runner.js +59 -104
- package/dist/wallet/mining/stop.d.ts +7 -0
- package/dist/wallet/mining/stop.js +23 -0
- package/dist/wallet/mining/supervisor.d.ts +2 -36
- package/dist/wallet/mining/supervisor.js +139 -246
- package/dist/wallet/read/context.d.ts +1 -5
- package/dist/wallet/read/context.js +20 -379
- package/dist/wallet/read/managed-services.d.ts +33 -0
- package/dist/wallet/read/managed-services.js +222 -0
- package/dist/wallet/state/client-password/bootstrap.d.ts +2 -0
- package/dist/wallet/state/client-password/bootstrap.js +3 -0
- package/dist/wallet/state/client-password/context.d.ts +10 -0
- package/dist/wallet/state/client-password/context.js +46 -0
- package/dist/wallet/state/client-password/crypto.d.ts +34 -0
- package/dist/wallet/state/client-password/crypto.js +117 -0
- package/dist/wallet/state/client-password/files.d.ts +10 -0
- package/dist/wallet/state/client-password/files.js +109 -0
- package/dist/wallet/state/client-password/legacy-cleanup.d.ts +11 -0
- package/dist/wallet/state/client-password/legacy-cleanup.js +338 -0
- package/dist/wallet/state/client-password/messages.d.ts +3 -0
- package/dist/wallet/state/client-password/messages.js +9 -0
- package/dist/wallet/state/client-password/migration.d.ts +4 -0
- package/dist/wallet/state/client-password/migration.js +32 -0
- package/dist/wallet/state/client-password/prompts.d.ts +12 -0
- package/dist/wallet/state/client-password/prompts.js +79 -0
- package/dist/wallet/state/client-password/protected-secrets.d.ts +13 -0
- package/dist/wallet/state/client-password/protected-secrets.js +90 -0
- package/dist/wallet/state/client-password/readiness.d.ts +4 -0
- package/dist/wallet/state/client-password/readiness.js +48 -0
- package/dist/wallet/state/client-password/references.d.ts +1 -0
- package/dist/wallet/state/client-password/references.js +56 -0
- package/dist/wallet/state/client-password/rotation.d.ts +6 -0
- package/dist/wallet/state/client-password/rotation.js +98 -0
- package/dist/wallet/state/client-password/session-policy.d.ts +6 -0
- package/dist/wallet/state/client-password/session-policy.js +28 -0
- package/dist/wallet/state/client-password/session.d.ts +19 -0
- package/dist/wallet/state/client-password/session.js +170 -0
- package/dist/wallet/state/client-password/setup.d.ts +8 -0
- package/dist/wallet/state/client-password/setup.js +49 -0
- package/dist/wallet/state/client-password/types.d.ts +82 -0
- package/dist/wallet/state/client-password/types.js +5 -0
- package/dist/wallet/state/client-password.d.ts +7 -38
- package/dist/wallet/state/client-password.js +52 -937
- package/dist/wallet/tx/anchor.js +123 -216
- package/dist/wallet/tx/cog.js +294 -489
- package/dist/wallet/tx/common.d.ts +2 -0
- package/dist/wallet/tx/common.js +2 -0
- package/dist/wallet/tx/domain-admin.js +111 -220
- package/dist/wallet/tx/domain-market.js +401 -681
- package/dist/wallet/tx/executor.d.ts +176 -0
- package/dist/wallet/tx/executor.js +302 -0
- package/dist/wallet/tx/field.js +109 -215
- package/dist/wallet/tx/register.js +158 -269
- package/dist/wallet/tx/reputation.js +120 -227
- package/package.json +1 -1
- package/dist/wallet/mining/worker-main.js +0 -17
- package/dist/wallet/state/client-password-agent.d.ts +0 -1
- package/dist/wallet/state/client-password-agent.js +0 -211
- /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,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,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[]>;
|