| … | … | @@ -309,7 +309,9 @@ function withRateLimit(handler) { |
| 309 | 309 | const ALLOWED_ORIGINS = [ |
| 310 | 310 | 'http://localhost:4004', |
| 311 | 311 | 'http://127.0.0.1:4004', |
| 312 | | 'http://0.0.0.0:4004' |
| 312 | 'http://0.0.0.0:4004', |
| 313 | 'http://ashatron.local:4004', // Network testing |
| 314 | 'http://tabertron.local:4004' // Network testing |
| 313 | 315 | // Add production origins here when deploying: |
| 314 | 316 | // 'https://your-production-domain.com' |
| 315 | 317 | ] |
| … | … | @@ -1370,6 +1372,75 @@ const routes = { |
| 1370 | 1372 | } |
| 1371 | 1373 | }), |
| 1372 | 1374 | |
| 1375 | // POST /api/unlock - Unlock existing wallet with password (caches password in memory) |
| 1376 | 'POST /api/unlock': async (req, res) => { |
| 1377 | try { |
| 1378 | const body = await readBody(req) |
| 1379 | const { password } = body |
| 1380 | |
| 1381 | // Validate password provided |
| 1382 | if (!password || password.length < 8) { |
| 1383 | return sendError(res, 'Password must be at least 8 characters', 400) |
| 1384 | } |
| 1385 | |
| 1386 | // Check if wallet exists |
| 1387 | if (!fs.existsSync(MNEMONIC_FILE)) { |
| 1388 | return sendError(res, 'Wallet not initialized. Use /api/init or /api/restore first.', 400) |
| 1389 | } |
| 1390 | |
| 1391 | // Try to load mnemonics with provided password |
| 1392 | let identityMnemonic, walletMnemonic |
| 1393 | try { |
| 1394 | identityMnemonic = loadIdentityMnemonic(password) |
| 1395 | walletMnemonic = loadWalletMnemonic(password) |
| 1396 | } catch (err) { |
| 1397 | // If decryption fails, check if it's plaintext (legacy) |
| 1398 | const identityContent = fs.readFileSync(MNEMONIC_FILE, 'utf8').trim() |
| 1399 | const walletContent = fs.readFileSync(path.join(STORAGE_DIR, '.wallet-mnemonic'), 'utf8').trim() |
| 1400 | |
| 1401 | if (identityContent.includes(' ') && identityContent.split(' ').length === 24 && |
| 1402 | walletContent.includes(' ') && walletContent.split(' ').length === 12) { |
| 1403 | // Legacy plaintext mnemonics - migrate to encrypted format |
| 1404 | console.log('🔄 Migrating plaintext mnemonics to encrypted format...') |
| 1405 | identityMnemonic = identityContent |
| 1406 | walletMnemonic = walletContent |
| 1407 | |
| 1408 | // Save encrypted versions |
| 1409 | saveIdentityMnemonic(identityMnemonic, password) |
| 1410 | saveWalletMnemonic(walletMnemonic, password) |
| 1411 | console.log('✅ Mnemonics encrypted successfully') |
| 1412 | } else { |
| 1413 | return sendError(res, 'Invalid password', 401) |
| 1414 | } |
| 1415 | } |
| 1416 | |
| 1417 | // Cache password in memory |
| 1418 | setCachedPassword(password) |
| 1419 | |
| 1420 | // Load or generate API token |
| 1421 | let apiToken = loadToken() |
| 1422 | if (!apiToken) { |
| 1423 | apiToken = generateToken() |
| 1424 | saveToken(apiToken) |
| 1425 | } |
| 1426 | |
| 1427 | // Get public key for confirmation |
| 1428 | const identity = await IdentityKey.from({ mnemonic: identityMnemonic }) |
| 1429 | const publicKey = b4a.toString(identity.identityKeyPair.publicKey, 'hex').slice(0, 16) + '...' |
| 1430 | |
| 1431 | sendJSON(res, { |
| 1432 | success: true, |
| 1433 | message: 'Wallet unlocked successfully', |
| 1434 | publicKey, |
| 1435 | token: apiToken, |
| 1436 | migrated: identityMnemonic !== loadIdentityMnemonic(password) // True if we migrated from plaintext |
| 1437 | }) |
| 1438 | } catch (err) { |
| 1439 | console.error('Unlock error:', err) |
| 1440 | sendError(res, err.message) |
| 1441 | } |
| 1442 | }, |
| 1443 | |
| 1373 | 1444 | // POST /api/init (full initialization: identity + wallet) |
| 1374 | 1445 | 'POST /api/init': async (req, res) => { |
| 1375 | 1446 | try { |