@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/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
+ };