@emblemvault/agentwallet 1.3.1 → 3.0.1
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 +157 -44
- package/docs/COMMANDS.md +171 -0
- package/docs/PLUGINS.md +106 -0
- package/docs/SETUP.md +180 -0
- package/emblemai.js +565 -859
- package/package.json +13 -5
- package/src/auth-server.js +300 -0
- package/src/auth.js +816 -0
- package/src/commands.js +754 -0
- package/src/formatter.js +250 -0
- package/src/glow.js +364 -0
- package/src/plugins/loader.js +533 -0
- package/src/session-store.js +94 -0
- package/src/tui.js +171 -0
package/src/auth.js
ADDED
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth.js - Authentication flow for emblem-enhanced TUI
|
|
3
|
+
*
|
|
4
|
+
* Handles password retrieval, credential storage (dotenvx encrypted),
|
|
5
|
+
* EmblemAuthSDK authentication, and the interactive auth menu.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import readline from 'readline';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import { execFile } from 'child_process';
|
|
14
|
+
import dotenvx from '@dotenvx/dotenvx';
|
|
15
|
+
import { saveSession, loadSession, clearSession, isSessionExpired } from './session-store.js';
|
|
16
|
+
import { startAuthServer } from './auth-server.js';
|
|
17
|
+
|
|
18
|
+
// ── Paths ────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const EMBLEMAI_DIR = path.join(os.homedir(), '.emblemai');
|
|
21
|
+
const ENV_PATH = path.join(EMBLEMAI_DIR, '.env');
|
|
22
|
+
const KEYS_PATH = path.join(EMBLEMAI_DIR, '.env.keys');
|
|
23
|
+
const SECRETS_PATH = path.join(EMBLEMAI_DIR, 'secrets.json');
|
|
24
|
+
const LEGACY_CRED_FILE = path.join(os.homedir(), '.emblem-vault');
|
|
25
|
+
|
|
26
|
+
// ── dotenvx Credential Storage ───────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read + decrypt a value from ~/.emblemai/.env using dotenvx.
|
|
30
|
+
* Returns null if the file doesn't exist or the key isn't found.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} key - Environment variable name (e.g. 'EMBLEM_PASSWORD')
|
|
33
|
+
* @returns {string | null}
|
|
34
|
+
*/
|
|
35
|
+
export function getCredential(key) {
|
|
36
|
+
if (!fs.existsSync(ENV_PATH)) return null;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const envContent = fs.readFileSync(ENV_PATH, 'utf8');
|
|
40
|
+
|
|
41
|
+
// Get private key for decryption
|
|
42
|
+
let privateKey = null;
|
|
43
|
+
if (fs.existsSync(KEYS_PATH)) {
|
|
44
|
+
const keysContent = fs.readFileSync(KEYS_PATH, 'utf8');
|
|
45
|
+
const match = keysContent.match(/DOTENV_PRIVATE_KEY\s*=\s*"?([^"\s]+)"?/);
|
|
46
|
+
if (match) privateKey = match[1];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parsed = privateKey
|
|
50
|
+
? dotenvx.parse(envContent, { privateKey })
|
|
51
|
+
: dotenvx.parse(envContent);
|
|
52
|
+
|
|
53
|
+
return parsed[key] || null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Encrypt + write a value to ~/.emblemai/.env via dotenvx.
|
|
61
|
+
* Auto-creates the keypair on first call.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} key - Environment variable name
|
|
64
|
+
* @param {string} value - Value to encrypt and store
|
|
65
|
+
*/
|
|
66
|
+
export function setCredential(key, value) {
|
|
67
|
+
fs.mkdirSync(EMBLEMAI_DIR, { recursive: true });
|
|
68
|
+
if (!fs.existsSync(ENV_PATH)) {
|
|
69
|
+
fs.writeFileSync(ENV_PATH, '', 'utf8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Suppress dotenvx stdout noise (banner, hints, etc.)
|
|
73
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
74
|
+
process.stdout.write = () => true;
|
|
75
|
+
try {
|
|
76
|
+
dotenvx.set(key, value, { path: ENV_PATH });
|
|
77
|
+
} finally {
|
|
78
|
+
process.stdout.write = origWrite;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Secure the keys file (contains the private decryption key)
|
|
82
|
+
if (fs.existsSync(KEYS_PATH)) {
|
|
83
|
+
fs.chmodSync(KEYS_PATH, 0o600);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Plugin Secrets (auth-sdk encrypted JSON) ─────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read plugin secrets from ~/.emblemai/secrets.json.
|
|
91
|
+
*
|
|
92
|
+
* @returns {Record<string, { ciphertext: string, dataToEncryptHash: string }>}
|
|
93
|
+
*/
|
|
94
|
+
export function readPluginSecrets() {
|
|
95
|
+
try {
|
|
96
|
+
if (!fs.existsSync(SECRETS_PATH)) return {};
|
|
97
|
+
const raw = fs.readFileSync(SECRETS_PATH, 'utf8').trim();
|
|
98
|
+
if (!raw) return {};
|
|
99
|
+
return JSON.parse(raw);
|
|
100
|
+
} catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Write plugin secrets to ~/.emblemai/secrets.json.
|
|
107
|
+
*
|
|
108
|
+
* @param {Record<string, { ciphertext: string, dataToEncryptHash: string }>} secrets
|
|
109
|
+
*/
|
|
110
|
+
export function writePluginSecrets(secrets) {
|
|
111
|
+
fs.mkdirSync(EMBLEMAI_DIR, { recursive: true });
|
|
112
|
+
fs.writeFileSync(SECRETS_PATH, JSON.stringify(secrets, null, 2) + '\n', 'utf8');
|
|
113
|
+
fs.chmodSync(SECRETS_PATH, 0o600);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Compatibility Wrappers ───────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Compatibility wrapper — reads password from dotenvx + secrets from JSON.
|
|
120
|
+
* @returns {{ password?: string, secrets?: Record<string, object> } | null}
|
|
121
|
+
*/
|
|
122
|
+
export function readCredentialFile() {
|
|
123
|
+
const password = getCredential('EMBLEM_PASSWORD');
|
|
124
|
+
const secrets = readPluginSecrets();
|
|
125
|
+
if (!password && Object.keys(secrets).length === 0) return null;
|
|
126
|
+
return { password, secrets };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Compatibility wrapper — routes password to dotenvx and secrets to JSON.
|
|
131
|
+
* @param {Record<string, unknown>} data - Fields to merge (password, secrets)
|
|
132
|
+
*/
|
|
133
|
+
export function writeCredentialFile(data) {
|
|
134
|
+
if (data.password) {
|
|
135
|
+
setCredential('EMBLEM_PASSWORD', data.password);
|
|
136
|
+
}
|
|
137
|
+
if (data.secrets) {
|
|
138
|
+
const existing = readPluginSecrets();
|
|
139
|
+
writePluginSecrets({ ...existing, ...data.secrets });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Legacy Migration ─────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Migrate credentials from legacy ~/.emblem-vault to new dotenvx format.
|
|
147
|
+
* Only runs if the old file exists AND ~/.emblemai/.env does NOT exist.
|
|
148
|
+
* Backs up old file to ~/.emblem-vault.bak.
|
|
149
|
+
*/
|
|
150
|
+
export function migrateLegacyCredentials() {
|
|
151
|
+
if (!fs.existsSync(LEGACY_CRED_FILE)) return;
|
|
152
|
+
if (fs.existsSync(ENV_PATH)) return; // already migrated
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const raw = fs.readFileSync(LEGACY_CRED_FILE, 'utf8').trim();
|
|
156
|
+
if (!raw) return;
|
|
157
|
+
|
|
158
|
+
let password = null;
|
|
159
|
+
let secrets = {};
|
|
160
|
+
|
|
161
|
+
if (raw.startsWith('{')) {
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(raw);
|
|
164
|
+
password = parsed.password;
|
|
165
|
+
secrets = parsed.secrets || {};
|
|
166
|
+
} catch {
|
|
167
|
+
password = raw;
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
password = raw;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (password) {
|
|
174
|
+
setCredential('EMBLEM_PASSWORD', password);
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(secrets).length > 0) {
|
|
177
|
+
writePluginSecrets(secrets);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Backup old file (never deleted)
|
|
181
|
+
fs.renameSync(LEGACY_CRED_FILE, LEGACY_CRED_FILE + '.bak');
|
|
182
|
+
} catch {
|
|
183
|
+
// Migration failed — old file stays, user can retry next run
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Password Prompt ──────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Prompt for a password with hidden input (shows * per character).
|
|
191
|
+
* Falls back to plain text prompt when stdin is not a TTY.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} question - Prompt text to display
|
|
194
|
+
* @returns {Promise<string>} The entered password
|
|
195
|
+
*/
|
|
196
|
+
export function promptPassword(question) {
|
|
197
|
+
if (process.stdin.isTTY) {
|
|
198
|
+
process.stdout.write(question);
|
|
199
|
+
return new Promise((resolve) => {
|
|
200
|
+
let password = '';
|
|
201
|
+
const onData = (char) => {
|
|
202
|
+
char = char.toString();
|
|
203
|
+
switch (char) {
|
|
204
|
+
case '\n':
|
|
205
|
+
case '\r':
|
|
206
|
+
case '\u0004':
|
|
207
|
+
process.stdin.removeListener('data', onData);
|
|
208
|
+
process.stdin.setRawMode(false);
|
|
209
|
+
process.stdout.write('\n');
|
|
210
|
+
resolve(password);
|
|
211
|
+
break;
|
|
212
|
+
case '\u0003':
|
|
213
|
+
process.stdout.write('\n');
|
|
214
|
+
process.exit();
|
|
215
|
+
break;
|
|
216
|
+
case '\u007F':
|
|
217
|
+
if (password.length > 0) {
|
|
218
|
+
password = password.slice(0, -1);
|
|
219
|
+
process.stdout.write('\b \b');
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
default:
|
|
223
|
+
password += char;
|
|
224
|
+
process.stdout.write('*');
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
process.stdin.setRawMode(true);
|
|
228
|
+
process.stdin.resume();
|
|
229
|
+
process.stdin.on('data', onData);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Non-TTY fallback: plain readline prompt
|
|
234
|
+
const rl = readline.createInterface({
|
|
235
|
+
input: process.stdin,
|
|
236
|
+
output: process.stdout,
|
|
237
|
+
});
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
rl.question(question, (answer) => {
|
|
240
|
+
rl.close();
|
|
241
|
+
resolve(answer);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Password Resolution ──────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get password from multiple sources in priority order:
|
|
250
|
+
* 1. args.password (-p flag) — use it AND store encrypted
|
|
251
|
+
* 2. process.env.EMBLEM_PASSWORD — use it (don't store)
|
|
252
|
+
* 3. Encrypted credential file — getCredential('EMBLEM_PASSWORD')
|
|
253
|
+
* 4. Agent mode, no password found — auto-generate, store encrypted
|
|
254
|
+
* 5. Interactive prompt
|
|
255
|
+
*
|
|
256
|
+
* @param {{ password?: string, isAgentMode?: boolean }} args
|
|
257
|
+
* @returns {Promise<string>} The resolved password
|
|
258
|
+
*/
|
|
259
|
+
export async function getPassword(args = {}) {
|
|
260
|
+
// 1. Explicit argument — store encrypted
|
|
261
|
+
if (args.password) {
|
|
262
|
+
setCredential('EMBLEM_PASSWORD', args.password);
|
|
263
|
+
return args.password;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 2. Environment variable
|
|
267
|
+
if (process.env.EMBLEM_PASSWORD) return process.env.EMBLEM_PASSWORD;
|
|
268
|
+
|
|
269
|
+
// 3. Encrypted credential file
|
|
270
|
+
const stored = getCredential('EMBLEM_PASSWORD');
|
|
271
|
+
if (stored) return stored;
|
|
272
|
+
|
|
273
|
+
// 4. Agent mode — auto-generate password
|
|
274
|
+
if (args.isAgentMode) {
|
|
275
|
+
const generated = crypto.randomBytes(32).toString('base64url');
|
|
276
|
+
setCredential('EMBLEM_PASSWORD', generated);
|
|
277
|
+
return generated;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 5. Interactive prompt
|
|
281
|
+
return promptPassword('Enter your EmblemVault password (min 16 chars): ');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Authentication ───────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Authenticate with EmblemAuthSDK using a password.
|
|
288
|
+
*
|
|
289
|
+
* @param {string} password - The user's password
|
|
290
|
+
* @param {{ authUrl?: string, apiUrl?: string }} config - Optional SDK config overrides
|
|
291
|
+
* @returns {Promise<{ authSdk: object, session: object }>}
|
|
292
|
+
*/
|
|
293
|
+
export async function authenticate(password, config = {}) {
|
|
294
|
+
const { EmblemAuthSDK } = await import('@emblemvault/auth-sdk');
|
|
295
|
+
|
|
296
|
+
const sdkConfig = {
|
|
297
|
+
appId: 'emblem-agent-wallet',
|
|
298
|
+
persistSession: false,
|
|
299
|
+
};
|
|
300
|
+
if (config.authUrl) sdkConfig.authUrl = config.authUrl;
|
|
301
|
+
if (config.apiUrl) sdkConfig.apiUrl = config.apiUrl;
|
|
302
|
+
|
|
303
|
+
const authSdk = new EmblemAuthSDK(sdkConfig);
|
|
304
|
+
const session = await authSdk.authenticatePassword({ password });
|
|
305
|
+
|
|
306
|
+
if (!session) {
|
|
307
|
+
throw new Error('Authentication failed');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { authSdk, session };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Browser Globals Polyfill ─────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Polyfill browser globals for Node.js environment.
|
|
317
|
+
* The auth-sdk checks for window/document/localStorage even in non-browser contexts.
|
|
318
|
+
*/
|
|
319
|
+
export function polyfillBrowserGlobals() {
|
|
320
|
+
if (typeof globalThis.window !== 'undefined') return;
|
|
321
|
+
|
|
322
|
+
globalThis.window = {
|
|
323
|
+
localStorage: {
|
|
324
|
+
getItem: () => null,
|
|
325
|
+
setItem: () => {},
|
|
326
|
+
removeItem: () => {},
|
|
327
|
+
clear: () => {},
|
|
328
|
+
},
|
|
329
|
+
sessionStorage: {
|
|
330
|
+
getItem: () => null,
|
|
331
|
+
setItem: () => {},
|
|
332
|
+
removeItem: () => {},
|
|
333
|
+
clear: () => {},
|
|
334
|
+
},
|
|
335
|
+
location: {
|
|
336
|
+
href: 'http://localhost',
|
|
337
|
+
origin: 'http://localhost',
|
|
338
|
+
protocol: 'http:',
|
|
339
|
+
host: 'localhost',
|
|
340
|
+
hostname: 'localhost',
|
|
341
|
+
port: '',
|
|
342
|
+
pathname: '/',
|
|
343
|
+
search: '',
|
|
344
|
+
hash: '',
|
|
345
|
+
},
|
|
346
|
+
addEventListener: () => {},
|
|
347
|
+
removeEventListener: () => {},
|
|
348
|
+
dispatchEvent: () => true,
|
|
349
|
+
navigator: { userAgent: 'Node.js' },
|
|
350
|
+
};
|
|
351
|
+
globalThis.document = {
|
|
352
|
+
addEventListener: () => {},
|
|
353
|
+
removeEventListener: () => {},
|
|
354
|
+
createElement: () => ({}),
|
|
355
|
+
querySelector: () => null,
|
|
356
|
+
querySelectorAll: () => [],
|
|
357
|
+
};
|
|
358
|
+
globalThis.localStorage = globalThis.window.localStorage;
|
|
359
|
+
globalThis.sessionStorage = globalThis.window.sessionStorage;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── Authenticate with Existing Session ──────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Create an SDK instance and hydrate it with an existing session.
|
|
366
|
+
*
|
|
367
|
+
* @param {object} session - A valid AuthSession object
|
|
368
|
+
* @param {{ authUrl?: string, apiUrl?: string }} config
|
|
369
|
+
* @returns {Promise<{ authSdk: object, session: object }>}
|
|
370
|
+
*/
|
|
371
|
+
export async function authenticateWithSession(session, config = {}) {
|
|
372
|
+
const { EmblemAuthSDK } = await import('@emblemvault/auth-sdk');
|
|
373
|
+
|
|
374
|
+
const sdkConfig = {
|
|
375
|
+
appId: 'emblem-agent-wallet',
|
|
376
|
+
persistSession: false,
|
|
377
|
+
};
|
|
378
|
+
if (config.authUrl) sdkConfig.authUrl = config.authUrl;
|
|
379
|
+
if (config.apiUrl) sdkConfig.apiUrl = config.apiUrl;
|
|
380
|
+
|
|
381
|
+
const authSdk = new EmblemAuthSDK(sdkConfig);
|
|
382
|
+
authSdk.hydrateSession(session);
|
|
383
|
+
|
|
384
|
+
return { authSdk, session };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Web Login Flow ──────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
const WEB_LOGIN_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Orchestrate browser-based authentication for interactive mode.
|
|
393
|
+
*
|
|
394
|
+
* 1. Check for saved session → if valid, hydrate SDK and return
|
|
395
|
+
* 2. If expired, clear it
|
|
396
|
+
* 3. Start local auth server → open browser → wait for callback
|
|
397
|
+
* 4. On success, save session and return { authSdk, session }
|
|
398
|
+
* 5. On failure/timeout, return null (caller falls back to password)
|
|
399
|
+
*
|
|
400
|
+
* @param {{ authUrl?: string, apiUrl?: string }} config
|
|
401
|
+
* @returns {Promise<{ authSdk: object, session: object } | null>}
|
|
402
|
+
*/
|
|
403
|
+
export async function webLogin(config = {}) {
|
|
404
|
+
// 1. Check for saved session
|
|
405
|
+
const existing = loadSession();
|
|
406
|
+
|
|
407
|
+
if (existing) {
|
|
408
|
+
if (!isSessionExpired(existing)) {
|
|
409
|
+
// Valid session — hydrate SDK and return
|
|
410
|
+
const result = await authenticateWithSession(existing, config);
|
|
411
|
+
return { ...result, source: 'saved' };
|
|
412
|
+
}
|
|
413
|
+
// Expired — clear it
|
|
414
|
+
clearSession();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 2. Start local auth server and open browser
|
|
418
|
+
return new Promise(async (resolve) => {
|
|
419
|
+
let serverResult = null;
|
|
420
|
+
let timeoutId = null;
|
|
421
|
+
|
|
422
|
+
// Timeout after 5 minutes
|
|
423
|
+
timeoutId = setTimeout(() => {
|
|
424
|
+
if (serverResult) serverResult.close();
|
|
425
|
+
resolve(null);
|
|
426
|
+
}, WEB_LOGIN_TIMEOUT);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
serverResult = await startAuthServer(config, {
|
|
430
|
+
onSession: async (session) => {
|
|
431
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
432
|
+
|
|
433
|
+
// Save session to disk
|
|
434
|
+
saveSession(session);
|
|
435
|
+
|
|
436
|
+
// Hydrate SDK
|
|
437
|
+
try {
|
|
438
|
+
const result = await authenticateWithSession(session, config);
|
|
439
|
+
resolve({ ...result, source: 'browser' });
|
|
440
|
+
} catch (err) {
|
|
441
|
+
resolve(null);
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
onError: (error) => {
|
|
445
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
446
|
+
if (serverResult) serverResult.close();
|
|
447
|
+
resolve(null);
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Try to open browser (uses execFile to prevent shell injection)
|
|
452
|
+
const opened = await openBrowser(serverResult.url);
|
|
453
|
+
|
|
454
|
+
if (!opened) {
|
|
455
|
+
console.log(`\nOpen this URL in your browser to authenticate:\n ${serverResult.url}\n`);
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
459
|
+
if (serverResult) serverResult.close();
|
|
460
|
+
resolve(null);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Try to open a URL in the default browser.
|
|
467
|
+
* Uses execFile (not exec) to prevent shell injection.
|
|
468
|
+
*
|
|
469
|
+
* @param {string} url
|
|
470
|
+
* @returns {Promise<boolean>}
|
|
471
|
+
*/
|
|
472
|
+
function openBrowser(url) {
|
|
473
|
+
return new Promise((resolve) => {
|
|
474
|
+
const platform = process.platform;
|
|
475
|
+
let cmd, args;
|
|
476
|
+
|
|
477
|
+
if (platform === 'darwin') {
|
|
478
|
+
cmd = 'open';
|
|
479
|
+
args = [url];
|
|
480
|
+
} else if (platform === 'win32') {
|
|
481
|
+
cmd = 'cmd';
|
|
482
|
+
args = ['/c', 'start', '', url];
|
|
483
|
+
} else {
|
|
484
|
+
cmd = 'xdg-open';
|
|
485
|
+
args = [url];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
execFile(cmd, args, (err) => {
|
|
489
|
+
resolve(!err);
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ── Auth Menu ────────────────────────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Interactive authentication menu.
|
|
498
|
+
* Displays options for key/address retrieval, session management, and logout.
|
|
499
|
+
*
|
|
500
|
+
* @param {object} authSdk - Authenticated EmblemAuthSDK instance
|
|
501
|
+
* @param {(question: string) => Promise<string>} promptFn - Function to prompt for user input
|
|
502
|
+
*/
|
|
503
|
+
export async function authMenu(authSdk, promptFn) {
|
|
504
|
+
console.log('\n========================================');
|
|
505
|
+
console.log(' Authentication Menu');
|
|
506
|
+
console.log('========================================');
|
|
507
|
+
console.log('');
|
|
508
|
+
console.log(' 1. Get API Key');
|
|
509
|
+
console.log(' 2. Get Vault Info');
|
|
510
|
+
console.log(' 3. Session Info');
|
|
511
|
+
console.log(' 4. Refresh Session');
|
|
512
|
+
console.log(' 5. EVM Address');
|
|
513
|
+
console.log(' 6. Solana Address');
|
|
514
|
+
console.log(' 7. BTC Addresses');
|
|
515
|
+
console.log(' 8. Backup Agent Auth');
|
|
516
|
+
console.log(' 9. Logout');
|
|
517
|
+
console.log(' 0. Back');
|
|
518
|
+
console.log('');
|
|
519
|
+
|
|
520
|
+
const choice = await promptFn('Select option (1-0): ');
|
|
521
|
+
|
|
522
|
+
switch (choice.trim()) {
|
|
523
|
+
case '1':
|
|
524
|
+
await _getApiKey(authSdk);
|
|
525
|
+
break;
|
|
526
|
+
case '2':
|
|
527
|
+
await _getVaultInfo(authSdk);
|
|
528
|
+
break;
|
|
529
|
+
case '3':
|
|
530
|
+
_showSessionInfo(authSdk);
|
|
531
|
+
break;
|
|
532
|
+
case '4':
|
|
533
|
+
await _refreshSession(authSdk);
|
|
534
|
+
break;
|
|
535
|
+
case '5':
|
|
536
|
+
await _getEvmAddress(authSdk);
|
|
537
|
+
break;
|
|
538
|
+
case '6':
|
|
539
|
+
await _getSolanaAddress(authSdk);
|
|
540
|
+
break;
|
|
541
|
+
case '7':
|
|
542
|
+
await _getBtcAddresses(authSdk);
|
|
543
|
+
break;
|
|
544
|
+
case '8':
|
|
545
|
+
await _backupAgentAuth(promptFn);
|
|
546
|
+
break;
|
|
547
|
+
case '9':
|
|
548
|
+
_doLogout(authSdk);
|
|
549
|
+
return 'logout'; // signal caller to exit
|
|
550
|
+
case '0':
|
|
551
|
+
return;
|
|
552
|
+
default:
|
|
553
|
+
console.log('Invalid option');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Recurse back to menu after handling an option
|
|
557
|
+
await authMenu(authSdk, promptFn);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ---- Internal helpers ----
|
|
561
|
+
|
|
562
|
+
async function _getApiKey(authSdk) {
|
|
563
|
+
console.log('\nFetching API key...');
|
|
564
|
+
try {
|
|
565
|
+
const apiKey = await authSdk.getVaultApiKey();
|
|
566
|
+
console.log('\n========================================');
|
|
567
|
+
console.log(' YOUR API KEY');
|
|
568
|
+
console.log('========================================');
|
|
569
|
+
console.log('');
|
|
570
|
+
console.log(` ${apiKey}`);
|
|
571
|
+
console.log('');
|
|
572
|
+
console.log('========================================');
|
|
573
|
+
console.log('');
|
|
574
|
+
console.log('IMPORTANT: Store this key securely!');
|
|
575
|
+
} catch (error) {
|
|
576
|
+
console.error('Error fetching API key:', error.message);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function _getVaultInfo(authSdk) {
|
|
581
|
+
console.log('\nFetching vault info...');
|
|
582
|
+
try {
|
|
583
|
+
const vaultInfo = await authSdk.getVaultInfo();
|
|
584
|
+
console.log('\n========================================');
|
|
585
|
+
console.log(' VAULT INFO');
|
|
586
|
+
console.log('========================================');
|
|
587
|
+
console.log('');
|
|
588
|
+
console.log(` Vault ID: ${vaultInfo.vaultId || 'N/A'}`);
|
|
589
|
+
console.log(
|
|
590
|
+
` Token ID: ${vaultInfo.tokenId || vaultInfo.vaultId || 'N/A'}`
|
|
591
|
+
);
|
|
592
|
+
console.log(` EVM Address: ${vaultInfo.evmAddress || 'N/A'}`);
|
|
593
|
+
console.log(
|
|
594
|
+
` Solana Address: ${vaultInfo.solanaAddress || vaultInfo.address || 'N/A'}`
|
|
595
|
+
);
|
|
596
|
+
console.log(` Hedera Account: ${vaultInfo.hederaAccountId || 'N/A'}`);
|
|
597
|
+
if (vaultInfo.btcPubkey) {
|
|
598
|
+
console.log(
|
|
599
|
+
` BTC Pubkey: ${vaultInfo.btcPubkey.substring(0, 20)}...`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (vaultInfo.btcAddresses) {
|
|
603
|
+
console.log(' BTC Addresses:');
|
|
604
|
+
if (vaultInfo.btcAddresses.p2pkh)
|
|
605
|
+
console.log(` P2PKH: ${vaultInfo.btcAddresses.p2pkh}`);
|
|
606
|
+
if (vaultInfo.btcAddresses.p2wpkh)
|
|
607
|
+
console.log(` P2WPKH: ${vaultInfo.btcAddresses.p2wpkh}`);
|
|
608
|
+
if (vaultInfo.btcAddresses.p2tr)
|
|
609
|
+
console.log(` P2TR: ${vaultInfo.btcAddresses.p2tr}`);
|
|
610
|
+
}
|
|
611
|
+
if (vaultInfo.createdAt)
|
|
612
|
+
console.log(` Created At: ${vaultInfo.createdAt}`);
|
|
613
|
+
console.log('');
|
|
614
|
+
console.log('========================================');
|
|
615
|
+
} catch (error) {
|
|
616
|
+
console.error('Error fetching vault info:', error.message);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function _showSessionInfo(authSdk) {
|
|
621
|
+
const sess = authSdk.getSession();
|
|
622
|
+
console.log('\n========================================');
|
|
623
|
+
console.log(' SESSION INFO');
|
|
624
|
+
console.log('========================================');
|
|
625
|
+
console.log('');
|
|
626
|
+
if (sess) {
|
|
627
|
+
console.log(` Identifier: ${sess.user?.identifier || 'N/A'}`);
|
|
628
|
+
console.log(` Vault ID: ${sess.user?.vaultId || 'N/A'}`);
|
|
629
|
+
console.log(` App ID: ${sess.appId || 'N/A'}`);
|
|
630
|
+
console.log(` Auth Type: ${sess.authType || 'N/A'}`);
|
|
631
|
+
console.log(
|
|
632
|
+
` Expires At: ${sess.expiresAt ? new Date(sess.expiresAt).toISOString() : 'N/A'}`
|
|
633
|
+
);
|
|
634
|
+
console.log(
|
|
635
|
+
` Auth Token: ${sess.authToken ? sess.authToken.substring(0, 20) + '...' : 'N/A'}`
|
|
636
|
+
);
|
|
637
|
+
} else {
|
|
638
|
+
console.log(' No active session');
|
|
639
|
+
}
|
|
640
|
+
console.log('');
|
|
641
|
+
console.log('========================================');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function _refreshSession(authSdk) {
|
|
645
|
+
console.log('\nRefreshing session...');
|
|
646
|
+
try {
|
|
647
|
+
const newSession = await authSdk.refreshSession();
|
|
648
|
+
if (newSession) {
|
|
649
|
+
console.log('Session refreshed successfully!');
|
|
650
|
+
console.log(
|
|
651
|
+
`New expiry: ${new Date(newSession.expiresAt).toISOString()}`
|
|
652
|
+
);
|
|
653
|
+
} else {
|
|
654
|
+
console.log('Failed to refresh session.');
|
|
655
|
+
}
|
|
656
|
+
} catch (error) {
|
|
657
|
+
console.error('Error refreshing session:', error.message);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function _getEvmAddress(authSdk) {
|
|
662
|
+
console.log('\nFetching EVM address...');
|
|
663
|
+
try {
|
|
664
|
+
const vaultInfo = await authSdk.getVaultInfo();
|
|
665
|
+
if (vaultInfo.evmAddress) {
|
|
666
|
+
console.log('\n========================================');
|
|
667
|
+
console.log(' EVM ADDRESS');
|
|
668
|
+
console.log('========================================');
|
|
669
|
+
console.log('');
|
|
670
|
+
console.log(` ${vaultInfo.evmAddress}`);
|
|
671
|
+
console.log('');
|
|
672
|
+
console.log('========================================');
|
|
673
|
+
} else {
|
|
674
|
+
console.log('No EVM address available for this vault.');
|
|
675
|
+
}
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.error('Error fetching EVM address:', error.message);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function _getSolanaAddress(authSdk) {
|
|
682
|
+
console.log('\nFetching Solana address...');
|
|
683
|
+
try {
|
|
684
|
+
const vaultInfo = await authSdk.getVaultInfo();
|
|
685
|
+
const solanaAddr = vaultInfo.solanaAddress || vaultInfo.address;
|
|
686
|
+
if (solanaAddr) {
|
|
687
|
+
console.log('\n========================================');
|
|
688
|
+
console.log(' SOLANA ADDRESS');
|
|
689
|
+
console.log('========================================');
|
|
690
|
+
console.log('');
|
|
691
|
+
console.log(` ${solanaAddr}`);
|
|
692
|
+
console.log('');
|
|
693
|
+
console.log('========================================');
|
|
694
|
+
} else {
|
|
695
|
+
console.log('No Solana address available for this vault.');
|
|
696
|
+
}
|
|
697
|
+
} catch (error) {
|
|
698
|
+
console.error('Error fetching Solana address:', error.message);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function _getBtcAddresses(authSdk) {
|
|
703
|
+
console.log('\nFetching BTC addresses...');
|
|
704
|
+
try {
|
|
705
|
+
const vaultInfo = await authSdk.getVaultInfo();
|
|
706
|
+
if (vaultInfo.btcAddresses || vaultInfo.btcPubkey) {
|
|
707
|
+
console.log('\n========================================');
|
|
708
|
+
console.log(' BTC ADDRESSES');
|
|
709
|
+
console.log('========================================');
|
|
710
|
+
console.log('');
|
|
711
|
+
if (vaultInfo.btcPubkey) {
|
|
712
|
+
console.log(` Pubkey: ${vaultInfo.btcPubkey}`);
|
|
713
|
+
console.log('');
|
|
714
|
+
}
|
|
715
|
+
if (vaultInfo.btcAddresses) {
|
|
716
|
+
if (vaultInfo.btcAddresses.p2pkh)
|
|
717
|
+
console.log(
|
|
718
|
+
` P2PKH (Legacy): ${vaultInfo.btcAddresses.p2pkh}`
|
|
719
|
+
);
|
|
720
|
+
if (vaultInfo.btcAddresses.p2wpkh)
|
|
721
|
+
console.log(
|
|
722
|
+
` P2WPKH (SegWit): ${vaultInfo.btcAddresses.p2wpkh}`
|
|
723
|
+
);
|
|
724
|
+
if (vaultInfo.btcAddresses.p2tr)
|
|
725
|
+
console.log(
|
|
726
|
+
` P2TR (Taproot): ${vaultInfo.btcAddresses.p2tr}`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
console.log('');
|
|
730
|
+
console.log('========================================');
|
|
731
|
+
} else {
|
|
732
|
+
console.log('No BTC addresses available for this vault.');
|
|
733
|
+
}
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.error('Error fetching BTC addresses:', error.message);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function _backupAgentAuth(promptFn) {
|
|
740
|
+
console.log('\n========================================');
|
|
741
|
+
console.log(' BACKUP AGENT AUTH');
|
|
742
|
+
console.log('========================================');
|
|
743
|
+
console.log('');
|
|
744
|
+
|
|
745
|
+
// Check that both files exist
|
|
746
|
+
if (!fs.existsSync(ENV_PATH)) {
|
|
747
|
+
console.log(' No agent credentials found (.env missing).');
|
|
748
|
+
console.log(' Agent auth is created on first agent-mode run.');
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (!fs.existsSync(KEYS_PATH)) {
|
|
752
|
+
console.log(' No encryption keys found (.env.keys missing).');
|
|
753
|
+
console.log(' Cannot backup without the decryption key.');
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Read both files
|
|
758
|
+
const envContent = fs.readFileSync(ENV_PATH, 'utf8');
|
|
759
|
+
const keysContent = fs.readFileSync(KEYS_PATH, 'utf8');
|
|
760
|
+
|
|
761
|
+
// Default backup path
|
|
762
|
+
const defaultPath = path.join(os.homedir(), 'emblemai-auth-backup.json');
|
|
763
|
+
const input = await promptFn(`Backup path [${defaultPath}]: `);
|
|
764
|
+
const backupPath = input.trim() || defaultPath;
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
const backup = {
|
|
768
|
+
_warning: 'This file contains your EmblemVault password. Keep it secure.',
|
|
769
|
+
exportedAt: new Date().toISOString(),
|
|
770
|
+
env: envContent,
|
|
771
|
+
envKeys: keysContent,
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// Also include secrets if they exist
|
|
775
|
+
if (fs.existsSync(SECRETS_PATH)) {
|
|
776
|
+
backup.secrets = fs.readFileSync(SECRETS_PATH, 'utf8');
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
fs.writeFileSync(backupPath, JSON.stringify(backup, null, 2), { mode: 0o600 });
|
|
780
|
+
|
|
781
|
+
console.log('');
|
|
782
|
+
console.log(' Saved to:');
|
|
783
|
+
console.log(` ${backupPath}`);
|
|
784
|
+
console.log('');
|
|
785
|
+
console.log(' This file contains your EmblemVault password.');
|
|
786
|
+
console.log(' Keep it safe — anyone with it can access your vault.');
|
|
787
|
+
console.log('');
|
|
788
|
+
console.log(' To restore on another machine, copy the backup file');
|
|
789
|
+
console.log(' and run: emblemai --restore-auth <path>');
|
|
790
|
+
console.log('');
|
|
791
|
+
console.log('========================================');
|
|
792
|
+
} catch (error) {
|
|
793
|
+
console.error(` Error writing backup: ${error.message}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function _doLogout(authSdk) {
|
|
798
|
+
console.log('\nLogging out...');
|
|
799
|
+
try {
|
|
800
|
+
authSdk.logout();
|
|
801
|
+
clearSession(); // Also clear saved web session
|
|
802
|
+
console.log('Logged out successfully.');
|
|
803
|
+
console.log('Session cleared.');
|
|
804
|
+
} catch (error) {
|
|
805
|
+
console.error('Error during logout:', error.message);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export { clearSession } from './session-store.js';
|
|
810
|
+
|
|
811
|
+
export default {
|
|
812
|
+
getPassword, authenticate, authenticateWithSession, promptPassword, authMenu,
|
|
813
|
+
webLogin, polyfillBrowserGlobals,
|
|
814
|
+
getCredential, setCredential, readPluginSecrets, writePluginSecrets,
|
|
815
|
+
readCredentialFile, writeCredentialFile, migrateLegacyCredentials,
|
|
816
|
+
};
|