@solard/cli 0.2.13 → 0.2.14

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.
@@ -0,0 +1,214 @@
1
+ export class VaultOnboardingError extends Error {
2
+ readonly name = "VaultOnboardingError";
3
+ }
4
+
5
+ export type VaultReadyResult = {
6
+ source: "env" | "interactive" | "interactive-created";
7
+ created: boolean;
8
+ remembered: false;
9
+ persisted: false;
10
+ };
11
+
12
+ function interactive(): boolean {
13
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
14
+ }
15
+
16
+ async function promptHidden(message: string): Promise<string> {
17
+ const input = process.stdin;
18
+ if (!interactive() || typeof input.setRawMode !== "function") {
19
+ throw new VaultOnboardingError(
20
+ "This command needs a wallet password, but stdin is not interactive. " +
21
+ "Set SLRD_MASTER_KEY for CI/server automation or run the command in a terminal.",
22
+ );
23
+ }
24
+
25
+ process.stdout.write(message);
26
+ const wasRaw = input.isRaw;
27
+ input.setRawMode(true);
28
+ input.resume();
29
+ input.setEncoding("utf8");
30
+
31
+ return await new Promise<string>((resolve, reject) => {
32
+ let value = "";
33
+ const cleanup = () => {
34
+ input.off("data", onData);
35
+ input.setRawMode(Boolean(wasRaw));
36
+ input.pause();
37
+ process.stdout.write("\n");
38
+ };
39
+ const onData = (chunk: string | Buffer) => {
40
+ for (const ch of String(chunk)) {
41
+ if (ch === "\u0003") {
42
+ cleanup();
43
+ reject(new VaultOnboardingError("Password entry cancelled."));
44
+ return;
45
+ }
46
+ if (ch === "\r" || ch === "\n") {
47
+ cleanup();
48
+ resolve(value);
49
+ return;
50
+ }
51
+ if (ch === "\u007f" || ch === "\b") {
52
+ value = value.slice(0, -1);
53
+ continue;
54
+ }
55
+ if (ch >= " ") value += ch;
56
+ }
57
+ };
58
+ input.on("data", onData);
59
+ });
60
+ }
61
+
62
+ function setMasterKey(value: string): void {
63
+ process.env.SLRD_MASTER_KEY = value;
64
+ }
65
+
66
+ function clearMasterKey(): void {
67
+ delete process.env.SLRD_MASTER_KEY;
68
+ }
69
+
70
+ async function confirmNewPassword(): Promise<string> {
71
+ process.stdout.write(
72
+ [
73
+ "",
74
+ "╭──────────────────────────────────────────────────────╮",
75
+ "│ 🦉 SOLARD │",
76
+ "│ Encrypted local wallets │",
77
+ "╰──────────────────────────────────────────────────────╯",
78
+ "",
79
+ "Your wallet secret keys are encrypted before they are stored.",
80
+ "Solard does NOT save this password. You will enter it again in a future",
81
+ "terminal session unless SLRD_MASTER_KEY is explicitly set in the environment.",
82
+ "",
83
+ ].join("\n"),
84
+ );
85
+
86
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
87
+ const first = await promptHidden("Create wallet password: ");
88
+ if (first.length < 8) {
89
+ process.stdout.write("Use at least 8 characters.\n\n");
90
+ continue;
91
+ }
92
+ const second = await promptHidden("Confirm password: ");
93
+ if (first !== second) {
94
+ process.stdout.write("Passwords did not match. Try again.\n\n");
95
+ continue;
96
+ }
97
+ return first;
98
+ }
99
+ throw new VaultOnboardingError(
100
+ "Could not confirm a wallet password after 3 attempts.",
101
+ );
102
+ }
103
+
104
+ async function validateExistingWalletPassword(password: string): Promise<void> {
105
+ setMasterKey(password);
106
+ const { createTraderSolard } = await import("@solard/sdk");
107
+ const probe = createTraderSolard();
108
+ try {
109
+ const wallets = probe.wallets.list();
110
+ if (!wallets.length) return;
111
+ // Decrypt one persisted wallet. All persisted wallets use the same process-level
112
+ // SLRD_MASTER_KEY contract, so this is enough to catch a mistyped password before
113
+ // the requested command begins doing work.
114
+ probe.wallets.signer(wallets[0]!.address);
115
+ } catch {
116
+ clearMasterKey();
117
+ throw new VaultOnboardingError(
118
+ "That wallet password could not decrypt the stored wallets.",
119
+ );
120
+ } finally {
121
+ probe.close();
122
+ }
123
+ }
124
+
125
+ async function unlockExistingWallets(): Promise<string> {
126
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
127
+ const password = await promptHidden("Wallet password: ");
128
+ try {
129
+ await validateExistingWalletPassword(password);
130
+ return password;
131
+ } catch (error) {
132
+ if (attempt === 3) throw error;
133
+ process.stdout.write("Incorrect password. Try again.\n\n");
134
+ }
135
+ }
136
+ throw new VaultOnboardingError("Unable to unlock the stored wallets.");
137
+ }
138
+
139
+ /**
140
+ * Ensure the process has an SLRD_MASTER_KEY before a command that needs a local
141
+ * signing wallet runs. Nothing is written to disk. The password/master-key value
142
+ * exists only in process.env for this process (and intentionally inherited child
143
+ * processes such as `slrd run ...`).
144
+ */
145
+ export async function ensureCliVaultReady(options: {
146
+ existingWalletCount: number;
147
+ allowCreate?: boolean;
148
+ }): Promise<VaultReadyResult> {
149
+ const existing = process.env.SLRD_MASTER_KEY?.trim();
150
+ if (existing) {
151
+ return {
152
+ source: "env",
153
+ created: false,
154
+ remembered: false,
155
+ persisted: false,
156
+ };
157
+ }
158
+
159
+ if (!interactive()) {
160
+ throw new VaultOnboardingError(
161
+ "This command needs a wallet password. Run it in an interactive terminal, " +
162
+ "or set SLRD_MASTER_KEY for CI/server automation.",
163
+ );
164
+ }
165
+
166
+ if (options.existingWalletCount > 0) {
167
+ await unlockExistingWallets();
168
+ return {
169
+ source: "interactive",
170
+ created: false,
171
+ remembered: false,
172
+ persisted: false,
173
+ };
174
+ }
175
+
176
+ if (options.allowCreate === false) {
177
+ throw new VaultOnboardingError(
178
+ "No stored signing wallet exists yet. Create one with `slrd wallet create [name]`.",
179
+ );
180
+ }
181
+
182
+ const password = await confirmNewPassword();
183
+ setMasterKey(password);
184
+ return {
185
+ source: "interactive-created",
186
+ created: true,
187
+ remembered: false,
188
+ persisted: false,
189
+ };
190
+ }
191
+
192
+ export function cliVaultStatus(): {
193
+ mode: "ephemeral-password";
194
+ environmentOverride: boolean;
195
+ passwordPersisted: false;
196
+ remembered: false;
197
+ } {
198
+ return {
199
+ mode: "ephemeral-password",
200
+ environmentOverride: Boolean(process.env.SLRD_MASTER_KEY?.trim()),
201
+ passwordPersisted: false,
202
+ remembered: false,
203
+ };
204
+ }
205
+
206
+ /** Optional helper for tests/tools that want a visible prompt without a signing command. */
207
+ export async function promptForWalletPassword(
208
+ existingWalletCount: number,
209
+ ): Promise<void> {
210
+ await ensureCliVaultReady({
211
+ existingWalletCount,
212
+ allowCreate: existingWalletCount === 0,
213
+ });
214
+ }