@solard/cli 0.2.13 → 0.2.15

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