OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet commits / de53f56

Phase 2: Add CORS network hosts + password unlock UI
- Added ashatron.local and tabertron.local to CORS whitelist - Created POST /api/unlock endpoint for password caching - Automatic migration of plaintext mnemonics to encrypted format - Web UI now prompts for password on startup if wallet initialized - Password cached in server memory for session duration - Fixes network testing with --network flag Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
de53f56b6c01e024fe70db85c8810c44bf70f200
by plankatron <fleeky@gmail.com> · 2026-01-27T20:32:11.000Z

Changed files (2) +150 -1

ui/app.js +78 -0
……@@ -2180,6 +2180,74 @@ function exportTransactions() {
21802180 showSuccess(`Exported ${allTransactions.length} transactions`)
21812181}
21822182
2183// Prompt for password to unlock wallet
2184async function promptForPassword() {
2185 let attempts = 0
2186 const maxAttempts = 3
2187
2188 while (attempts < maxAttempts) {
2189 const password = prompt(`🔐 Unlock Wallet\n\nEnter your password to access the wallet:\n\n(Attempt ${attempts + 1}/${maxAttempts})`)
2190
2191 // User cancelled
2192 if (password === null) {
2193 if (attempts === 0) {
2194 return false // First cancellation - let them refresh
2195 }
2196 continue // Subsequent cancellations - give another chance
2197 }
2198
2199 // Validate password length
2200 if (password.length < 8) {
2201 alert('❌ Password must be at least 8 characters')
2202 attempts++
2203 continue
2204 }
2205
2206 try {
2207 // Call unlock endpoint
2208 const res = await fetch(`${API_BASE}/unlock`, {
2209 method: 'POST',
2210 headers: { 'Content-Type': 'application/json' },
2211 body: JSON.stringify({ password })
2212 })
2213
2214 if (!res.ok) {
2215 const error = await res.json()
2216 if (res.status === 401) {
2217 alert('❌ Invalid password. Please try again.')
2218 attempts++
2219 continue
2220 }
2221 throw new Error(error.error || 'Unlock failed')
2222 }
2223
2224 const data = await res.json()
2225
2226 // Store API token
2227 localStorage.setItem('apiToken', data.token)
2228
2229 // Show success message
2230 if (data.migrated) {
2231 showSuccess('✅ Wallet unlocked and mnemonics encrypted!')
2232 } else {
2233 showSuccess('✅ Wallet unlocked successfully')
2234 }
2235
2236 console.log('🔓 Wallet unlocked, public key:', data.publicKey)
2237 return true
2238
2239 } catch (err) {
2240 console.error('Unlock error:', err)
2241 alert(`❌ Error: ${err.message}`)
2242 attempts++
2243 }
2244 }
2245
2246 // Max attempts reached
2247 alert('❌ Maximum unlock attempts reached. Please refresh the page to try again.')
2248 return false
2249}
2250
21832251// Initialize on page load
21842252async function init() {
21852253 console.log('🚀 Hyper Wallet UI initialized')
……@@ -2197,6 +2265,16 @@ async function init() {
21972265
21982266 const initialized = await checkStatus()
21992267
2268 // If initialized but no API token, prompt for password
2269 if (initialized && !localStorage.getItem('apiToken')) {
2270 console.log('🔐 Wallet initialized but password needed')
2271 const success = await promptForPassword()
2272 if (!success) {
2273 showError('Failed to unlock wallet. Please refresh and try again.')
2274 return
2275 }
2276 }
2277
22002278 // Populate currency selector from SUPPORTED_CHAINS
22012279 const currencySelector = document.getElementById('currency-selector')
22022280 if (currencySelector) {
wallet-server.cjs +72 -1
……@@ -309,7 +309,9 @@ function withRateLimit(handler) {
309309const ALLOWED_ORIGINS = [
310310 'http://localhost:4004',
311311 '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
313315 // Add production origins here when deploying:
314316 // 'https://your-production-domain.com'
315317]
……@@ -1370,6 +1372,75 @@ const routes = {
13701372 }
13711373 }),
13721374
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
13731444 // POST /api/init (full initialization: identity + wallet)
13741445 'POST /api/init': async (req, res) => {
13751446 try {