@cogcoin/client 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/bitcoind/indexer-daemon.d.ts +3 -7
- package/dist/bitcoind/indexer-daemon.js +39 -204
- package/dist/bitcoind/managed-runtime/bitcoind-policy.d.ts +16 -0
- package/dist/bitcoind/managed-runtime/bitcoind-policy.js +177 -0
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.d.ts +20 -0
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +74 -0
- package/dist/bitcoind/managed-runtime/bitcoind-status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/bitcoind-status.js +44 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
- package/dist/bitcoind/managed-runtime/indexer-runtime.d.ts +15 -0
- package/dist/bitcoind/managed-runtime/indexer-runtime.js +82 -0
- package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/status.js +59 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +77 -0
- package/dist/bitcoind/node.d.ts +2 -2
- package/dist/bitcoind/node.js +2 -2
- package/dist/bitcoind/rpc.d.ts +2 -1
- package/dist/bitcoind/rpc.js +53 -3
- package/dist/bitcoind/service.d.ts +2 -7
- package/dist/bitcoind/service.js +79 -207
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +2 -64
- package/dist/cli/commands/client-admin.js +3 -18
- package/dist/cli/commands/mining-runtime.js +4 -60
- package/dist/cli/commands/wallet-admin.js +6 -6
- package/dist/cli/context.js +1 -3
- package/dist/cli/mining-json.d.ts +1 -22
- package/dist/cli/mining-json.js +0 -23
- package/dist/cli/output.js +16 -2
- package/dist/cli/parse.js +0 -2
- package/dist/cli/preview-json.d.ts +1 -22
- package/dist/cli/preview-json.js +0 -19
- package/dist/cli/types.d.ts +1 -3
- package/dist/cli/wallet-format.js +1 -1
- package/dist/cli/workflow-hints.d.ts +1 -2
- package/dist/cli/workflow-hints.js +5 -8
- package/dist/wallet/lifecycle/access.d.ts +5 -0
- package/dist/wallet/lifecycle/access.js +79 -0
- package/dist/wallet/lifecycle/context.d.ts +26 -0
- package/dist/wallet/lifecycle/context.js +57 -0
- package/dist/wallet/lifecycle/managed-core.d.ts +1 -9
- package/dist/wallet/lifecycle/managed-core.js +3 -63
- package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
- package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
- package/dist/wallet/lifecycle/repair-indexer.js +117 -0
- package/dist/wallet/lifecycle/repair-mining.d.ts +1 -5
- package/dist/wallet/lifecycle/repair-mining.js +5 -39
- package/dist/wallet/lifecycle/repair.d.ts +2 -4
- package/dist/wallet/lifecycle/repair.js +74 -318
- package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
- package/dist/wallet/lifecycle/setup-prompts.js +88 -0
- package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
- package/dist/wallet/lifecycle/setup-state.js +159 -0
- package/dist/wallet/lifecycle/setup.d.ts +3 -4
- package/dist/wallet/lifecycle/setup.js +47 -351
- package/dist/wallet/lifecycle/types.d.ts +33 -5
- package/dist/wallet/managed-core-wallet.d.ts +2 -0
- package/dist/wallet/managed-core-wallet.js +27 -1
- package/dist/wallet/mining/candidate.d.ts +1 -0
- package/dist/wallet/mining/candidate.js +38 -6
- package/dist/wallet/mining/competitiveness.d.ts +1 -0
- package/dist/wallet/mining/competitiveness.js +6 -0
- package/dist/wallet/mining/cycle.d.ts +2 -0
- package/dist/wallet/mining/cycle.js +14 -4
- package/dist/wallet/mining/engine-types.d.ts +1 -0
- package/dist/wallet/mining/index.d.ts +1 -1
- package/dist/wallet/mining/index.js +1 -1
- package/dist/wallet/mining/publish.d.ts +3 -0
- package/dist/wallet/mining/publish.js +78 -6
- package/dist/wallet/mining/runner.d.ts +0 -32
- package/dist/wallet/mining/runner.js +59 -104
- package/dist/wallet/mining/stop.d.ts +7 -0
- package/dist/wallet/mining/stop.js +23 -0
- package/dist/wallet/mining/supervisor.d.ts +2 -36
- package/dist/wallet/mining/supervisor.js +139 -246
- package/dist/wallet/read/context.d.ts +1 -5
- package/dist/wallet/read/context.js +20 -379
- package/dist/wallet/read/managed-services.d.ts +33 -0
- package/dist/wallet/read/managed-services.js +222 -0
- package/dist/wallet/state/client-password/bootstrap.d.ts +2 -0
- package/dist/wallet/state/client-password/bootstrap.js +3 -0
- package/dist/wallet/state/client-password/context.d.ts +10 -0
- package/dist/wallet/state/client-password/context.js +46 -0
- package/dist/wallet/state/client-password/crypto.d.ts +34 -0
- package/dist/wallet/state/client-password/crypto.js +117 -0
- package/dist/wallet/state/client-password/files.d.ts +10 -0
- package/dist/wallet/state/client-password/files.js +109 -0
- package/dist/wallet/state/client-password/legacy-cleanup.d.ts +11 -0
- package/dist/wallet/state/client-password/legacy-cleanup.js +338 -0
- package/dist/wallet/state/client-password/messages.d.ts +3 -0
- package/dist/wallet/state/client-password/messages.js +9 -0
- package/dist/wallet/state/client-password/migration.d.ts +4 -0
- package/dist/wallet/state/client-password/migration.js +32 -0
- package/dist/wallet/state/client-password/prompts.d.ts +12 -0
- package/dist/wallet/state/client-password/prompts.js +79 -0
- package/dist/wallet/state/client-password/protected-secrets.d.ts +13 -0
- package/dist/wallet/state/client-password/protected-secrets.js +90 -0
- package/dist/wallet/state/client-password/readiness.d.ts +4 -0
- package/dist/wallet/state/client-password/readiness.js +48 -0
- package/dist/wallet/state/client-password/references.d.ts +1 -0
- package/dist/wallet/state/client-password/references.js +56 -0
- package/dist/wallet/state/client-password/rotation.d.ts +6 -0
- package/dist/wallet/state/client-password/rotation.js +98 -0
- package/dist/wallet/state/client-password/session-policy.d.ts +6 -0
- package/dist/wallet/state/client-password/session-policy.js +28 -0
- package/dist/wallet/state/client-password/session.d.ts +19 -0
- package/dist/wallet/state/client-password/session.js +170 -0
- package/dist/wallet/state/client-password/setup.d.ts +8 -0
- package/dist/wallet/state/client-password/setup.js +49 -0
- package/dist/wallet/state/client-password/types.d.ts +82 -0
- package/dist/wallet/state/client-password/types.js +5 -0
- package/dist/wallet/state/client-password.d.ts +7 -38
- package/dist/wallet/state/client-password.js +52 -937
- package/dist/wallet/tx/anchor.js +123 -216
- package/dist/wallet/tx/cog.js +294 -489
- package/dist/wallet/tx/common.d.ts +2 -0
- package/dist/wallet/tx/common.js +2 -0
- package/dist/wallet/tx/domain-admin.js +111 -220
- package/dist/wallet/tx/domain-market.js +401 -681
- package/dist/wallet/tx/executor.d.ts +176 -0
- package/dist/wallet/tx/executor.js +302 -0
- package/dist/wallet/tx/field.js +109 -215
- package/dist/wallet/tx/register.js +158 -269
- package/dist/wallet/tx/reputation.js +120 -227
- package/package.json +1 -1
- package/dist/wallet/mining/worker-main.js +0 -17
- package/dist/wallet/state/client-password-agent.d.ts +0 -1
- package/dist/wallet/state/client-password-agent.js +0 -211
- /package/dist/{wallet/mining/worker-main.d.ts → bitcoind/managed-runtime/types.js} +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { deserializeIndexerState, loadBundledGenesisParameters } from "@cogcoin/indexer";
|
|
2
|
+
import { attachOrStartIndexerDaemon, INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED, probeIndexerDaemon, readObservedIndexerDaemonStatus, readSnapshotWithRetry, } from "../../bitcoind/indexer-daemon.js";
|
|
3
|
+
import { deriveManagedBitcoindWalletStatus, resolveManagedBitcoindProbeDecision, } from "../../bitcoind/managed-runtime/bitcoind-policy.js";
|
|
4
|
+
import { deriveManagedIndexerWalletStatus, resolveIndexerDaemonProbeDecision, } from "../../bitcoind/managed-runtime/indexer-policy.js";
|
|
5
|
+
import { createRpcClient } from "../../bitcoind/node.js";
|
|
6
|
+
import { resolveCogcoinProcessingStartHeight } from "../../bitcoind/processing-start-height.js";
|
|
7
|
+
import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, } from "../../bitcoind/service.js";
|
|
8
|
+
import { verifyManagedCoreWalletReplica } from "../lifecycle.js";
|
|
9
|
+
const TOLERATED_NODE_HEADER_LEAD_BLOCKS = 2;
|
|
10
|
+
const TOLERATED_NODE_HEADER_LEAD_MESSAGE = "Bitcoin headers can briefly lead validated blocks; a short 1-2 block lead is normal and is being tolerated.";
|
|
11
|
+
const NODE_CATCHING_UP_MESSAGE = "Bitcoin Core is still catching up to headers.";
|
|
12
|
+
const defaultManagedWalletReadServiceDeps = {
|
|
13
|
+
loadBundledGenesisParameters,
|
|
14
|
+
probeManagedBitcoindService,
|
|
15
|
+
attachOrStartManagedBitcoindService,
|
|
16
|
+
createRpcClient,
|
|
17
|
+
verifyManagedCoreWalletReplica,
|
|
18
|
+
probeIndexerDaemon,
|
|
19
|
+
attachOrStartIndexerDaemon,
|
|
20
|
+
readSnapshotWithRetry,
|
|
21
|
+
readObservedIndexerDaemonStatus,
|
|
22
|
+
};
|
|
23
|
+
function deriveNodeHealth(status, bitcoindHealth) {
|
|
24
|
+
if (bitcoindHealth !== "ready" || status === null || !status.ready) {
|
|
25
|
+
return {
|
|
26
|
+
health: "catching-up",
|
|
27
|
+
message: NODE_CATCHING_UP_MESSAGE,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const headerLead = status.nodeBestHeight !== null && status.nodeHeaderHeight !== null
|
|
31
|
+
? status.nodeHeaderHeight - status.nodeBestHeight
|
|
32
|
+
: null;
|
|
33
|
+
if (headerLead !== null && headerLead > 0) {
|
|
34
|
+
if (headerLead <= TOLERATED_NODE_HEADER_LEAD_BLOCKS) {
|
|
35
|
+
return {
|
|
36
|
+
health: "synced",
|
|
37
|
+
message: TOLERATED_NODE_HEADER_LEAD_MESSAGE,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
health: "catching-up",
|
|
42
|
+
message: NODE_CATCHING_UP_MESSAGE,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
health: "synced",
|
|
47
|
+
message: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function deriveNodeHealthForTesting(status, bitcoindHealth) {
|
|
51
|
+
return deriveNodeHealth(status, bitcoindHealth);
|
|
52
|
+
}
|
|
53
|
+
async function attachNodeStatus(options, dependencies) {
|
|
54
|
+
try {
|
|
55
|
+
const probe = await dependencies.probeManagedBitcoindService({
|
|
56
|
+
dataDir: options.dataDir,
|
|
57
|
+
chain: "main",
|
|
58
|
+
startHeight: 0,
|
|
59
|
+
walletRootId: options.walletRootId,
|
|
60
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
61
|
+
});
|
|
62
|
+
const decision = resolveManagedBitcoindProbeDecision(probe);
|
|
63
|
+
if (decision.action === "reject") {
|
|
64
|
+
return {
|
|
65
|
+
handle: null,
|
|
66
|
+
rpc: null,
|
|
67
|
+
status: null,
|
|
68
|
+
observedStatus: probe.status,
|
|
69
|
+
error: decision.error,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const genesis = await dependencies.loadBundledGenesisParameters();
|
|
73
|
+
const handle = await dependencies.attachOrStartManagedBitcoindService({
|
|
74
|
+
dataDir: options.dataDir,
|
|
75
|
+
chain: "main",
|
|
76
|
+
startHeight: resolveCogcoinProcessingStartHeight(genesis),
|
|
77
|
+
walletRootId: options.walletRootId,
|
|
78
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
79
|
+
});
|
|
80
|
+
const rpc = dependencies.createRpcClient(handle.rpc);
|
|
81
|
+
const [chainInfo, serviceStatus] = await Promise.all([
|
|
82
|
+
rpc.getBlockchainInfo(),
|
|
83
|
+
handle.refreshServiceStatus?.(),
|
|
84
|
+
]);
|
|
85
|
+
const status = {
|
|
86
|
+
ready: true,
|
|
87
|
+
chain: chainInfo.chain,
|
|
88
|
+
pid: handle.pid,
|
|
89
|
+
walletRootId: handle.walletRootId ?? null,
|
|
90
|
+
nodeBestHeight: chainInfo.blocks,
|
|
91
|
+
nodeBestHashHex: chainInfo.bestblockhash,
|
|
92
|
+
nodeHeaderHeight: chainInfo.headers,
|
|
93
|
+
serviceUpdatedAtUnixMs: serviceStatus?.updatedAtUnixMs ?? null,
|
|
94
|
+
serviceStatus: serviceStatus ?? null,
|
|
95
|
+
walletReplica: serviceStatus?.walletReplica ?? null,
|
|
96
|
+
walletReplicaMessage: serviceStatus?.walletReplica?.message ?? null,
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
handle,
|
|
100
|
+
rpc,
|
|
101
|
+
status,
|
|
102
|
+
observedStatus: serviceStatus ?? null,
|
|
103
|
+
error: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
handle: null,
|
|
109
|
+
rpc: null,
|
|
110
|
+
status: null,
|
|
111
|
+
observedStatus: null,
|
|
112
|
+
error: error instanceof Error ? error.message : String(error),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function openManagedWalletReadServiceBundle(options, dependencies = defaultManagedWalletReadServiceDeps) {
|
|
117
|
+
const node = await attachNodeStatus({
|
|
118
|
+
dataDir: options.dataDir,
|
|
119
|
+
walletRootId: options.walletRootId,
|
|
120
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
121
|
+
}, dependencies);
|
|
122
|
+
if (options.localState.state !== null && node.status !== null) {
|
|
123
|
+
const verifiedReplica = await dependencies.verifyManagedCoreWalletReplica(options.localState.state, options.dataDir, {
|
|
124
|
+
nodeHandle: node.handle ?? undefined,
|
|
125
|
+
});
|
|
126
|
+
node.status = {
|
|
127
|
+
...node.status,
|
|
128
|
+
walletReplica: verifiedReplica,
|
|
129
|
+
walletReplicaMessage: verifiedReplica.message ?? null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const bitcoind = deriveManagedBitcoindWalletStatus({
|
|
133
|
+
status: node.observedStatus,
|
|
134
|
+
nodeStatus: node.status,
|
|
135
|
+
startupError: node.error,
|
|
136
|
+
});
|
|
137
|
+
const nodeDerived = deriveNodeHealth(node.status, bitcoind.health);
|
|
138
|
+
let daemonClient = null;
|
|
139
|
+
let daemonStatus = null;
|
|
140
|
+
let observedDaemonStatus = null;
|
|
141
|
+
let snapshot = null;
|
|
142
|
+
let indexerSource = "none";
|
|
143
|
+
let daemonError = null;
|
|
144
|
+
try {
|
|
145
|
+
const probe = await dependencies.probeIndexerDaemon({
|
|
146
|
+
dataDir: options.dataDir,
|
|
147
|
+
walletRootId: options.walletRootId,
|
|
148
|
+
});
|
|
149
|
+
const probeDecision = resolveIndexerDaemonProbeDecision({
|
|
150
|
+
probe,
|
|
151
|
+
expectedBinaryVersion: options.expectedIndexerBinaryVersion,
|
|
152
|
+
});
|
|
153
|
+
if (probeDecision.action !== "reject") {
|
|
154
|
+
await probe.client?.close().catch(() => undefined);
|
|
155
|
+
daemonClient = await dependencies.attachOrStartIndexerDaemon({
|
|
156
|
+
dataDir: options.dataDir,
|
|
157
|
+
databasePath: options.databasePath,
|
|
158
|
+
walletRootId: options.walletRootId,
|
|
159
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
160
|
+
ensureBackgroundFollow: true,
|
|
161
|
+
expectedBinaryVersion: options.expectedIndexerBinaryVersion,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
observedDaemonStatus = probe.status;
|
|
166
|
+
indexerSource = probe.status === null ? "none" : "probe";
|
|
167
|
+
daemonError = probeDecision.error;
|
|
168
|
+
}
|
|
169
|
+
if (daemonClient !== null) {
|
|
170
|
+
const lease = await dependencies.readSnapshotWithRetry(daemonClient, options.walletRootId);
|
|
171
|
+
daemonStatus = lease.status;
|
|
172
|
+
observedDaemonStatus = lease.status;
|
|
173
|
+
snapshot = {
|
|
174
|
+
tip: lease.payload.tip,
|
|
175
|
+
state: deserializeIndexerState(Buffer.from(lease.payload.stateBase64, "base64")),
|
|
176
|
+
source: "lease",
|
|
177
|
+
daemonInstanceId: lease.payload.daemonInstanceId,
|
|
178
|
+
snapshotSeq: lease.payload.snapshotSeq,
|
|
179
|
+
openedAtUnixMs: lease.payload.openedAtUnixMs,
|
|
180
|
+
};
|
|
181
|
+
indexerSource = "lease";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
daemonError = error instanceof Error ? error.message : String(error);
|
|
186
|
+
if (daemonError === INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED) {
|
|
187
|
+
await daemonClient?.close().catch(() => undefined);
|
|
188
|
+
await node.handle?.stop().catch(() => undefined);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
if (observedDaemonStatus === null) {
|
|
192
|
+
observedDaemonStatus = await dependencies.readObservedIndexerDaemonStatus({
|
|
193
|
+
dataDir: options.dataDir,
|
|
194
|
+
walletRootId: options.walletRootId,
|
|
195
|
+
}).catch(() => null);
|
|
196
|
+
if (observedDaemonStatus !== null) {
|
|
197
|
+
indexerSource = "status-file";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const indexer = deriveManagedIndexerWalletStatus({
|
|
202
|
+
daemonStatus,
|
|
203
|
+
observedStatus: observedDaemonStatus,
|
|
204
|
+
snapshot,
|
|
205
|
+
source: indexerSource,
|
|
206
|
+
now: options.now,
|
|
207
|
+
startupError: daemonError,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
node,
|
|
211
|
+
bitcoind,
|
|
212
|
+
nodeHealth: nodeDerived.health,
|
|
213
|
+
nodeMessage: nodeDerived.message,
|
|
214
|
+
daemonClient,
|
|
215
|
+
indexer,
|
|
216
|
+
snapshot,
|
|
217
|
+
async close() {
|
|
218
|
+
await daemonClient?.close().catch(() => undefined);
|
|
219
|
+
await node.handle?.stop().catch(() => undefined);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -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>;
|