@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
@@ -0,0 +1,117 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, } from "node:crypto";
2
+ import { argon2idAsync } from "@noble/hashes/argon2.js";
3
+ import { decryptBytesWithKey, encryptBytesWithKey } from "../crypto.js";
4
+ import { CLIENT_PASSWORD_STATE_FORMAT, CLIENT_PASSWORD_VERIFIER_FORMAT, CLIENT_PASSWORD_VERIFIER_TEXT, LOCAL_SECRET_ENVELOPE_FORMAT, } from "./types.js";
5
+ const CLIENT_PASSWORD_DERIVED_KEY_BYTES = 32;
6
+ const CLIENT_PASSWORD_KDF = {
7
+ memoryKib: 65_536,
8
+ iterations: 3,
9
+ parallelism: 1,
10
+ };
11
+ export const CLIENT_PASSWORD_DEFAULT_UNLOCK_SECONDS = 3_600;
12
+ export const CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS = 86_400;
13
+ export function zeroizeBuffer(buffer) {
14
+ if (buffer != null) {
15
+ buffer.fill(0);
16
+ }
17
+ }
18
+ export async function derivePasswordKey(passwordBytes, saltBytes) {
19
+ return Buffer.from(await argon2idAsync(passwordBytes, saltBytes, {
20
+ m: CLIENT_PASSWORD_KDF.memoryKib,
21
+ t: CLIENT_PASSWORD_KDF.iterations,
22
+ p: CLIENT_PASSWORD_KDF.parallelism,
23
+ dkLen: CLIENT_PASSWORD_DERIVED_KEY_BYTES,
24
+ }));
25
+ }
26
+ export async function createClientPasswordState(options) {
27
+ const salt = randomBytes(16);
28
+ const derivedKey = await derivePasswordKey(options.passwordBytes, salt);
29
+ const verifier = encryptBytesWithKey(Buffer.from(CLIENT_PASSWORD_VERIFIER_TEXT, "utf8"), derivedKey, {
30
+ format: CLIENT_PASSWORD_VERIFIER_FORMAT,
31
+ wrappedBy: "client-password-verifier",
32
+ });
33
+ return {
34
+ state: {
35
+ format: CLIENT_PASSWORD_STATE_FORMAT,
36
+ version: 1,
37
+ passwordHint: options.passwordHint,
38
+ kdf: {
39
+ name: "argon2id",
40
+ memoryKib: CLIENT_PASSWORD_KDF.memoryKib,
41
+ iterations: CLIENT_PASSWORD_KDF.iterations,
42
+ parallelism: CLIENT_PASSWORD_KDF.parallelism,
43
+ salt: salt.toString("base64"),
44
+ },
45
+ verifier: {
46
+ cipher: "aes-256-gcm",
47
+ nonce: verifier.nonce,
48
+ tag: verifier.tag,
49
+ ciphertext: verifier.ciphertext,
50
+ },
51
+ },
52
+ derivedKey,
53
+ };
54
+ }
55
+ export function createWrappedSecretEnvelope(secret, derivedKey) {
56
+ const envelope = encryptBytesWithKey(secret, derivedKey, {
57
+ format: LOCAL_SECRET_ENVELOPE_FORMAT,
58
+ wrappedBy: "client-password",
59
+ });
60
+ return {
61
+ format: LOCAL_SECRET_ENVELOPE_FORMAT,
62
+ version: 1,
63
+ cipher: "aes-256-gcm",
64
+ wrappedBy: "client-password",
65
+ nonce: envelope.nonce,
66
+ tag: envelope.tag,
67
+ ciphertext: envelope.ciphertext,
68
+ };
69
+ }
70
+ export async function verifyPassword(options) {
71
+ const derivedKey = await derivePasswordKey(options.passwordBytes, Buffer.from(options.state.kdf.salt, "base64"));
72
+ try {
73
+ const plaintext = decryptBytesWithKey({
74
+ format: CLIENT_PASSWORD_VERIFIER_FORMAT,
75
+ version: 1,
76
+ cipher: "aes-256-gcm",
77
+ wrappedBy: "client-password-verifier",
78
+ nonce: options.state.verifier.nonce,
79
+ tag: options.state.verifier.tag,
80
+ ciphertext: options.state.verifier.ciphertext,
81
+ }, derivedKey);
82
+ if (plaintext.toString("utf8") !== CLIENT_PASSWORD_VERIFIER_TEXT) {
83
+ zeroizeBuffer(derivedKey);
84
+ return null;
85
+ }
86
+ return derivedKey;
87
+ }
88
+ catch {
89
+ zeroizeBuffer(derivedKey);
90
+ return null;
91
+ }
92
+ }
93
+ export function decryptWrappedSecretEnvelope(envelope, derivedKey) {
94
+ return decryptBytesWithKey(envelope, derivedKey);
95
+ }
96
+ export function encryptSessionSecretBase64(options) {
97
+ const nonce = randomBytes(12);
98
+ const cipher = createCipheriv("aes-256-gcm", options.key, nonce);
99
+ const ciphertext = Buffer.concat([
100
+ cipher.update(Buffer.from(options.secretBase64, "base64")),
101
+ cipher.final(),
102
+ ]);
103
+ const tag = cipher.getAuthTag();
104
+ return {
105
+ nonce: nonce.toString("base64"),
106
+ tag: tag.toString("base64"),
107
+ ciphertext: ciphertext.toString("base64"),
108
+ };
109
+ }
110
+ export function decryptSessionSecretBase64(options) {
111
+ const decipher = createDecipheriv("aes-256-gcm", options.key, Buffer.from(options.envelope.nonce, "base64"));
112
+ decipher.setAuthTag(Buffer.from(options.envelope.tag, "base64"));
113
+ return Buffer.concat([
114
+ decipher.update(Buffer.from(options.envelope.ciphertext, "base64")),
115
+ decipher.final(),
116
+ ]).toString("base64");
117
+ }
@@ -0,0 +1,10 @@
1
+ import type { ClientPasswordRotationJournalV1, ClientPasswordStateV1, LocalSecretFile, WrappedSecretEnvelopeV1 } from "./types.js";
2
+ export declare function readLocalSecretFile(path: string): Promise<LocalSecretFile>;
3
+ export declare function loadClientPasswordStateOrNull(path: string): Promise<ClientPasswordStateV1 | null>;
4
+ export declare function loadClientPasswordRotationJournalOrNull(path: string): Promise<ClientPasswordRotationJournalV1 | null>;
5
+ export declare function writeClientPasswordState(path: string, state: ClientPasswordStateV1): Promise<void>;
6
+ export declare function writeClientPasswordRotationJournal(path: string, journal: ClientPasswordRotationJournalV1): Promise<void>;
7
+ export declare function writeWrappedSecretEnvelope(path: string, envelope: WrappedSecretEnvelopeV1): Promise<void>;
8
+ export declare function listLocalSecretFilesForTesting(options: {
9
+ directoryPath: string;
10
+ }): Promise<string[]>;
@@ -0,0 +1,109 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { writeJsonFileAtomic } from "../../fs/atomic.js";
3
+ import { isMissingFileError, } from "./context.js";
4
+ import { CLIENT_PASSWORD_ROTATION_JOURNAL_FORMAT, CLIENT_PASSWORD_STATE_FORMAT, LOCAL_SECRET_ENVELOPE_FORMAT, } from "./types.js";
5
+ function isClientPasswordStateV1(value) {
6
+ return value !== null
7
+ && typeof value === "object"
8
+ && value.format === CLIENT_PASSWORD_STATE_FORMAT
9
+ && value.version === 1
10
+ && typeof value.passwordHint === "string"
11
+ && value.kdf?.name === "argon2id"
12
+ && typeof value.verifier?.nonce === "string"
13
+ && typeof value.verifier?.tag === "string"
14
+ && typeof value.verifier?.ciphertext === "string";
15
+ }
16
+ function isWrappedSecretEnvelope(value) {
17
+ return value !== null
18
+ && typeof value === "object"
19
+ && value.format === LOCAL_SECRET_ENVELOPE_FORMAT
20
+ && value.version === 1
21
+ && value.cipher === "aes-256-gcm"
22
+ && value.wrappedBy === "client-password"
23
+ && typeof value.nonce === "string"
24
+ && typeof value.tag === "string"
25
+ && typeof value.ciphertext === "string";
26
+ }
27
+ function isClientPasswordRotationJournalV1(value) {
28
+ return value !== null
29
+ && typeof value === "object"
30
+ && value.format === CLIENT_PASSWORD_ROTATION_JOURNAL_FORMAT
31
+ && value.version === 1
32
+ && isClientPasswordStateV1(value.nextState)
33
+ && Array.isArray(value.secrets)
34
+ && (value.secrets).every((entry) => (entry !== null
35
+ && typeof entry === "object"
36
+ && typeof entry.keyId === "string"
37
+ && entry.keyId.trim().length > 0
38
+ && isWrappedSecretEnvelope(entry.envelope)));
39
+ }
40
+ export async function readLocalSecretFile(path) {
41
+ try {
42
+ const raw = await readFile(path, "utf8");
43
+ const trimmed = raw.trim();
44
+ try {
45
+ const parsed = JSON.parse(trimmed);
46
+ if (isWrappedSecretEnvelope(parsed)) {
47
+ return {
48
+ state: "wrapped",
49
+ envelope: parsed,
50
+ };
51
+ }
52
+ }
53
+ catch {
54
+ // Legacy local secrets were raw base64 bytes.
55
+ }
56
+ return {
57
+ state: "raw",
58
+ secret: new Uint8Array(Buffer.from(trimmed, "base64")),
59
+ };
60
+ }
61
+ catch (error) {
62
+ if (isMissingFileError(error)) {
63
+ return { state: "missing" };
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+ export async function loadClientPasswordStateOrNull(path) {
69
+ try {
70
+ const parsed = JSON.parse(await readFile(path, "utf8"));
71
+ if (!isClientPasswordStateV1(parsed)) {
72
+ return null;
73
+ }
74
+ return parsed;
75
+ }
76
+ catch (error) {
77
+ if (isMissingFileError(error)) {
78
+ return null;
79
+ }
80
+ return null;
81
+ }
82
+ }
83
+ export async function loadClientPasswordRotationJournalOrNull(path) {
84
+ try {
85
+ const parsed = JSON.parse(await readFile(path, "utf8"));
86
+ if (!isClientPasswordRotationJournalV1(parsed)) {
87
+ return null;
88
+ }
89
+ return parsed;
90
+ }
91
+ catch (error) {
92
+ if (isMissingFileError(error)) {
93
+ return null;
94
+ }
95
+ return null;
96
+ }
97
+ }
98
+ export async function writeClientPasswordState(path, state) {
99
+ await writeJsonFileAtomic(path, state, { mode: 0o600 });
100
+ }
101
+ export async function writeClientPasswordRotationJournal(path, journal) {
102
+ await writeJsonFileAtomic(path, journal, { mode: 0o600 });
103
+ }
104
+ export async function writeWrappedSecretEnvelope(path, envelope) {
105
+ await writeJsonFileAtomic(path, envelope, { mode: 0o600 });
106
+ }
107
+ export function listLocalSecretFilesForTesting(options) {
108
+ return readdir(options.directoryPath).catch(() => []);
109
+ }
@@ -0,0 +1,11 @@
1
+ import type { ClientPasswordResolvedContext } from "./types.js";
2
+ export interface LegacyClientPasswordCleanupDependencies {
3
+ runCleanupPass?(context: ClientPasswordResolvedContext): Promise<void>;
4
+ }
5
+ export declare function resolveLegacyClientPasswordAgentEndpointForTesting(stateRoot: string, hostPlatform?: NodeJS.Platform): string;
6
+ export declare function extractLegacyClientPasswordAgentProcessIdsForTesting(options: {
7
+ endpoint: string;
8
+ hostPlatform: NodeJS.Platform;
9
+ stdout: string;
10
+ }): number[];
11
+ export declare function cleanupLegacyClientPasswordArtifactsResolved(context: ClientPasswordResolvedContext, deps?: LegacyClientPasswordCleanupDependencies): Promise<void>;
@@ -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
+ }>;