Phase log 9: 2026-06-12 - Full Bug Audit + Fixes
Salvaged from the Session History of
CLAUDE.original.md, a rolling log that held every session before Phase 12 in one file. That file was split up and deleted on 2026-08-25; its pitfalls and decisions live inCLAUDE.md. This entry is its text verbatim - written as a log, not to the handoff template, so it has no Goal/Next Steps sections.
Goal: Comprehensive audit of all TypeScript and Rust code for security vulnerabilities, functional bugs, listener leaks, and unintended behavior. Fix everything found.
Changes Applied
Security: Non-constant-time legacy SHA-256 comparison (vault-core/src/users.rs):
hex::encode(h.finalize()) == hash_hex- regular string comparison, vulnerable to timing attacks on legacy password hashes- Fix: decode
hash_hexto bytes; constant-time XOR-fold comparison:computed.iter().zip(expected.iter()).fold(0u8, |acc,(a,b)| acc | (a^b)) == 0 - No new crate dependency (avoids adding
subtle)
Security: PUT /api/vault auth order (envv-server/src/main.rs):
- Payload validation (api_keys check) ran before
extract_session()- unauthenticated malformed requests got 400 instead of 401 - Fix:
extract_session()moved before api_keys validation
Security: GET /api/status unlocked logic (envv-server/src/main.rs):
!state.sessions.lock().unwrap().is_empty()- reportedunlocked: truewhen only user sessions existed (no owner session)- Fix:
state.sessions.lock().unwrap().values().any(|s| s.is_owner)
Functional: openAdd() listener leak (src/ts/modals.ts):
- Added new
input/changelisteners on every call without removing previous ones - After N modal opens, each form field had N
saveDraftlisteners stacked - multiple sessionStorage writes per keystroke - Fix: module-level stable
_saveDraftfunction +_draftBoundflag; listeners bound once (overlay DOM is permanent)
Functional: showDropdown() / showContextMenu() listener accumulation (src/ts/modals.ts):
onClicklistener added to#dropdownon each call;closelistener added todocument- Item-click set
display:nonebut did NOT calldd.removeEventListener(onClick)ordocument.removeEventListener(close) - Re-clicking anchor element skipped the
closehandler (e.target === anchorElguard), so old listeners never removed - After N opens: N stacked
onClicklisteners on#dropdown, N stackedcloselisteners ondocument - Fix:
_ddCleanupmodule-level ref storing() => { dd.removeEventListener(onClick); document.removeEventListener(close); }. Called on: re-open (removes previous), item-click, outside-click. BothshowDropdownandshowContextMenushare this pattern.
Functional: Local vault switcher doesn't reload (src/ts/vault.ts):
- Switching remote → local set
st.store = new TauriVaultStore()but never calledfinishInit() - Remote vault data stayed visible indefinitely after switching to local
- Fix: clear
st.vault.api_keysandst.vault.user_categories, then callfinishInit()
Functional: st.allExpanded not reset on lock (src/ts/lock.ts):
lockVault()calledst.expanded.clear()but notst.allExpanded = false- After unlock, all cards appeared expanded if the user had activated expand-all before locking
- Fix:
st.allExpanded = falseadded tolockVault()
Functional: Visibility lock fires before vault unlocked (src/ts/state.ts + lock.ts + vault.ts):
- With
autoLockMinutes > 0, minimizing window before completing first unlock calledlockVault('visibility') showRelockScreen()stacked on top ofshowUnlockModal()(two overlays)- Fix:
st.vaultOpen: booleanflag added to global state; settrueafter successfulfinishInit()data load; setfalseinlockVault(); visibility handler checks&& st.vaultOpenbefore locking
Data integrity: Rate-limiter HashMap never pruned (envv-server/src/main.rs):
HashMap<ip, RateBucket>grew unboundedly - IP-rotating attacks could exhaust process memory- Fix:
record_auth_failure()callsbuckets.retain(...)when count >1000, evicting buckets withwindow_startolder than 300s
Consistency: Env field detection gap (chunk-ops.ts + render.ts):
chunk.fields.filter(f => f.description === 'env')missed fields manually added via chunk edit modal withfield_type: 'env_var'- Docker service env fields created by docker-compose import have
description: 'env'; manually-added ones havefield_type: 'env_var' copy-chunk-raw,copy-chunk-envhandlers inrender.tshad the same gapchunkEnvFieldsandisEnvFieldinchunk-ops.tshad the same gap- Fix: all 4 filter sites now check
f.description === 'env' || f.field_type === 'env_var'
Verification
npx tsc --noEmit- exit 0 (clean)cargo check- all three crates (src-tauri, vault-core, envv-server) exit 0 (clean)
What Worked
- TypeScript modular flag patterns (
_draftBound,_ddCleanup) cleanly isolated all listener leaks without restructuring callers - Rust XOR-fold constant-time comparison without adding
subtlecrate st.vaultOpenflag minimal footprint - single bool added tost, set in 3 places
What Didn't Work
Nothing failed. All fixes applied cleanly on first attempt.
Next Steps
cargo tauri dev- end-to-end UI verification- Test dropdown: verify no double-fire after multiple re-clicks on vault-switcher
- Test local vault reload after switching from remote vault
- Consider bumping
tauri.conf.jsonversion to 0.6.1