@cogcoin/client 1.1.6 → 1.1.8

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 (125) 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 +47 -127
  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-state.js +10 -0
  46. package/dist/wallet/mining/engine-types.d.ts +1 -0
  47. package/dist/wallet/mining/index.d.ts +1 -1
  48. package/dist/wallet/mining/index.js +1 -1
  49. package/dist/wallet/mining/publish.d.ts +3 -0
  50. package/dist/wallet/mining/publish.js +78 -6
  51. package/dist/wallet/mining/runner.d.ts +0 -32
  52. package/dist/wallet/mining/runner.js +59 -104
  53. package/dist/wallet/mining/stop.d.ts +7 -0
  54. package/dist/wallet/mining/stop.js +23 -0
  55. package/dist/wallet/mining/supervisor.d.ts +2 -36
  56. package/dist/wallet/mining/supervisor.js +139 -246
  57. package/dist/wallet/mining/visualizer-sync.js +79 -15
  58. package/dist/wallet/read/context.d.ts +1 -5
  59. package/dist/wallet/read/context.js +21 -205
  60. package/dist/wallet/read/managed-services.d.ts +33 -0
  61. package/dist/wallet/read/managed-services.js +222 -0
  62. package/dist/wallet/reset/artifacts.d.ts +16 -0
  63. package/dist/wallet/reset/artifacts.js +141 -0
  64. package/dist/wallet/reset/execution.d.ts +38 -0
  65. package/dist/wallet/reset/execution.js +458 -0
  66. package/dist/wallet/reset/preflight.d.ts +7 -0
  67. package/dist/wallet/reset/preflight.js +116 -0
  68. package/dist/wallet/reset/preview.d.ts +2 -0
  69. package/dist/wallet/reset/preview.js +50 -0
  70. package/dist/wallet/reset/process-cleanup.d.ts +12 -0
  71. package/dist/wallet/reset/process-cleanup.js +179 -0
  72. package/dist/wallet/reset/types.d.ts +189 -0
  73. package/dist/wallet/reset/types.js +1 -0
  74. package/dist/wallet/reset.d.ts +4 -119
  75. package/dist/wallet/reset.js +4 -882
  76. package/dist/wallet/state/client-password/bootstrap.d.ts +2 -0
  77. package/dist/wallet/state/client-password/bootstrap.js +3 -0
  78. package/dist/wallet/state/client-password/context.d.ts +10 -0
  79. package/dist/wallet/state/client-password/context.js +46 -0
  80. package/dist/wallet/state/client-password/crypto.d.ts +34 -0
  81. package/dist/wallet/state/client-password/crypto.js +117 -0
  82. package/dist/wallet/state/client-password/files.d.ts +10 -0
  83. package/dist/wallet/state/client-password/files.js +109 -0
  84. package/dist/wallet/state/client-password/legacy-cleanup.d.ts +11 -0
  85. package/dist/wallet/state/client-password/legacy-cleanup.js +338 -0
  86. package/dist/wallet/state/client-password/messages.d.ts +3 -0
  87. package/dist/wallet/state/client-password/messages.js +9 -0
  88. package/dist/wallet/state/client-password/migration.d.ts +4 -0
  89. package/dist/wallet/state/client-password/migration.js +32 -0
  90. package/dist/wallet/state/client-password/prompts.d.ts +12 -0
  91. package/dist/wallet/state/client-password/prompts.js +79 -0
  92. package/dist/wallet/state/client-password/protected-secrets.d.ts +13 -0
  93. package/dist/wallet/state/client-password/protected-secrets.js +90 -0
  94. package/dist/wallet/state/client-password/readiness.d.ts +4 -0
  95. package/dist/wallet/state/client-password/readiness.js +48 -0
  96. package/dist/wallet/state/client-password/references.d.ts +1 -0
  97. package/dist/wallet/state/client-password/references.js +56 -0
  98. package/dist/wallet/state/client-password/rotation.d.ts +6 -0
  99. package/dist/wallet/state/client-password/rotation.js +98 -0
  100. package/dist/wallet/state/client-password/session-policy.d.ts +6 -0
  101. package/dist/wallet/state/client-password/session-policy.js +28 -0
  102. package/dist/wallet/state/client-password/session.d.ts +19 -0
  103. package/dist/wallet/state/client-password/session.js +170 -0
  104. package/dist/wallet/state/client-password/setup.d.ts +8 -0
  105. package/dist/wallet/state/client-password/setup.js +49 -0
  106. package/dist/wallet/state/client-password/types.d.ts +82 -0
  107. package/dist/wallet/state/client-password/types.js +5 -0
  108. package/dist/wallet/state/client-password.d.ts +7 -38
  109. package/dist/wallet/state/client-password.js +52 -937
  110. package/dist/wallet/tx/anchor.js +123 -216
  111. package/dist/wallet/tx/cog.js +294 -489
  112. package/dist/wallet/tx/common.d.ts +2 -0
  113. package/dist/wallet/tx/common.js +2 -0
  114. package/dist/wallet/tx/domain-admin.js +111 -220
  115. package/dist/wallet/tx/domain-market.js +401 -681
  116. package/dist/wallet/tx/executor.d.ts +176 -0
  117. package/dist/wallet/tx/executor.js +302 -0
  118. package/dist/wallet/tx/field.js +109 -215
  119. package/dist/wallet/tx/register.js +158 -269
  120. package/dist/wallet/tx/reputation.js +120 -227
  121. package/package.json +1 -1
  122. package/dist/wallet/mining/worker-main.d.ts +0 -1
  123. package/dist/wallet/mining/worker-main.js +0 -17
  124. package/dist/wallet/state/client-password-agent.d.ts +0 -1
  125. package/dist/wallet/state/client-password-agent.js +0 -211
@@ -0,0 +1,2 @@
1
+ import type { ClientPasswordAgentBootstrapState } from "./types.js";
2
+ export declare function createAgentBootstrapState(state: ClientPasswordAgentBootstrapState): ClientPasswordAgentBootstrapState;
@@ -0,0 +1,3 @@
1
+ export function createAgentBootstrapState(state) {
2
+ return state;
3
+ }
@@ -0,0 +1,10 @@
1
+ import type { WalletRuntimePaths } from "../../runtime.js";
2
+ import type { ClientPasswordResolvedContext, ClientPasswordStorageOptions } from "./types.js";
3
+ export declare function resolveLocalSecretFilePath(directoryPath: string, keyId: string): string;
4
+ export declare function resolveClientPasswordStatePath(directoryPath: string): string;
5
+ export declare function resolveClientPasswordRotationJournalPath(directoryPath: string): string;
6
+ export declare function isMissingFileError(error: unknown): boolean;
7
+ export declare function createRuntimeError(code: string, cause?: unknown): Error;
8
+ export declare function resolveClientPasswordContext(options: ClientPasswordStorageOptions): ClientPasswordResolvedContext;
9
+ export declare function resolveClientPasswordStorageOptionsForWalletPaths(paths: Pick<WalletRuntimePaths, "stateRoot" | "runtimeRoot">, platform?: NodeJS.Platform): ClientPasswordStorageOptions;
10
+ export declare function createLegacyKeychainServiceName(): string;
@@ -0,0 +1,46 @@
1
+ import { join } from "node:path";
2
+ function sanitizeSecretKeyId(keyId) {
3
+ return keyId.replace(/[^a-zA-Z0-9._-]+/g, "-");
4
+ }
5
+ export function resolveLocalSecretFilePath(directoryPath, keyId) {
6
+ return join(directoryPath, `${sanitizeSecretKeyId(keyId)}.secret`);
7
+ }
8
+ export function resolveClientPasswordStatePath(directoryPath) {
9
+ return join(directoryPath, "client-password.json");
10
+ }
11
+ export function resolveClientPasswordRotationJournalPath(directoryPath) {
12
+ return join(directoryPath, "client-password-rotation.json");
13
+ }
14
+ export function isMissingFileError(error) {
15
+ return error instanceof Error
16
+ && "code" in error
17
+ && error.code === "ENOENT";
18
+ }
19
+ export function createRuntimeError(code, cause) {
20
+ return cause === undefined ? new Error(code) : new Error(code, { cause });
21
+ }
22
+ export function resolveClientPasswordContext(options) {
23
+ return {
24
+ ...options,
25
+ legacyMacKeychainReader: options.legacyMacKeychainReader ?? null,
26
+ passwordStatePath: resolveClientPasswordStatePath(options.directoryPath),
27
+ rotationJournalPath: resolveClientPasswordRotationJournalPath(options.directoryPath),
28
+ };
29
+ }
30
+ export function resolveClientPasswordStorageOptionsForWalletPaths(paths, platform = process.platform) {
31
+ return {
32
+ platform,
33
+ stateRoot: paths.stateRoot,
34
+ runtimeRoot: paths.runtimeRoot,
35
+ directoryPath: join(paths.stateRoot, "secrets"),
36
+ runtimeErrorCode: platform === "win32"
37
+ ? "wallet_secret_provider_windows_runtime_error"
38
+ : platform === "darwin"
39
+ ? "wallet_secret_provider_macos_runtime_error"
40
+ : "wallet_secret_provider_linux_runtime_error",
41
+ legacyMacKeychainReader: null,
42
+ };
43
+ }
44
+ export function createLegacyKeychainServiceName() {
45
+ return "org.cogcoin.wallet";
46
+ }
@@ -0,0 +1,34 @@
1
+ import type { ClientPasswordStateV1, WrappedSecretEnvelopeV1 } from "./types.js";
2
+ export declare const CLIENT_PASSWORD_DEFAULT_UNLOCK_SECONDS = 3600;
3
+ export declare const CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS = 86400;
4
+ export declare function zeroizeBuffer(buffer: Uint8Array | null | undefined): void;
5
+ export declare function derivePasswordKey(passwordBytes: Uint8Array, saltBytes: Uint8Array): Promise<Buffer>;
6
+ export declare function createClientPasswordState(options: {
7
+ passwordBytes: Uint8Array;
8
+ passwordHint: string;
9
+ }): Promise<{
10
+ state: ClientPasswordStateV1;
11
+ derivedKey: Buffer;
12
+ }>;
13
+ export declare function createWrappedSecretEnvelope(secret: Uint8Array, derivedKey: Uint8Array): WrappedSecretEnvelopeV1;
14
+ export declare function verifyPassword(options: {
15
+ state: ClientPasswordStateV1;
16
+ passwordBytes: Uint8Array;
17
+ }): Promise<Buffer | null>;
18
+ export declare function decryptWrappedSecretEnvelope(envelope: WrappedSecretEnvelopeV1, derivedKey: Uint8Array): Uint8Array;
19
+ export declare function encryptSessionSecretBase64(options: {
20
+ key: Uint8Array;
21
+ secretBase64: string;
22
+ }): {
23
+ nonce: string;
24
+ tag: string;
25
+ ciphertext: string;
26
+ };
27
+ export declare function decryptSessionSecretBase64(options: {
28
+ key: Uint8Array;
29
+ envelope: {
30
+ nonce: string;
31
+ tag: string;
32
+ ciphertext: string;
33
+ };
34
+ }): string;
@@ -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
+ }