@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,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[]>;
@@ -0,0 +1,56 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { isMissingFileError } from "./context.js";
4
+ async function collectWalletStateRoots(stateRoot) {
5
+ const roots = [stateRoot];
6
+ const seedsRoot = join(stateRoot, "seeds");
7
+ try {
8
+ const entries = await readdir(seedsRoot, { withFileTypes: true });
9
+ for (const entry of entries) {
10
+ if (entry.isDirectory()) {
11
+ roots.push(join(seedsRoot, entry.name));
12
+ }
13
+ }
14
+ }
15
+ catch (error) {
16
+ if (!isMissingFileError(error)) {
17
+ throw error;
18
+ }
19
+ }
20
+ return roots;
21
+ }
22
+ async function readReferencedSecretIdsFromWalletStateRoot(walletStateRoot) {
23
+ const ids = new Set();
24
+ const candidatePaths = [
25
+ join(walletStateRoot, "wallet-state.enc"),
26
+ join(walletStateRoot, "wallet-state.enc.bak"),
27
+ join(walletStateRoot, "wallet-init-pending.enc"),
28
+ join(walletStateRoot, "wallet-init-pending.enc.bak"),
29
+ ];
30
+ for (const candidatePath of candidatePaths) {
31
+ try {
32
+ const parsed = JSON.parse(await readFile(candidatePath, "utf8"));
33
+ const keyId = parsed.secretProvider?.keyId?.trim() ?? "";
34
+ if (keyId.length > 0) {
35
+ ids.add(keyId);
36
+ }
37
+ }
38
+ catch (error) {
39
+ if (!isMissingFileError(error)) {
40
+ continue;
41
+ }
42
+ }
43
+ }
44
+ return ids;
45
+ }
46
+ export async function collectReferencedSecretIds(stateRoot) {
47
+ const ids = new Set();
48
+ const roots = await collectWalletStateRoots(stateRoot);
49
+ for (const root of roots) {
50
+ const rootIds = await readReferencedSecretIdsFromWalletStateRoot(root);
51
+ for (const keyId of rootIds) {
52
+ ids.add(keyId);
53
+ }
54
+ }
55
+ return [...ids].sort((left, right) => left.localeCompare(right));
56
+ }
@@ -0,0 +1,6 @@
1
+ import type { ClientPasswordPrompt, ClientPasswordResolvedContext, ClientPasswordSessionStatus } from "./types.js";
2
+ export declare function finalizePendingClientPasswordRotationIfNeeded(context: ClientPasswordResolvedContext): Promise<void>;
3
+ export declare function changeClientPasswordResolved(options: {
4
+ context: ClientPasswordResolvedContext;
5
+ prompt: ClientPasswordPrompt;
6
+ }): Promise<ClientPasswordSessionStatus>;
@@ -0,0 +1,98 @@
1
+ import { mkdir, rm } from "node:fs/promises";
2
+ import { resolveLocalSecretFilePath } from "./context.js";
3
+ import { createClientPasswordState, createWrappedSecretEnvelope, decryptWrappedSecretEnvelope, zeroizeBuffer, } from "./crypto.js";
4
+ import { loadClientPasswordRotationJournalOrNull, readLocalSecretFile, writeClientPasswordRotationJournal, writeClientPasswordState, writeWrappedSecretEnvelope, } from "./files.js";
5
+ import { collectReferencedSecretIds } from "./references.js";
6
+ import { promptForNewPassword, promptForVerifiedClientPassword, } from "./prompts.js";
7
+ import { resolveClientPasswordPromptSessionPolicy, resolvePostChangeClientPasswordUnlockUntilUnixMs, } from "./session-policy.js";
8
+ import { readClientPasswordSessionStatusResolved, startClientPasswordSessionWithExpiryResolved, } from "./session.js";
9
+ export async function finalizePendingClientPasswordRotationIfNeeded(context) {
10
+ const journal = await loadClientPasswordRotationJournalOrNull(context.rotationJournalPath);
11
+ if (journal === null) {
12
+ return;
13
+ }
14
+ await mkdir(context.directoryPath, { recursive: true, mode: 0o700 });
15
+ for (const secretEntry of journal.secrets) {
16
+ await writeWrappedSecretEnvelope(resolveLocalSecretFilePath(context.directoryPath, secretEntry.keyId), secretEntry.envelope);
17
+ }
18
+ await writeClientPasswordState(context.passwordStatePath, journal.nextState);
19
+ await rm(context.rotationJournalPath, { force: true });
20
+ }
21
+ async function prepareClientPasswordRotation(options) {
22
+ const next = await createClientPasswordState({
23
+ passwordBytes: options.newPasswordBytes,
24
+ passwordHint: options.newPasswordHint,
25
+ });
26
+ const keyIds = await collectReferencedSecretIds(options.stateRoot);
27
+ const secrets = [];
28
+ try {
29
+ for (const keyId of keyIds) {
30
+ const localState = await readLocalSecretFile(resolveLocalSecretFilePath(options.directoryPath, keyId));
31
+ if (localState.state === "missing") {
32
+ throw new Error(`wallet_secret_missing_${keyId}`);
33
+ }
34
+ if (localState.state === "raw") {
35
+ throw new Error("wallet_client_password_migration_required");
36
+ }
37
+ const secret = decryptWrappedSecretEnvelope(localState.envelope, options.currentDerivedKey);
38
+ try {
39
+ secrets.push({
40
+ keyId,
41
+ envelope: createWrappedSecretEnvelope(secret, next.derivedKey),
42
+ });
43
+ }
44
+ finally {
45
+ zeroizeBuffer(secret);
46
+ }
47
+ }
48
+ return {
49
+ journal: {
50
+ format: "cogcoin-client-password-rotation",
51
+ version: 1,
52
+ nextState: next.state,
53
+ secrets,
54
+ },
55
+ newDerivedKey: next.derivedKey,
56
+ };
57
+ }
58
+ catch (error) {
59
+ zeroizeBuffer(next.derivedKey);
60
+ throw error;
61
+ }
62
+ }
63
+ export async function changeClientPasswordResolved(options) {
64
+ await finalizePendingClientPasswordRotationIfNeeded(options.context);
65
+ const previousSession = await readClientPasswordSessionStatusResolved(options.context);
66
+ const currentDerivedKey = await promptForVerifiedClientPassword({
67
+ context: options.context,
68
+ prompt: options.prompt,
69
+ promptMessage: "Current client password: ",
70
+ ttyErrorCode: "wallet_client_password_change_requires_tty",
71
+ });
72
+ const nextPassword = await promptForNewPassword(options.prompt);
73
+ let newDerivedKey = null;
74
+ try {
75
+ const prepared = await prepareClientPasswordRotation({
76
+ ...options.context,
77
+ currentDerivedKey,
78
+ newPasswordBytes: nextPassword.passwordBytes,
79
+ newPasswordHint: nextPassword.passwordHint,
80
+ });
81
+ newDerivedKey = prepared.newDerivedKey;
82
+ await mkdir(options.context.directoryPath, { recursive: true, mode: 0o700 });
83
+ await writeClientPasswordRotationJournal(options.context.rotationJournalPath, prepared.journal);
84
+ await finalizePendingClientPasswordRotationIfNeeded(options.context);
85
+ const session = await startClientPasswordSessionWithExpiryResolved({
86
+ ...options.context,
87
+ derivedKey: newDerivedKey,
88
+ unlockUntilUnixMs: resolvePostChangeClientPasswordUnlockUntilUnixMs(previousSession, resolveClientPasswordPromptSessionPolicy(options.prompt)),
89
+ });
90
+ newDerivedKey = null;
91
+ return session;
92
+ }
93
+ finally {
94
+ zeroizeBuffer(currentDerivedKey);
95
+ zeroizeBuffer(nextPassword.passwordBytes);
96
+ zeroizeBuffer(newDerivedKey);
97
+ }
98
+ }
@@ -0,0 +1,6 @@
1
+ import type { ClientPasswordPrompt, ClientPasswordSessionStatus } from "./types.js";
2
+ export type ClientPasswordSessionPolicy = "default-60m" | "init-24h" | "mining-indefinite";
3
+ export declare function bindClientPasswordPromptSessionPolicy<T extends ClientPasswordPrompt>(prompt: T, policy: ClientPasswordSessionPolicy): T;
4
+ export declare function resolveClientPasswordPromptSessionPolicy(prompt: ClientPasswordPrompt | null | undefined): ClientPasswordSessionPolicy;
5
+ export declare function resolveClientPasswordSessionUnlockUntilUnixMs(policy: ClientPasswordSessionPolicy, nowUnixMs?: number): number | null;
6
+ export declare function resolvePostChangeClientPasswordUnlockUntilUnixMs(status: ClientPasswordSessionStatus, policy: ClientPasswordSessionPolicy, nowUnixMs?: number): number | null;
@@ -0,0 +1,28 @@
1
+ import { CLIENT_PASSWORD_DEFAULT_UNLOCK_SECONDS, CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS } from "./crypto.js";
2
+ const promptPolicies = new WeakMap();
3
+ export function bindClientPasswordPromptSessionPolicy(prompt, policy) {
4
+ promptPolicies.set(prompt, policy);
5
+ return prompt;
6
+ }
7
+ export function resolveClientPasswordPromptSessionPolicy(prompt) {
8
+ if (prompt == null) {
9
+ return "default-60m";
10
+ }
11
+ return promptPolicies.get(prompt) ?? "default-60m";
12
+ }
13
+ export function resolveClientPasswordSessionUnlockUntilUnixMs(policy, nowUnixMs = Date.now()) {
14
+ switch (policy) {
15
+ case "default-60m":
16
+ return nowUnixMs + (CLIENT_PASSWORD_DEFAULT_UNLOCK_SECONDS * 1_000);
17
+ case "init-24h":
18
+ return nowUnixMs + (CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS * 1_000);
19
+ case "mining-indefinite":
20
+ return null;
21
+ }
22
+ }
23
+ export function resolvePostChangeClientPasswordUnlockUntilUnixMs(status, policy, nowUnixMs = Date.now()) {
24
+ if (status.unlocked) {
25
+ return status.unlockUntilUnixMs;
26
+ }
27
+ return resolveClientPasswordSessionUnlockUntilUnixMs(policy, nowUnixMs);
28
+ }
@@ -0,0 +1,19 @@
1
+ import { type ClientPasswordSessionPolicy } from "./session-policy.js";
2
+ import type { ClientPasswordPrompt, ClientPasswordResolvedContext, ClientPasswordSessionStatus, WrappedSecretEnvelopeV1 } from "./types.js";
3
+ export declare function destroyAllClientPasswordSessionsResolved(): void;
4
+ export declare function readClientPasswordSessionStatusResolved(context: ClientPasswordResolvedContext): Promise<ClientPasswordSessionStatus>;
5
+ export declare function lockClientPasswordSessionResolved(context: ClientPasswordResolvedContext): Promise<ClientPasswordSessionStatus>;
6
+ export declare function startClientPasswordSessionResolved(options: ClientPasswordResolvedContext & {
7
+ derivedKey: Buffer;
8
+ sessionPolicy: ClientPasswordSessionPolicy;
9
+ }): Promise<ClientPasswordSessionStatus>;
10
+ export declare function startClientPasswordSessionWithExpiryResolved(options: ClientPasswordResolvedContext & {
11
+ derivedKey: Buffer;
12
+ unlockUntilUnixMs: number | null;
13
+ }): Promise<ClientPasswordSessionStatus>;
14
+ export declare function unlockClientPasswordSessionResolved(options: {
15
+ context: ClientPasswordResolvedContext;
16
+ prompt: ClientPasswordPrompt;
17
+ }): Promise<ClientPasswordSessionStatus>;
18
+ export declare function decryptClientProtectedSecretWithSessionResolved(context: ClientPasswordResolvedContext, envelope: WrappedSecretEnvelopeV1): Uint8Array | null;
19
+ export declare function encryptClientProtectedSecretWithSessionResolved(context: ClientPasswordResolvedContext, secret: Uint8Array): WrappedSecretEnvelopeV1 | null;
@@ -0,0 +1,170 @@
1
+ import { createWrappedSecretEnvelope, decryptWrappedSecretEnvelope, zeroizeBuffer, } from "./crypto.js";
2
+ import { promptForVerifiedClientPassword } from "./prompts.js";
3
+ import { resolveClientPasswordPromptSessionPolicy, resolveClientPasswordSessionUnlockUntilUnixMs, } from "./session-policy.js";
4
+ const activeSessions = new Map();
5
+ let processCleanupRegistered = false;
6
+ function resolveSessionCacheKey(context) {
7
+ return `${context.platform}\n${context.stateRoot}\n${context.directoryPath}\n${context.passwordStatePath}`;
8
+ }
9
+ function clearExpiryTimer(session) {
10
+ if (session.expiryTimer !== null) {
11
+ clearTimeout(session.expiryTimer);
12
+ session.expiryTimer = null;
13
+ }
14
+ }
15
+ function destroySession(session) {
16
+ if (session === undefined) {
17
+ return;
18
+ }
19
+ clearExpiryTimer(session);
20
+ zeroizeBuffer(session.derivedKey);
21
+ }
22
+ function clearSessionByKey(cacheKey) {
23
+ const existing = activeSessions.get(cacheKey);
24
+ if (existing === undefined) {
25
+ return;
26
+ }
27
+ activeSessions.delete(cacheKey);
28
+ destroySession(existing);
29
+ }
30
+ function scheduleSessionExpiry(cacheKey, session) {
31
+ clearExpiryTimer(session);
32
+ if (session.unlockUntilUnixMs === null) {
33
+ return;
34
+ }
35
+ const remainingMs = Math.max(0, session.unlockUntilUnixMs - Date.now());
36
+ session.expiryTimer = setTimeout(() => {
37
+ clearSessionByKey(cacheKey);
38
+ }, remainingMs);
39
+ session.expiryTimer.unref();
40
+ }
41
+ export function destroyAllClientPasswordSessionsResolved() {
42
+ for (const session of activeSessions.values()) {
43
+ destroySession(session);
44
+ }
45
+ activeSessions.clear();
46
+ }
47
+ function registerProcessCleanup() {
48
+ if (processCleanupRegistered) {
49
+ return;
50
+ }
51
+ processCleanupRegistered = true;
52
+ process.once("exit", () => {
53
+ destroyAllClientPasswordSessionsResolved();
54
+ });
55
+ }
56
+ function getActiveSession(context) {
57
+ const cacheKey = resolveSessionCacheKey(context);
58
+ const session = activeSessions.get(cacheKey);
59
+ if (session === undefined) {
60
+ return null;
61
+ }
62
+ if (session.unlockUntilUnixMs !== null && session.unlockUntilUnixMs <= Date.now()) {
63
+ clearSessionByKey(cacheKey);
64
+ return null;
65
+ }
66
+ return session;
67
+ }
68
+ function putActiveSession(options) {
69
+ registerProcessCleanup();
70
+ const cacheKey = resolveSessionCacheKey(options);
71
+ clearSessionByKey(cacheKey);
72
+ if (options.unlockUntilUnixMs !== null && options.unlockUntilUnixMs <= Date.now()) {
73
+ return {
74
+ unlocked: false,
75
+ unlockUntilUnixMs: null,
76
+ };
77
+ }
78
+ const session = {
79
+ derivedKey: Buffer.from(options.derivedKey),
80
+ unlockUntilUnixMs: options.unlockUntilUnixMs,
81
+ expiryTimer: null,
82
+ };
83
+ activeSessions.set(cacheKey, session);
84
+ scheduleSessionExpiry(cacheKey, session);
85
+ return {
86
+ unlocked: true,
87
+ unlockUntilUnixMs: session.unlockUntilUnixMs,
88
+ };
89
+ }
90
+ export async function readClientPasswordSessionStatusResolved(context) {
91
+ const session = getActiveSession(context);
92
+ return {
93
+ unlocked: session !== null,
94
+ unlockUntilUnixMs: session?.unlockUntilUnixMs ?? null,
95
+ };
96
+ }
97
+ export async function lockClientPasswordSessionResolved(context) {
98
+ clearSessionByKey(resolveSessionCacheKey(context));
99
+ return {
100
+ unlocked: false,
101
+ unlockUntilUnixMs: null,
102
+ };
103
+ }
104
+ export async function startClientPasswordSessionResolved(options) {
105
+ return await startClientPasswordSessionWithExpiryResolved({
106
+ ...options,
107
+ unlockUntilUnixMs: resolveClientPasswordSessionUnlockUntilUnixMs(options.sessionPolicy),
108
+ });
109
+ }
110
+ export async function startClientPasswordSessionWithExpiryResolved(options) {
111
+ try {
112
+ return putActiveSession(options);
113
+ }
114
+ finally {
115
+ zeroizeBuffer(options.derivedKey);
116
+ }
117
+ }
118
+ async function refreshClientPasswordSessionResolved(context) {
119
+ const session = getActiveSession(context);
120
+ if (session === null) {
121
+ return null;
122
+ }
123
+ session.unlockUntilUnixMs = context.unlockUntilUnixMs;
124
+ scheduleSessionExpiry(resolveSessionCacheKey(context), session);
125
+ return await readClientPasswordSessionStatusResolved(context);
126
+ }
127
+ async function unlockClientPasswordSessionWithPromptResolved(options) {
128
+ const derivedKey = await promptForVerifiedClientPassword({
129
+ context: options.context,
130
+ prompt: options.prompt,
131
+ promptMessage: "Client password: ",
132
+ ttyErrorCode: "wallet_client_password_unlock_requires_tty",
133
+ });
134
+ return await startClientPasswordSessionResolved({
135
+ ...options.context,
136
+ derivedKey,
137
+ sessionPolicy: resolveClientPasswordPromptSessionPolicy(options.prompt),
138
+ });
139
+ }
140
+ export async function unlockClientPasswordSessionResolved(options) {
141
+ const sessionPolicy = resolveClientPasswordPromptSessionPolicy(options.prompt);
142
+ const currentStatus = await readClientPasswordSessionStatusResolved(options.context);
143
+ if (currentStatus.unlocked) {
144
+ const refreshed = await refreshClientPasswordSessionResolved({
145
+ ...options.context,
146
+ unlockUntilUnixMs: resolveClientPasswordSessionUnlockUntilUnixMs(sessionPolicy),
147
+ });
148
+ if (refreshed !== null) {
149
+ return refreshed;
150
+ }
151
+ }
152
+ if (!options.prompt.isInteractive) {
153
+ throw new Error("wallet_client_password_unlock_requires_tty");
154
+ }
155
+ return await unlockClientPasswordSessionWithPromptResolved(options);
156
+ }
157
+ export function decryptClientProtectedSecretWithSessionResolved(context, envelope) {
158
+ const session = getActiveSession(context);
159
+ if (session === null) {
160
+ return null;
161
+ }
162
+ return new Uint8Array(decryptWrappedSecretEnvelope(envelope, session.derivedKey));
163
+ }
164
+ export function encryptClientProtectedSecretWithSessionResolved(context, secret) {
165
+ const session = getActiveSession(context);
166
+ if (session === null) {
167
+ return null;
168
+ }
169
+ return createWrappedSecretEnvelope(secret, session.derivedKey);
170
+ }
@@ -0,0 +1,8 @@
1
+ import type { ClientPasswordPrompt, ClientPasswordResolvedContext, ClientPasswordSessionStatus, ClientPasswordSetupAction } from "./types.js";
2
+ export declare function ensureClientPasswordConfiguredResolved(options: {
3
+ context: ClientPasswordResolvedContext;
4
+ prompt: ClientPasswordPrompt;
5
+ }): Promise<{
6
+ action: ClientPasswordSetupAction;
7
+ session: ClientPasswordSessionStatus;
8
+ }>;