@wlfi-agent/cli 1.4.17 → 1.4.19
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/Cargo.lock +5 -0
- package/README.md +61 -28
- package/crates/vault-cli-admin/src/io_utils.rs +149 -1
- package/crates/vault-cli-admin/src/main.rs +639 -16
- package/crates/vault-cli-admin/src/shared_config.rs +18 -18
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
- package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
- package/crates/vault-cli-admin/src/tui.rs +1205 -120
- package/crates/vault-cli-agent/Cargo.toml +1 -0
- package/crates/vault-cli-agent/src/io_utils.rs +163 -2
- package/crates/vault-cli-agent/src/main.rs +648 -32
- package/crates/vault-cli-daemon/Cargo.toml +4 -0
- package/crates/vault-cli-daemon/src/main.rs +617 -67
- package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
- package/crates/vault-daemon/src/persistence.rs +637 -100
- package/crates/vault-daemon/src/tests.rs +1013 -3
- package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
- package/crates/vault-domain/src/nonce.rs +4 -0
- package/crates/vault-domain/src/tests.rs +616 -0
- package/crates/vault-policy/src/engine.rs +55 -32
- package/crates/vault-policy/src/tests.rs +195 -0
- package/crates/vault-sdk-agent/src/lib.rs +415 -22
- package/crates/vault-signer/Cargo.toml +3 -0
- package/crates/vault-signer/src/lib.rs +266 -40
- package/crates/vault-transport-unix/src/lib.rs +653 -5
- package/crates/vault-transport-xpc/src/tests.rs +531 -3
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
- package/dist/cli.cjs +756 -194
- package/dist/cli.cjs.map +1 -1
- package/package.json +5 -2
- package/packages/cache/.turbo/turbo-build.log +20 -20
- package/packages/cache/coverage/clover.xml +529 -394
- package/packages/cache/coverage/coverage-final.json +2 -2
- package/packages/cache/coverage/index.html +21 -21
- package/packages/cache/coverage/src/client/index.html +1 -1
- package/packages/cache/coverage/src/client/index.ts.html +1 -1
- package/packages/cache/coverage/src/errors/index.html +1 -1
- package/packages/cache/coverage/src/errors/index.ts.html +12 -12
- package/packages/cache/coverage/src/index.html +1 -1
- package/packages/cache/coverage/src/index.ts.html +1 -1
- package/packages/cache/coverage/src/service/index.html +21 -21
- package/packages/cache/coverage/src/service/index.ts.html +769 -313
- package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
- package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
- package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
- package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
- package/packages/cache/dist/index.cjs +2 -2
- package/packages/cache/dist/index.js +1 -1
- package/packages/cache/dist/service/index.cjs +2 -2
- package/packages/cache/dist/service/index.js +1 -1
- package/packages/cache/node_modules/.bin/tsc +2 -2
- package/packages/cache/node_modules/.bin/tsserver +2 -2
- package/packages/cache/node_modules/.bin/tsup +2 -2
- package/packages/cache/node_modules/.bin/tsup-node +2 -2
- package/packages/cache/node_modules/.bin/vitest +4 -4
- package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/cache/src/service/index.test.ts +165 -19
- package/packages/cache/src/service/index.ts +38 -1
- package/packages/config/.turbo/turbo-build.log +4 -4
- package/packages/config/dist/index.cjs +0 -17
- package/packages/config/dist/index.cjs.map +1 -1
- package/packages/config/src/index.ts +0 -17
- package/packages/rpc/.turbo/turbo-build.log +11 -11
- package/packages/rpc/dist/index.cjs +0 -17
- package/packages/rpc/dist/index.cjs.map +1 -1
- package/packages/rpc/src/index.js +1 -0
- package/packages/ui/node_modules/.bin/tsc +2 -2
- package/packages/ui/node_modules/.bin/tsserver +2 -2
- package/packages/ui/node_modules/.bin/tsup +2 -2
- package/packages/ui/node_modules/.bin/tsup-node +2 -2
- package/scripts/install-cli-launcher.mjs +37 -0
- package/scripts/install-rust-binaries.mjs +47 -0
- package/scripts/run-tests-isolated.mjs +210 -0
- package/src/cli.ts +310 -50
- package/src/lib/admin-reset.ts +101 -33
- package/src/lib/admin-setup.ts +285 -55
- package/src/lib/agent-auth-migrate.ts +5 -1
- package/src/lib/asset-broadcast.ts +15 -4
- package/src/lib/config-amounts.ts +6 -4
- package/src/lib/hidden-tty-prompt.js +1 -0
- package/src/lib/hidden-tty-prompt.ts +105 -0
- package/src/lib/keychain.ts +1 -0
- package/src/lib/local-admin-access.ts +4 -29
- package/src/lib/rust.ts +129 -33
- package/src/lib/signed-tx.ts +1 -0
- package/src/lib/sudo.ts +15 -5
- package/src/lib/wallet-profile.ts +3 -0
- package/src/lib/wallet-setup.ts +52 -0
- package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
- package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
package/src/lib/admin-setup.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
resolveWalletSetupCleanupAction,
|
|
42
42
|
type WalletSetupPlan,
|
|
43
43
|
} from './wallet-setup.js';
|
|
44
|
+
import { promptHiddenTty } from './hidden-tty-prompt.js';
|
|
44
45
|
|
|
45
46
|
const DEFAULT_LAUNCH_DAEMON_LABEL = 'com.wlfi.agent.daemon';
|
|
46
47
|
const DEFAULT_SIGNER_BACKEND = 'software';
|
|
@@ -69,6 +70,7 @@ interface AdminSetupOptions {
|
|
|
69
70
|
nonInteractive?: boolean;
|
|
70
71
|
plan?: boolean;
|
|
71
72
|
yes?: boolean;
|
|
73
|
+
reuseExistingWallet?: boolean;
|
|
72
74
|
printAgentAuthToken?: boolean;
|
|
73
75
|
daemonSocket?: string;
|
|
74
76
|
perTxMaxWei?: string;
|
|
@@ -170,6 +172,12 @@ export interface ExistingWalletSetupTarget {
|
|
|
170
172
|
hasLegacyAgentAuthToken: boolean;
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
interface ReusableWalletSetupTarget {
|
|
176
|
+
address?: string;
|
|
177
|
+
existingVaultKeyId: string;
|
|
178
|
+
existingVaultPublicKey: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
173
181
|
interface ConfirmAdminSetupOverwriteDeps {
|
|
174
182
|
prompt?: (query: string) => Promise<string>;
|
|
175
183
|
stderr?: Pick<NodeJS.WriteStream, 'write'>;
|
|
@@ -203,34 +211,13 @@ async function readTrimmedStdin(label: string): Promise<string> {
|
|
|
203
211
|
return validateSecret(raw.replace(/[\r\n]+$/u, ''), label);
|
|
204
212
|
}
|
|
205
213
|
|
|
206
|
-
async function promptHidden(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
output: process.stdout,
|
|
214
|
-
terminal: true,
|
|
215
|
-
}) as readline.Interface & { stdoutMuted?: boolean; _writeToOutput?: (value: string) => void };
|
|
216
|
-
|
|
217
|
-
rl.stdoutMuted = true;
|
|
218
|
-
rl._writeToOutput = (value: string) => {
|
|
219
|
-
if (value.includes(query)) {
|
|
220
|
-
(rl as unknown as { output: NodeJS.WritableStream }).output.write(value);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (!rl.stdoutMuted) {
|
|
224
|
-
(rl as unknown as { output: NodeJS.WritableStream }).output.write(value);
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const answer = await new Promise<string>((resolve) => {
|
|
229
|
-
rl.question(query, resolve);
|
|
230
|
-
});
|
|
231
|
-
rl.close();
|
|
232
|
-
process.stdout.write('\n');
|
|
233
|
-
return validateSecret(answer, 'vault password');
|
|
214
|
+
async function promptHidden(
|
|
215
|
+
query: string,
|
|
216
|
+
label = 'vault password',
|
|
217
|
+
nonInteractiveError = 'vault password is required; use --vault-password-stdin or a local TTY prompt',
|
|
218
|
+
): Promise<string> {
|
|
219
|
+
const answer = await promptHiddenTty(query, nonInteractiveError);
|
|
220
|
+
return validateSecret(answer, label);
|
|
234
221
|
}
|
|
235
222
|
|
|
236
223
|
async function promptVisible(query: string, nonInteractiveError: string): Promise<string> {
|
|
@@ -260,6 +247,7 @@ function deriveWalletAddress(vaultPublicKey: string | undefined): string | undef
|
|
|
260
247
|
return publicKeyToAddress(
|
|
261
248
|
(normalized.startsWith('0x') ? normalized : `0x${normalized}`) as Hex,
|
|
262
249
|
);
|
|
250
|
+
/* c8 ignore next 2 -- viem currently normalizes malformed public-key strings instead of throwing; catch is defensive */
|
|
263
251
|
} catch {
|
|
264
252
|
return undefined;
|
|
265
253
|
}
|
|
@@ -284,8 +272,33 @@ export function resolveExistingWalletSetupTarget(
|
|
|
284
272
|
};
|
|
285
273
|
}
|
|
286
274
|
|
|
275
|
+
function resolveReusableWalletSetupTarget(config: WlfiConfig): ReusableWalletSetupTarget {
|
|
276
|
+
let walletProfile: ReturnType<typeof resolveWalletProfile>;
|
|
277
|
+
try {
|
|
278
|
+
walletProfile = resolveWalletProfile(config);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`--reuse-existing-wallet requires a local wallet to reuse; ${renderError(error)}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const existingVaultKeyId = walletProfile.vaultKeyId?.trim();
|
|
286
|
+
const existingVaultPublicKey = walletProfile.vaultPublicKey?.trim();
|
|
287
|
+
if (!existingVaultKeyId || !existingVaultPublicKey) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
'--reuse-existing-wallet requires wallet.vaultKeyId and wallet.vaultPublicKey in the local wallet profile',
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
address: walletProfile.address?.trim() || deriveWalletAddress(existingVaultPublicKey),
|
|
295
|
+
existingVaultKeyId,
|
|
296
|
+
existingVaultPublicKey,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
287
300
|
export async function confirmAdminSetupOverwrite(
|
|
288
|
-
options: Pick<AdminSetupOptions, 'yes' | 'nonInteractive'>,
|
|
301
|
+
options: Pick<AdminSetupOptions, 'yes' | 'nonInteractive' | 'reuseExistingWallet'>,
|
|
289
302
|
config: WlfiConfig,
|
|
290
303
|
deps: ConfirmAdminSetupOverwriteDeps = {},
|
|
291
304
|
): Promise<void> {
|
|
@@ -295,13 +308,17 @@ export async function confirmAdminSetupOverwrite(
|
|
|
295
308
|
}
|
|
296
309
|
if (options.nonInteractive) {
|
|
297
310
|
throw new Error(
|
|
298
|
-
|
|
311
|
+
options.reuseExistingWallet
|
|
312
|
+
? '`wlfi-agent admin setup --reuse-existing-wallet` would refresh the existing local wallet metadata and agent credentials; rerun with --yes in non-interactive mode'
|
|
313
|
+
: '`wlfi-agent admin setup` would overwrite the existing wallet; rerun with --yes in non-interactive mode',
|
|
299
314
|
);
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
const stderr = deps.stderr ?? process.stderr;
|
|
303
318
|
stderr.write(
|
|
304
|
-
|
|
319
|
+
options.reuseExistingWallet
|
|
320
|
+
? 'warning: admin setup will reuse the current vault and refresh the local wallet metadata and agent credentials.\n'
|
|
321
|
+
: 'warning: admin setup will overwrite the current local wallet metadata and agent credentials.\n',
|
|
305
322
|
);
|
|
306
323
|
if (existing.address) {
|
|
307
324
|
stderr.write(`current address: ${existing.address}\n`);
|
|
@@ -313,15 +330,21 @@ export async function confirmAdminSetupOverwrite(
|
|
|
313
330
|
stderr.write('legacy agent auth token is still present in config.json\n');
|
|
314
331
|
}
|
|
315
332
|
|
|
333
|
+
const confirmationToken = options.reuseExistingWallet ? 'REUSE' : 'OVERWRITE';
|
|
334
|
+
const confirmationPrompt = options.reuseExistingWallet
|
|
335
|
+
? 'Type REUSE to reattach the current local vault: '
|
|
336
|
+
: 'Type OVERWRITE to replace the current local wallet: ';
|
|
316
337
|
const confirmation = await (
|
|
317
338
|
deps.prompt ??
|
|
318
339
|
((query: string) =>
|
|
319
340
|
promptVisible(
|
|
320
341
|
query,
|
|
321
|
-
|
|
342
|
+
options.reuseExistingWallet
|
|
343
|
+
? '`wlfi-agent admin setup --reuse-existing-wallet` requires --yes in non-interactive environments when reusing an existing wallet'
|
|
344
|
+
: '`wlfi-agent admin setup` requires --yes in non-interactive environments when overwriting an existing wallet',
|
|
322
345
|
))
|
|
323
|
-
)(
|
|
324
|
-
if (confirmation !==
|
|
346
|
+
)(confirmationPrompt);
|
|
347
|
+
if (confirmation !== confirmationToken) {
|
|
325
348
|
throw new Error('admin setup aborted');
|
|
326
349
|
}
|
|
327
350
|
}
|
|
@@ -357,7 +380,9 @@ export async function resolveAdminSetupVaultPassword(
|
|
|
357
380
|
'vault password is required in non-interactive mode; use --vault-password-stdin',
|
|
358
381
|
);
|
|
359
382
|
}
|
|
360
|
-
return promptForVaultPassword(
|
|
383
|
+
return promptForVaultPassword(
|
|
384
|
+
'Vault password (input hidden; this unlocks the wallet, not sudo): ',
|
|
385
|
+
);
|
|
361
386
|
}
|
|
362
387
|
|
|
363
388
|
function resolveDaemonSocket(optionValue: string | undefined): string {
|
|
@@ -368,6 +393,35 @@ function resolveStateFile(): string {
|
|
|
368
393
|
return path.resolve(DEFAULT_MANAGED_STATE_FILE);
|
|
369
394
|
}
|
|
370
395
|
|
|
396
|
+
function resolveDefaultActiveChainForFreshSetup(
|
|
397
|
+
config: WlfiConfig,
|
|
398
|
+
): { chainId: number; chainName: string; rpcUrl?: string } | null {
|
|
399
|
+
try {
|
|
400
|
+
const profile = resolveCliNetworkProfile('bsc', config);
|
|
401
|
+
return {
|
|
402
|
+
chainId: profile.chainId,
|
|
403
|
+
chainName: profile.key?.trim() || profile.name.trim().toLowerCase() || 'bsc',
|
|
404
|
+
...(profile.rpcUrl?.trim() ? { rpcUrl: profile.rpcUrl.trim() } : {}),
|
|
405
|
+
};
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function shouldSeedDefaultActiveChain(
|
|
412
|
+
options: Pick<AdminSetupOptions, 'network' | 'rpcUrl' | 'chainName'>,
|
|
413
|
+
config: WlfiConfig,
|
|
414
|
+
): boolean {
|
|
415
|
+
return (
|
|
416
|
+
!options.network &&
|
|
417
|
+
!options.rpcUrl &&
|
|
418
|
+
!options.chainName &&
|
|
419
|
+
config.chainId === undefined &&
|
|
420
|
+
!config.chainName?.trim() &&
|
|
421
|
+
!config.rpcUrl?.trim()
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
371
425
|
export function createAdminSetupPlan(
|
|
372
426
|
options: AdminSetupOptions,
|
|
373
427
|
deps: CreateAdminSetupPlanDeps = {},
|
|
@@ -398,6 +452,9 @@ export function createAdminSetupPlan(
|
|
|
398
452
|
}
|
|
399
453
|
|
|
400
454
|
const existingWallet = resolveExistingWalletSetupTarget(config);
|
|
455
|
+
const reusableWallet = options.reuseExistingWallet
|
|
456
|
+
? resolveReusableWalletSetupTarget(config)
|
|
457
|
+
: null;
|
|
401
458
|
|
|
402
459
|
return {
|
|
403
460
|
command: 'setup',
|
|
@@ -442,6 +499,8 @@ export function createAdminSetupPlan(
|
|
|
442
499
|
recipient: options.recipient,
|
|
443
500
|
attachPolicyId: options.attachPolicyId,
|
|
444
501
|
attachBootstrapPolicies: options.attachBootstrapPolicies,
|
|
502
|
+
existingVaultKeyId: reusableWallet?.existingVaultKeyId,
|
|
503
|
+
existingVaultPublicKey: reusableWallet?.existingVaultPublicKey,
|
|
445
504
|
bootstrapOutputPath: options.bootstrapOutput,
|
|
446
505
|
deleteBootstrapOutput: options.deleteBootstrapOutput,
|
|
447
506
|
},
|
|
@@ -851,7 +910,9 @@ async function daemonAcceptsVaultPassword(
|
|
|
851
910
|
const sudoSession = createSudoSession({
|
|
852
911
|
promptPassword: async () =>
|
|
853
912
|
await promptHidden(
|
|
854
|
-
'
|
|
913
|
+
'macOS admin password for sudo (input hidden; required to install or recover the root daemon): ',
|
|
914
|
+
'macOS admin password for sudo',
|
|
915
|
+
'macOS admin password for sudo is required; rerun on a local TTY',
|
|
855
916
|
),
|
|
856
917
|
});
|
|
857
918
|
|
|
@@ -873,6 +934,158 @@ async function managedStateFileExists(stateFile: string): Promise<boolean> {
|
|
|
873
934
|
);
|
|
874
935
|
}
|
|
875
936
|
|
|
937
|
+
async function inspectManagedState(
|
|
938
|
+
stateFile: string,
|
|
939
|
+
showProgress: boolean,
|
|
940
|
+
message = 'Inspecting managed daemon state',
|
|
941
|
+
): Promise<boolean> {
|
|
942
|
+
await sudoSession.prime();
|
|
943
|
+
const stateProbeProgress = createProgress(message, showProgress);
|
|
944
|
+
let stateExists: boolean;
|
|
945
|
+
try {
|
|
946
|
+
stateExists = await managedStateFileExists(stateFile);
|
|
947
|
+
stateProbeProgress.succeed(
|
|
948
|
+
stateExists ? 'Managed daemon state already exists' : 'No managed daemon state found',
|
|
949
|
+
);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
stateProbeProgress.fail();
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
return stateExists;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function createManagedStatePasswordMismatchError(stateFile: string): Error {
|
|
958
|
+
return new Error(
|
|
959
|
+
`managed daemon state already exists at ${stateFile} and is encrypted with a different vault password. Re-run setup with the original vault password, or remove/reset the managed daemon state before initializing a fresh wallet.`,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function isManagedStatePasswordMismatch(output: string): boolean {
|
|
964
|
+
return /failed to decrypt state|wrong password or tampered file|authentication failed/iu.test(
|
|
965
|
+
output,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async function managedStateAcceptsRequestedVaultPassword(
|
|
970
|
+
config: WlfiConfig,
|
|
971
|
+
daemonSocket: string,
|
|
972
|
+
stateFile: string,
|
|
973
|
+
vaultPassword: string,
|
|
974
|
+
): Promise<boolean> {
|
|
975
|
+
const installPreconditions = assertManagedDaemonInstallPreconditions(
|
|
976
|
+
config,
|
|
977
|
+
daemonSocket,
|
|
978
|
+
stateFile,
|
|
979
|
+
);
|
|
980
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'wlfi-managed-state-probe-'));
|
|
981
|
+
const tempSocket = path.join(tempRoot, 'daemon.sock');
|
|
982
|
+
const allowedUid = String(process.getuid?.() ?? process.geteuid?.() ?? 0);
|
|
983
|
+
const probeScript = [
|
|
984
|
+
'set -euo pipefail',
|
|
985
|
+
'vault_password="$(cat)"',
|
|
986
|
+
'daemon_bin="$1"',
|
|
987
|
+
'state_file="$2"',
|
|
988
|
+
'daemon_socket="$3"',
|
|
989
|
+
'admin_uid="$4"',
|
|
990
|
+
'agent_uid="$5"',
|
|
991
|
+
'signer_backend="$6"',
|
|
992
|
+
'"$daemon_bin" \\',
|
|
993
|
+
' --non-interactive \\',
|
|
994
|
+
' --vault-password-stdin \\',
|
|
995
|
+
' --state-file "$state_file" \\',
|
|
996
|
+
' --daemon-socket "$daemon_socket" \\',
|
|
997
|
+
' --signer-backend "$signer_backend" \\',
|
|
998
|
+
' --allow-admin-euid "$admin_uid" \\',
|
|
999
|
+
' --allow-agent-euid "$agent_uid" <<<"$vault_password" &',
|
|
1000
|
+
'child="$!"',
|
|
1001
|
+
'for _ in $(seq 1 20); do',
|
|
1002
|
+
' if ! kill -0 "$child" 2>/dev/null; then',
|
|
1003
|
+
' wait "$child"',
|
|
1004
|
+
' exit $?',
|
|
1005
|
+
' fi',
|
|
1006
|
+
' sleep 0.25',
|
|
1007
|
+
'done',
|
|
1008
|
+
'kill "$child" >/dev/null 2>&1 || true',
|
|
1009
|
+
'wait "$child" >/dev/null 2>&1 || true',
|
|
1010
|
+
'exit 0',
|
|
1011
|
+
].join('\n');
|
|
1012
|
+
|
|
1013
|
+
try {
|
|
1014
|
+
const result = await sudoSession.run(
|
|
1015
|
+
[
|
|
1016
|
+
'/bin/bash',
|
|
1017
|
+
'-lc',
|
|
1018
|
+
probeScript,
|
|
1019
|
+
'--',
|
|
1020
|
+
installPreconditions.daemonBin,
|
|
1021
|
+
stateFile,
|
|
1022
|
+
tempSocket,
|
|
1023
|
+
allowedUid,
|
|
1024
|
+
allowedUid,
|
|
1025
|
+
DEFAULT_SIGNER_BACKEND,
|
|
1026
|
+
],
|
|
1027
|
+
{
|
|
1028
|
+
stdin: `${vaultPassword}\n`,
|
|
1029
|
+
},
|
|
1030
|
+
);
|
|
1031
|
+
if (result.code === 0) {
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const combinedOutput = `${result.stderr}\n${result.stdout}`.trim();
|
|
1036
|
+
if (isManagedStatePasswordMismatch(combinedOutput)) {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
combinedOutput ||
|
|
1041
|
+
`failed to validate the managed daemon state with the requested vault password (exit code ${result.code})`,
|
|
1042
|
+
);
|
|
1043
|
+
} finally {
|
|
1044
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
async function assertManagedStateMatchesRequestedVaultPasswordBeforeInstall(
|
|
1049
|
+
config: WlfiConfig,
|
|
1050
|
+
daemonSocket: string,
|
|
1051
|
+
stateFile: string,
|
|
1052
|
+
vaultPassword: string,
|
|
1053
|
+
showProgress: boolean,
|
|
1054
|
+
): Promise<void> {
|
|
1055
|
+
const stateExists = await inspectManagedState(
|
|
1056
|
+
stateFile,
|
|
1057
|
+
showProgress,
|
|
1058
|
+
'Inspecting managed daemon state before install',
|
|
1059
|
+
);
|
|
1060
|
+
if (!stateExists) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const verifyProgress = createProgress(
|
|
1065
|
+
'Verifying the requested vault password against the managed daemon state',
|
|
1066
|
+
showProgress,
|
|
1067
|
+
);
|
|
1068
|
+
let accepted: boolean;
|
|
1069
|
+
try {
|
|
1070
|
+
accepted = await managedStateAcceptsRequestedVaultPassword(
|
|
1071
|
+
config,
|
|
1072
|
+
daemonSocket,
|
|
1073
|
+
stateFile,
|
|
1074
|
+
vaultPassword,
|
|
1075
|
+
);
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
verifyProgress.fail();
|
|
1078
|
+
throw error;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (!accepted) {
|
|
1082
|
+
verifyProgress.fail('Existing daemon password does not unlock the stored daemon state');
|
|
1083
|
+
throw createManagedStatePasswordMismatchError(stateFile);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
verifyProgress.succeed('Requested vault password unlocks the existing managed daemon state');
|
|
1087
|
+
}
|
|
1088
|
+
|
|
876
1089
|
async function waitForTrustedDaemonSocket(targetPath: string, timeoutMs = 15_000): Promise<void> {
|
|
877
1090
|
async function canConnect(socketPath: string): Promise<boolean> {
|
|
878
1091
|
return new Promise<boolean>((resolve) => {
|
|
@@ -1109,6 +1322,12 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
|
|
|
1109
1322
|
const config = readConfig();
|
|
1110
1323
|
const daemonSocket = resolveDaemonSocket(options.daemonSocket);
|
|
1111
1324
|
const stateFile = resolveStateFile();
|
|
1325
|
+
const defaultActiveChain = shouldSeedDefaultActiveChain(options, config)
|
|
1326
|
+
? resolveDefaultActiveChainForFreshSetup(config)
|
|
1327
|
+
: null;
|
|
1328
|
+
const reusableWallet = options.reuseExistingWallet
|
|
1329
|
+
? resolveReusableWalletSetupTarget(config)
|
|
1330
|
+
: null;
|
|
1112
1331
|
assertManagedDaemonInstallPreconditions(config, daemonSocket, stateFile);
|
|
1113
1332
|
await confirmAdminSetupOverwrite(options, config);
|
|
1114
1333
|
const vaultPassword = await resolveAdminSetupVaultPassword(options);
|
|
@@ -1176,10 +1395,17 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
|
|
|
1176
1395
|
}
|
|
1177
1396
|
if (!options.json && typeof process.geteuid === 'function' && process.geteuid() !== 0) {
|
|
1178
1397
|
process.stderr.write(
|
|
1179
|
-
'
|
|
1398
|
+
'macOS admin password required: setup uses sudo to install or recover the root LaunchDaemon and store the daemon password in System Keychain.\n',
|
|
1180
1399
|
);
|
|
1181
1400
|
}
|
|
1182
1401
|
await sudoSession.prime();
|
|
1402
|
+
await assertManagedStateMatchesRequestedVaultPasswordBeforeInstall(
|
|
1403
|
+
config,
|
|
1404
|
+
daemonSocket,
|
|
1405
|
+
stateFile,
|
|
1406
|
+
vaultPassword,
|
|
1407
|
+
showProgress,
|
|
1408
|
+
);
|
|
1183
1409
|
const installProgress = createProgress('Installing and restarting daemon', showProgress);
|
|
1184
1410
|
try {
|
|
1185
1411
|
daemon = await installLaunchDaemon(config, daemonSocket, stateFile, vaultPassword);
|
|
@@ -1212,29 +1438,15 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
|
|
|
1212
1438
|
if (!daemonAcceptedPassword) {
|
|
1213
1439
|
if (existingDaemonRejectedPassword) {
|
|
1214
1440
|
authProgress.fail('Existing daemon password does not unlock the stored daemon state');
|
|
1215
|
-
throw
|
|
1216
|
-
`managed daemon state already exists at ${stateFile} and is encrypted with a different vault password. Re-run setup with the original vault password, or remove/reset the managed daemon state before initializing a fresh wallet.`,
|
|
1217
|
-
);
|
|
1441
|
+
throw createManagedStatePasswordMismatchError(stateFile);
|
|
1218
1442
|
}
|
|
1219
1443
|
|
|
1220
1444
|
authProgress.info('Daemon rejected the requested vault password; inspecting managed state');
|
|
1221
|
-
const
|
|
1222
|
-
let stateExists: boolean;
|
|
1223
|
-
try {
|
|
1224
|
-
stateExists = await managedStateFileExists(stateFile);
|
|
1225
|
-
stateProbeProgress.succeed(
|
|
1226
|
-
stateExists ? 'Managed daemon state already exists' : 'No managed daemon state found',
|
|
1227
|
-
);
|
|
1228
|
-
} catch (error) {
|
|
1229
|
-
stateProbeProgress.fail();
|
|
1230
|
-
throw error;
|
|
1231
|
-
}
|
|
1445
|
+
const stateExists = await inspectManagedState(stateFile, showProgress);
|
|
1232
1446
|
|
|
1233
1447
|
if (stateExists) {
|
|
1234
1448
|
authProgress.fail('Existing daemon password does not unlock the stored daemon state');
|
|
1235
|
-
throw
|
|
1236
|
-
`managed daemon state already exists at ${stateFile} and is encrypted with a different vault password. Re-run setup with the original vault password, or remove/reset the managed daemon state before initializing a fresh wallet.`,
|
|
1237
|
-
);
|
|
1449
|
+
throw createManagedStatePasswordMismatchError(stateFile);
|
|
1238
1450
|
}
|
|
1239
1451
|
|
|
1240
1452
|
authProgress.fail(
|
|
@@ -1243,7 +1455,7 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
|
|
|
1243
1455
|
|
|
1244
1456
|
if (!options.json && typeof process.geteuid === 'function' && process.geteuid() !== 0) {
|
|
1245
1457
|
process.stderr.write(
|
|
1246
|
-
'
|
|
1458
|
+
'macOS admin password required: setup uses sudo to reinstall the root LaunchDaemon and rotate the managed daemon password.\n',
|
|
1247
1459
|
);
|
|
1248
1460
|
}
|
|
1249
1461
|
await sudoSession.prime();
|
|
@@ -1333,6 +1545,8 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
|
|
|
1333
1545
|
recipient: options.recipient,
|
|
1334
1546
|
attachPolicyId: options.attachPolicyId,
|
|
1335
1547
|
attachBootstrapPolicies: options.attachBootstrapPolicies,
|
|
1548
|
+
existingVaultKeyId: reusableWallet?.existingVaultKeyId,
|
|
1549
|
+
existingVaultPublicKey: reusableWallet?.existingVaultPublicKey,
|
|
1336
1550
|
bootstrapOutputPath: bootstrapOutput.path,
|
|
1337
1551
|
});
|
|
1338
1552
|
|
|
@@ -1406,6 +1620,13 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
|
|
|
1406
1620
|
const persistedConfig = writeConfig({
|
|
1407
1621
|
daemonSocket,
|
|
1408
1622
|
stateFile,
|
|
1623
|
+
...(defaultActiveChain
|
|
1624
|
+
? {
|
|
1625
|
+
chainId: defaultActiveChain.chainId,
|
|
1626
|
+
chainName: defaultActiveChain.chainName,
|
|
1627
|
+
...(defaultActiveChain.rpcUrl ? { rpcUrl: defaultActiveChain.rpcUrl } : {}),
|
|
1628
|
+
}
|
|
1629
|
+
: {}),
|
|
1409
1630
|
});
|
|
1410
1631
|
|
|
1411
1632
|
printCliPayload(
|
|
@@ -1460,6 +1681,8 @@ export function buildAdminSetupBootstrapInvocation(input: {
|
|
|
1460
1681
|
recipient?: string;
|
|
1461
1682
|
attachPolicyId?: string[];
|
|
1462
1683
|
attachBootstrapPolicies?: boolean;
|
|
1684
|
+
existingVaultKeyId?: string;
|
|
1685
|
+
existingVaultPublicKey?: string;
|
|
1463
1686
|
bootstrapOutputPath: string;
|
|
1464
1687
|
}): { args: string[]; stdin: string } {
|
|
1465
1688
|
return {
|
|
@@ -1481,6 +1704,8 @@ export function buildAdminSetupBootstrapInvocation(input: {
|
|
|
1481
1704
|
recipient: input.recipient,
|
|
1482
1705
|
attachPolicyId: input.attachPolicyId,
|
|
1483
1706
|
attachBootstrapPolicies: input.attachBootstrapPolicies,
|
|
1707
|
+
existingVaultKeyId: input.existingVaultKeyId,
|
|
1708
|
+
existingVaultPublicKey: input.existingVaultPublicKey,
|
|
1484
1709
|
bootstrapOutputPath: input.bootstrapOutputPath,
|
|
1485
1710
|
}),
|
|
1486
1711
|
stdin: `${validateSecret(input.vaultPassword, 'vault password')}\n`,
|
|
@@ -1552,6 +1777,11 @@ export async function runAdminSetupCli(argv: string[]): Promise<void> {
|
|
|
1552
1777
|
.option('--vault-password-stdin', 'Read vault password from stdin', false)
|
|
1553
1778
|
.option('--non-interactive', 'Disable password prompts', false)
|
|
1554
1779
|
.option('-y, --yes', 'Skip the overwrite confirmation prompt', false)
|
|
1780
|
+
.option(
|
|
1781
|
+
'--reuse-existing-wallet',
|
|
1782
|
+
'Reuse the current local vault instead of generating a fresh wallet',
|
|
1783
|
+
false,
|
|
1784
|
+
)
|
|
1555
1785
|
.option('--daemon-socket <path>', 'Daemon unix socket path')
|
|
1556
1786
|
.option('--per-tx-max-wei <wei>', 'Per-transaction max spend in wei')
|
|
1557
1787
|
.option('--daily-max-wei <wei>', 'Daily max spend in wei')
|
|
@@ -62,8 +62,12 @@ function resolveConfiguredAgentKeyId(
|
|
|
62
62
|
if (explicitAgentKeyId) {
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
|
+
const renderedError =
|
|
66
|
+
error instanceof Error
|
|
67
|
+
? error.message
|
|
68
|
+
: /* c8 ignore next -- assertValidAgentKeyId throws Error objects */ String(error);
|
|
65
69
|
throw new Error(
|
|
66
|
-
|
|
70
|
+
renderedError +
|
|
67
71
|
'; pass --agent-key-id to migrate the legacy config secret explicitly'
|
|
68
72
|
);
|
|
69
73
|
}
|
|
@@ -94,6 +94,16 @@ export interface WaitForOnchainReceiptDeps {
|
|
|
94
94
|
sleep?: (ms: number) => Promise<void>;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export function resolveEstimatedPriorityFeePerGasWei(fees: BroadcastFeeEstimate): bigint {
|
|
98
|
+
const resolved = fees.maxPriorityFeePerGas ?? fees.gasPrice;
|
|
99
|
+
if (resolved === null || resolved <= 0n) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
'Could not determine maxPriorityFeePerGas; pass --max-priority-fee-per-gas-wei',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
export function encodeErc20TransferData(recipient: Address, amountWei: bigint): Hex {
|
|
98
108
|
return encodeFunctionData({
|
|
99
109
|
abi: erc20Abi,
|
|
@@ -128,14 +138,15 @@ export async function resolveAssetBroadcastPlan(
|
|
|
128
138
|
const fees = await deps.estimateFees(input.rpcUrl);
|
|
129
139
|
const resolvedFees = fees as BroadcastFeeEstimate;
|
|
130
140
|
const maxFeePerGasWei = input.maxFeePerGasWei ?? (resolvedFees.maxFeePerGas ?? resolvedFees.gasPrice);
|
|
131
|
-
const maxPriorityFeePerGasWei =
|
|
132
|
-
input.maxPriorityFeePerGasWei
|
|
133
|
-
?? (resolvedFees.maxPriorityFeePerGas ?? resolvedFees.gasPrice ?? 0n);
|
|
134
141
|
|
|
135
142
|
if (maxFeePerGasWei === null || maxFeePerGasWei <= 0n) {
|
|
136
143
|
throw new Error('Could not determine maxFeePerGas; pass --max-fee-per-gas-wei');
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
const maxPriorityFeePerGasWei =
|
|
147
|
+
input.maxPriorityFeePerGasWei
|
|
148
|
+
?? resolveEstimatedPriorityFeePerGasWei(resolvedFees);
|
|
149
|
+
|
|
139
150
|
return {
|
|
140
151
|
rpcUrl: input.rpcUrl,
|
|
141
152
|
chainId: input.chainId,
|
|
@@ -166,7 +177,7 @@ export async function completeAssetBroadcast(
|
|
|
166
177
|
to: plan.to,
|
|
167
178
|
chainId: plan.chainId,
|
|
168
179
|
nonce: plan.nonce,
|
|
169
|
-
allowHigherNonce:
|
|
180
|
+
allowHigherNonce: false,
|
|
170
181
|
value: plan.valueWei,
|
|
171
182
|
data: plan.dataHex,
|
|
172
183
|
gasLimit: plan.gasLimit,
|
|
@@ -181,9 +181,11 @@ export function rewriteAmountPolicyErrorMessage(
|
|
|
181
181
|
minAmountLiteral: string,
|
|
182
182
|
minAmountWei: string | undefined,
|
|
183
183
|
maxAmountWei: string,
|
|
184
|
-
) =>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
) => {
|
|
185
|
+
const minAmountDisplay =
|
|
186
|
+
/* c8 ignore next -- both None and Some(...) paths are exercised, but c8 misattributes this ternary under --experimental-strip-types */
|
|
187
|
+
minAmountLiteral === 'None' ? 'None' : `Some(${formatAmount(minAmountWei ?? '0')})`;
|
|
188
|
+
return `requires manual approval for requested amount ${formatAmount(requestedAmountWei)} within range ${minAmountDisplay}..=${formatAmount(maxAmountWei)}`;
|
|
189
|
+
},
|
|
188
190
|
);
|
|
189
191
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './hidden-tty-prompt.ts';
|