OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet commits / 0dd446e

Phase 2: Professional password dialog with validation
- Replaced dual prompts with single dialog UI - Both password fields in one modal with real-time validation - Password strength indicator (weak/fair/good/strong) - Instant match validation before submission allowed - Clear warnings for first-time encryption setup - Proper focus management and Enter key support - Prevents mismatched passwords from being submitted Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
0dd446e4a378d0008a2764ca781b268f0237f325
by plankatron <fleeky@gmail.com> · 2026-01-27T20:49:38.000Z

Changed files (3) +250 -94

.claude/tmux-registry.json +1 -1
……@@ -8,7 +8,7 @@
88 "purpose": "Seeding pear://18x3enb54ttcr6uuhg5hh1cqzwi8ydnm658fmgyyjkzoqxd1ybso",
99 "command": "pear seed pear://18x3enb54ttcr6uuhg5hh1cqzwi8ydnm658fmgyyjkzoqxd1ybso .",
1010 "persistent": true,
11 "latest_version": "11451",
11 "latest_version": "11455",
1212 "last_deployed": "2026-01-27",
1313 "features": "Phase 2 COMPLETE: Auth + Encryption + Validation + Rate Limiting + CORS + Signed Invites + Password Unlock UI ✅"
1414 },
ui/app.js +185 -93
……@@ -2194,127 +2194,219 @@ async function checkEncryptionStatus() {
21942194 return null // Unknown status
21952195}
21962196
2197// Prompt for password to unlock wallet
2197// Prompt for password to unlock wallet using dialog
21982198async function promptForPassword() {
2199 let attempts = 0
22002199 const maxAttempts = 3
2200 let attempts = 0
22012201
22022202 // Check if this is first-time password setup or regular unlock
22032203 const isEncrypted = await checkEncryptionStatus()
22042204 const isFirstTimeSetup = isEncrypted === false
22052205
2206 while (attempts < maxAttempts) {
2207 let promptMessage
2208
2209 if (isFirstTimeSetup && attempts === 0) {
2210 // First-time password setup - clear warning
2211 promptMessage = `⚠️ SET WALLET PASSWORD (First Time Setup)
2212
2213This will ENCRYPT your wallet mnemonics with a password.
2214
2215IMPORTANT:
2216• You MUST remember this password
2217• Required every time you access the wallet
2218• Cannot be recovered if forgotten
2219• Minimum 8 characters
2220
2221Enter a strong password:`
2222 } else if (isFirstTimeSetup) {
2223 // First-time setup, retry attempt
2224 promptMessage = `⚠️ SET WALLET PASSWORD
2225
2226Enter a strong password (min 8 characters):
2227
2228(Attempt ${attempts + 1}/${maxAttempts})`
2206 return new Promise((resolve) => {
2207 const dialog = document.getElementById('password-dialog')
2208 const titleEl = document.getElementById('password-dialog-title')
2209 const warningEl = document.getElementById('password-dialog-warning')
2210 const passwordInput = document.getElementById('password-input')
2211 const confirmGroup = document.getElementById('password-confirm-group')
2212 const confirmInput = document.getElementById('password-confirm-input')
2213 const matchError = document.getElementById('password-match-error')
2214 const strengthDiv = document.getElementById('password-strength')
2215 const strengthText = document.getElementById('password-strength-text')
2216 const strengthBar = document.getElementById('password-strength-bar')
2217 const submitBtn = document.getElementById('password-submit-btn')
2218 const cancelBtn = document.getElementById('password-cancel-btn')
2219 const attemptsEl = document.getElementById('password-attempts')
2220
2221 // Configure dialog for first-time setup vs unlock
2222 if (isFirstTimeSetup) {
2223 titleEl.textContent = '⚠️ SET WALLET PASSWORD'
2224 warningEl.style.display = 'block'
2225 confirmGroup.style.display = 'block'
2226 strengthDiv.style.display = 'block'
22292227 } else {
2230 // Regular unlock
2231 promptMessage = `🔐 Unlock Wallet
2232
2233Enter your wallet password:
2234
2235(Attempt ${attempts + 1}/${maxAttempts})`
2228 titleEl.textContent = '🔐 Unlock Wallet'
2229 warningEl.style.display = 'none'
2230 confirmGroup.style.display = 'none'
2231 strengthDiv.style.display = 'none'
22362232 }
22372233
2238 const password = prompt(promptMessage)
2234 // Show attempts counter
2235 if (attempts > 0) {
2236 attemptsEl.textContent = `Attempt ${attempts + 1} of ${maxAttempts}`
2237 attemptsEl.style.display = 'block'
2238 } else {
2239 attemptsEl.style.display = 'none'
2240 }
2241
2242 // Password strength calculator
2243 function checkPasswordStrength(password) {
2244 if (!password) return { strength: 0, text: 'Too short', color: '#d32f2f' }
2245
2246 let strength = 0
2247 if (password.length >= 8) strength += 25
2248 if (password.length >= 12) strength += 25
2249 if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength += 20
2250 if (/\d/.test(password)) strength += 15
2251 if (/[^a-zA-Z0-9]/.test(password)) strength += 15
2252
2253 if (strength < 40) return { strength, text: 'Weak', color: '#d32f2f' }
2254 if (strength < 60) return { strength, text: 'Fair', color: '#ff9800' }
2255 if (strength < 80) return { strength, text: 'Good', color: '#2196f3' }
2256 return { strength, text: 'Strong', color: '#4caf50' }
2257 }
2258
2259 // Validation function
2260 function validateInputs() {
2261 const password = passwordInput.value
2262 const confirm = confirmInput.value
2263
2264 // Update strength indicator (only for first-time setup)
2265 if (isFirstTimeSetup) {
2266 const { strength, text, color} = checkPasswordStrength(password)
2267 strengthText.textContent = text
2268 strengthText.style.color = color
2269 strengthBar.style.width = `${strength}%`
2270 strengthBar.style.background = color
2271 }
22392272
2240 // User cancelled
2241 if (password === null) {
2242 if (attempts === 0) {
2243 return false // First cancellation - let them refresh
2273 // Check password length
2274 if (password.length < 8) {
2275 submitBtn.disabled = true
2276 return
2277 }
2278
2279 // Check password match (only for first-time setup)
2280 if (isFirstTimeSetup) {
2281 if (password !== confirm) {
2282 matchError.style.display = 'block'
2283 submitBtn.disabled = true
2284 return
2285 } else {
2286 matchError.style.display = 'none'
2287 }
22442288 }
2245 continue // Subsequent cancellations - give another chance
2246 }
22472289
2248 // Validate password length
2249 if (password.length < 8) {
2250 alert('❌ Password must be at least 8 characters')
2251 attempts++
2252 continue
2290 // All validations passed
2291 submitBtn.disabled = false
22532292 }
22542293
2255 // For first-time setup, ask for confirmation
2294 // Add input listeners
2295 passwordInput.addEventListener('input', validateInputs)
22562296 if (isFirstTimeSetup) {
2257 const confirmPassword = prompt('🔐 CONFIRM PASSWORD\n\nPlease re-enter your password to confirm:')
2297 confirmInput.addEventListener('input', validateInputs)
2298 }
2299
2300 // Handle form submission
2301 async function handleSubmit() {
2302 const password = passwordInput.value
2303
2304 try {
2305 submitBtn.disabled = true
2306 submitBtn.textContent = 'Submitting...'
2307
2308 // Call unlock endpoint
2309 const res = await fetch(`${API_BASE}/unlock`, {
2310 method: 'POST',
2311 headers: { 'Content-Type': 'application/json' },
2312 body: JSON.stringify({ password })
2313 })
2314
2315 if (!res.ok) {
2316 const error = await res.json()
2317 if (res.status === 401) {
2318 attempts++
2319
2320 if (attempts >= maxAttempts) {
2321 alert('❌ Maximum unlock attempts reached. Please refresh the page to try again.')
2322 dialog.close()
2323 resolve(false)
2324 return
2325 }
2326
2327 // Show error and reset
2328 if (isFirstTimeSetup) {
2329 alert('❌ Failed to set password. Please try again.')
2330 } else {
2331 alert('❌ Invalid password. Please try again.')
2332 }
2333
2334 passwordInput.value = ''
2335 confirmInput.value = ''
2336 submitBtn.textContent = 'Submit'
2337 validateInputs()
2338
2339 // Update attempts counter
2340 attemptsEl.textContent = `Attempt ${attempts + 1} of ${maxAttempts}`
2341 attemptsEl.style.display = 'block'
2342 return
2343 }
2344 throw new Error(error.error || 'Unlock failed')
2345 }
22582346
2259 if (confirmPassword === null) {
2260 continue // Let them try again
2261 }
2347 const data = await res.json()
22622348
2263 if (password !== confirmPassword) {
2264 alert('❌ Passwords do not match. Please try again.')
2265 attempts++
2266 continue
2267 }
2268 }
2349 // Store API token
2350 saveToken(data.token)
22692351
2270 try {
2271 // Call unlock endpoint
2272 const res = await fetch(`${API_BASE}/unlock`, {
2273 method: 'POST',
2274 headers: { 'Content-Type': 'application/json' },
2275 body: JSON.stringify({ password })
2276 })
2352 // Close dialog
2353 dialog.close()
22772354
2278 if (!res.ok) {
2279 const error = await res.json()
2280 if (res.status === 401) {
2281 if (isFirstTimeSetup) {
2282 alert('❌ Failed to set password. Please try again.')
2283 } else {
2284 alert('❌ Invalid password. Please try again.')
2285 }
2286 attempts++
2287 continue
2355 // Show success message
2356 if (data.migrated) {
2357 alert('✅ SUCCESS!\n\nYour wallet mnemonics have been encrypted.\n\nREMEMBER YOUR PASSWORD - you will need it every time you access the wallet!')
2358 showSuccess('✅ Wallet encrypted and unlocked!')
2359 } else {
2360 showSuccess('✅ Wallet unlocked successfully')
22882361 }
2289 throw new Error(error.error || 'Unlock failed')
2290 }
22912362
2292 const data = await res.json()
2363 console.log('🔓 Wallet unlocked, public key:', data.publicKey)
2364 resolve(true)
22932365
2294 // Store API token using the correct function
2295 saveToken(data.token)
2366 } catch (err) {
2367 console.error('Unlock error:', err)
2368 alert(`❌ Error: ${err.message}`)
2369 attempts++
22962370
2297 // Show success message
2298 if (data.migrated) {
2299 alert('✅ SUCCESS!\n\nYour wallet mnemonics have been encrypted.\n\nREMEMBER YOUR PASSWORD - you will need it every time you access the wallet!')
2300 showSuccess('✅ Wallet encrypted and unlocked!')
2301 } else {
2302 showSuccess('✅ Wallet unlocked successfully')
2371 if (attempts >= maxAttempts) {
2372 dialog.close()
2373 resolve(false)
2374 } else {
2375 submitBtn.textContent = 'Submit'
2376 submitBtn.disabled = false
2377 }
23032378 }
2379 }
23042380
2305 console.log('🔓 Wallet unlocked, public key:', data.publicKey)
2306 return true
2307
2308 } catch (err) {
2309 console.error('Unlock error:', err)
2310 alert(`❌ Error: ${err.message}`)
2311 attempts++
2381 // Button handlers
2382 submitBtn.onclick = handleSubmit
2383 cancelBtn.onclick = () => {
2384 dialog.close()
2385 resolve(false)
23122386 }
2313 }
23142387
2315 // Max attempts reached
2316 alert('❌ Maximum unlock attempts reached. Please refresh the page to try again.')
2317 return false
2388 // Enter key submits
2389 passwordInput.addEventListener('keydown', (e) => {
2390 if (e.key === 'Enter' && !submitBtn.disabled) {
2391 handleSubmit()
2392 }
2393 })
2394 confirmInput.addEventListener('keydown', (e) => {
2395 if (e.key === 'Enter' && !submitBtn.disabled) {
2396 handleSubmit()
2397 }
2398 })
2399
2400 // Reset and show dialog
2401 passwordInput.value = ''
2402 confirmInput.value = ''
2403 matchError.style.display = 'none'
2404 submitBtn.textContent = 'Submit'
2405 submitBtn.disabled = true
2406
2407 dialog.showModal()
2408 passwordInput.focus()
2409 })
23182410}
23192411
23202412// Initialize on page load
ui/index.html +64 -0
……@@ -736,6 +736,70 @@
736736 </div>
737737 </dialog>
738738
739 <!-- Password Setup/Unlock Dialog -->
740 <dialog id="password-dialog">
741 <div class="modal-header">
742 <h3 id="password-dialog-title">🔐 Unlock Wallet</h3>
743 </div>
744 <div class="modal-body">
745 <div id="password-dialog-warning" class="warning-box" style="display: none; background: #fff3cd; border: 2px solid #ffc107; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
746 <strong style="color: #856404;">⚠️ IMPORTANT - SET ENCRYPTION PASSWORD:</strong>
747 <ul style="margin: 10px 0; padding-left: 20px; text-align: left; color: #856404;">
748 <li>This password will <strong>ENCRYPT</strong> your wallet mnemonics</li>
749 <li>You <strong>MUST remember</strong> this password</li>
750 <li>Required <strong>every time</strong> you access the wallet</li>
751 <li><strong>Cannot be recovered</strong> if forgotten</li>
752 </ul>
753 </div>
754
755 <div class="form-group">
756 <label for="password-input">Password (min 8 characters):</label>
757 <input
758 type="password"
759 id="password-input"
760 class="input-field"
761 placeholder="Enter password"
762 autocomplete="new-password"
763 style="width: 100%; padding: 10px; font-size: 16px; border: 2px solid #ddd; border-radius: 6px;"
764 />
765 </div>
766
767 <div class="form-group" id="password-confirm-group">
768 <label for="password-confirm-input">Confirm Password:</label>
769 <input
770 type="password"
771 id="password-confirm-input"
772 class="input-field"
773 placeholder="Re-enter password"
774 autocomplete="new-password"
775 style="width: 100%; padding: 10px; font-size: 16px; border: 2px solid #ddd; border-radius: 6px;"
776 />
777 </div>
778
779 <div id="password-match-error" style="color: #d32f2f; font-weight: bold; display: none; margin-top: 10px;">
780 ❌ Passwords do not match
781 </div>
782
783 <div id="password-strength" style="margin-top: 15px; display: none;">
784 <div style="display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 14px;">
785 <span>Strength:</span>
786 <span id="password-strength-text" style="font-weight: bold;">Weak</span>
787 </div>
788 <div style="height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden;">
789 <div id="password-strength-bar" style="height: 100%; width: 0%; transition: width 0.3s, background 0.3s;"></div>
790 </div>
791 </div>
792
793 <div class="button-row" style="margin-top: 25px; display: flex; gap: 10px; justify-content: flex-end;">
794 <button id="password-cancel-btn" class="secondary-btn" style="padding: 10px 20px; font-size: 16px;">Cancel</button>
795 <button id="password-submit-btn" class="primary-btn" disabled style="padding: 10px 20px; font-size: 16px;">Submit</button>
796 </div>
797
798 <div id="password-attempts" style="margin-top: 15px; color: #666; font-size: 0.9em; text-align: center;">
799 </div>
800 </div>
801 </dialog>
802
739803 <!-- Error Toast -->
740804 <div id="error-toast" class="toast"></div>
741805