Decompose: Feature: "Little Black Book" mode — locked private vault for discreet contacts #26

Closed
opened 2026-05-28 00:04:38 +00:00 by Mark · 4 comments
Owner

Decompose: Feature: "Little Black Book" mode — locked private vault for discreet contacts

Context

Parent: #26. Decision (2026-06-28): unlock method is PIN or biometric, user-pickable, PIN set on first setup. Implementation shipped at #121 (commit de8436f): unlock UI, vault-crypto.ts key derivation, 5-attempt lockout / 30s cooldown. Auto-exit gesture closed at #77.

What's still missing against the original #26 spec: enrollment flow (first-launch), separate Dexie kin_vault DB (currently a shared DB with an inVault flag), contact-isolation plumbing (move in/out + exclusion from main views), discreet entry point, auto-lock-on-background timer, and forensic-trace verification.

Repo: Mark/kimi_agent_kin_app_sync. Stack: React + Capacitor + Dexie + Zustand. Vault surface: src/vault/Vault.tsx, src/vault/vault-crypto.ts, src/db.ts, src/types.ts.

Out of scope (do NOT absorb into these sub-issues)

  • Cloud sync of vault (original spec says explicitly "not synced").
  • Panic-PIN / emergency wipe (separate, post-MVP).
  • Per-vault notification preferences (post-MVP).
  • iOS-vault-on-iCloud parity (separate issue once iOS ambient gap is closed).

Per-phase ordering rule

Data dependency, not feature size. Phase N consumes Phase N-1's output. Pipeline can dispatch each phase independently once the previous lands.


P1: Vault DB + store split (separate Dexie instance)

Goal: Stand up kin_vault as a distinct Dexie database. Vault data NEVER lives in the main kin_db. The main DB has zero knowledge of vault contacts. Future readers cannot infer vault existence from main-DB shape.

Approach candidates:

  • New file src/vault/vault-db.ts exporting a vaultDb instance with name kin_vault, version 1. Schema mirrors main Person / Interaction / Gift / Date tables but scoped to the vault.
  • vaultDb is constructed lazily inside VaultStore, never imported by db.ts or schemas.ts.

Outputs:

  • src/vault/vault-db.ts with tables: vaultPeople, vaultInteractions, vaultGifts, vaultDates, vaultNotes.
  • src/vault/VaultStore.ts skeleton wrapping all vault queries; rejects reads unless unlock state is "unlocked" in-memory.

Acceptance criteria:

  • kin_vault Dexie DB created with name kin_vault, separate from kin_db.
  • No import of vault-db from src/db.ts, src/schemas.ts, or any main-DB module.
  • VaultStore exports isUnlocked(): boolean; any query called while locked throws VaultLockedError.
  • Unit test proves kin_db (Inspect) has no vaultPeople table; kin_vault (Inspect) has no people table.

Files touched: src/vault/vault-db.ts (new), src/vault/VaultStore.ts (new), src/db.ts (untouched, verify no import path), new unit tests under src/vault/__tests__/.


P2: Vault enrollment + move-in/move-out plumbing

Goal: First-launch path enrolls a PIN (or biometric) before the vault is usable. Existing contacts can be moved in/out via PersonDetail. Main DB people flagged vault-only via a foreign-key reference to the vault row id; the main Person row never carries vault data.

Approach candidates:

  • Enrollment screen reached on first vault entry: "Set up vault — choose primary factor." Routes to PIN entry or Capacitor Biometrics enrollment.
  • Stored preference vault.primaryFactor = "pin" | "biometric" in app settings.
  • PersonDetail adds "Move to Little Black Book" and "Remove from Little Black Book" actions which call VaultStore.moveFromMain(personId) / VaultStore.moveToMain(personId). Move is a real cross-DB write, not a flag flip.

Outputs:

  • Enrollment UI in or adjacent to Vault.tsx.
  • vault.primaryFactor setting with default pin.
  • VaultStore.moveFromMain(personId) and moveToMain(personId).

Acceptance criteria:

  • First vault entry shows enrollment screen before unlock UI.
  • User can pick biometric or PIN as primary; choice persisted.
  • "Move to vault" copies the contact (phones, emails, notes) into kin_vault and deletes from kin_db.
  • "Move to main" copies from kin_vault to kin_db and deletes the vault row.
  • Round-trip preserves all fields.
  • Test: after move-to-vault, PersonDetail for that person is inaccessible from the main app until vault is unlocked AND user explicitly moves back.

Files touched: src/vault/Vault.tsx (enrollment sub-screen), src/vault/VaultStore.ts (move methods), src/screens/PersonDetail.tsx (move actions).


P3: Contact isolation — keep vault contacts out of main UI

Goal: Vault contacts, their interactions, gifts, and dates must not appear in the main app's People page, search, Dashboard, or analytics. No visible hint that the vault exists while locked.

Approach candidates:

  • All main-app list / search / aggregate queries are routed through a single mainPersonQuery() helper. The helper itself reads only kin_db; it cannot reach into kin_vault even if the vault is unlocked.
  • No global search index includes vault-side data.
  • Dashboard tiles explicitly exclude interactionCount / giftCount derived from vault tables.

Outputs:

  • src/db.ts query helpers (or wrapper) that guarantee vault data is unreachable from the main app.
  • Update to src/screens/People.tsx, src/screens/Search.tsx, src/screens/Dashboard.tsx, src/screens/Analytics.tsx if any of them aggregated over interactions.

Acceptance criteria:

  • With vault locked or unlocked, main People page shows zero vault contacts.
  • Search results contain zero vault contacts or their interaction text.
  • Dashboard tiles (total interactions, gift counts, upcoming dates) reflect only main-DB data.
  • Test asserting all four surfaces with a seed of 5 vault contacts + 5 main contacts → main surfaces show 5.

Files touched: query layer in src/db.ts, the four surface screens.


P4: Discreet entry point + auto-lock timer

Goal: Vault entry is not labeled "Little Black Book" or "Vault." Auto-lock when the app goes to background, with configurable delay.

Approach candidates:

  • Settings icon long-press (≥ 800ms) opens vault enrollment/unlock.
  • App-pause listener (Capacitor App.addListener("appStateChange", …)) records background timestamp. On resume, if elapsed > delay and vault is unlocked, lock it.
  • User setting vault.autoLockDelay = "immediate" | "1min" | "5min". Default 1min.

Outputs:

  • Long-press handler on Settings icon → vault entry.
  • App background/resume wiring in src/vault/lifecycle.ts.
  • Setting UI in src/screens/Settings.tsx (delay radio).

Acceptance criteria:

  • Settings icon long-press opens vault entry. Short tap still opens Settings.
  • Setting unlock → background the app for 65s → resume → vault is locked.
  • Setting immediate → background → resume → vault locked.
  • Setting 5min → background 30s → resume → vault still unlocked.
  • Test: simulate appStateChange events with faked timestamps; verify unlock-state transitions.

Files touched: src/screens/Settings.tsx, src/App.tsx (lifecycle wiring), src/vault/lifecycle.ts (new).


P5: Forensic-trace verification + close-out

Goal: Verify and harden the "no trace of vault existence" claim. Close out #26 once all phases P1-P4 land and this verification passes.

Approach candidates:

  • Grep-driven test that asserts no vault-named key appears in Preferences / localStorage while vault is locked.
  • Test asserts kin_db schema does not contain vault tables (re-verifies P1).
  • Test asserts no console errors leak vault existence to logs.

Outputs:

  • src/vault/__tests__/forensic-trace.test.ts (new).

Acceptance criteria:

  • Test passes against a fresh dev install.
  • Manual QA on Android emulator + iOS simulator (without enrolled vault, then with) — no vault keys visible in Capacitor Preferences UI or Safari web inspector storage panel.
  • All of P1-P4 merged on main, each with passing test suite.
  • Comment on closed P1-P4 issues linking to P5 verification. Close #26 once all five phases are closed.

Files touched: new test file only; verification run as a script scripts/verify-vault-trace.sh.


Per-phase deliverable shape

Each phase ships as one PR with:

  • Implementation
  • Tests (Vitest for unit, Playwright for UI where applicable)
  • No new lint errors (npm run lint clean)
  • One commit per phase, one push to main, one issue close
Decompose: Feature: "Little Black Book" mode — locked private vault for discreet contacts ## Context Parent: #26. Decision (2026-06-28): unlock method is PIN or biometric, user-pickable, PIN set on first setup. Implementation shipped at #121 (commit `de8436f`): unlock UI, `vault-crypto.ts` key derivation, 5-attempt lockout / 30s cooldown. Auto-exit gesture closed at #77. What's still missing against the original #26 spec: enrollment flow (first-launch), **separate Dexie `kin_vault` DB** (currently a shared DB with an `inVault` flag), contact-isolation plumbing (move in/out + exclusion from main views), discreet entry point, auto-lock-on-background timer, and forensic-trace verification. Repo: Mark/kimi_agent_kin_app_sync. Stack: React + Capacitor + Dexie + Zustand. Vault surface: `src/vault/Vault.tsx`, `src/vault/vault-crypto.ts`, `src/db.ts`, `src/types.ts`. ## Out of scope (do NOT absorb into these sub-issues) - Cloud sync of vault (original spec says explicitly "not synced"). - Panic-PIN / emergency wipe (separate, post-MVP). - Per-vault notification preferences (post-MVP). - iOS-vault-on-iCloud parity (separate issue once iOS ambient gap is closed). ## Per-phase ordering rule Data dependency, not feature size. Phase N consumes Phase N-1's output. Pipeline can dispatch each phase independently once the previous lands. --- ### P1: Vault DB + store split (separate Dexie instance) **Goal:** Stand up `kin_vault` as a distinct Dexie database. Vault data NEVER lives in the main `kin_db`. The main DB has zero knowledge of vault contacts. Future readers cannot infer vault existence from main-DB shape. **Approach candidates:** - New file `src/vault/vault-db.ts` exporting a `vaultDb` instance with name `kin_vault`, version 1. Schema mirrors main Person / Interaction / Gift / Date tables but scoped to the vault. - `vaultDb` is constructed lazily inside `VaultStore`, never imported by `db.ts` or `schemas.ts`. **Outputs:** - `src/vault/vault-db.ts` with tables: `vaultPeople`, `vaultInteractions`, `vaultGifts`, `vaultDates`, `vaultNotes`. - `src/vault/VaultStore.ts` skeleton wrapping all vault queries; rejects reads unless unlock state is "unlocked" in-memory. **Acceptance criteria:** - [ ] `kin_vault` Dexie DB created with name `kin_vault`, separate from `kin_db`. - [ ] No import of `vault-db` from `src/db.ts`, `src/schemas.ts`, or any main-DB module. - [ ] `VaultStore` exports `isUnlocked(): boolean`; any query called while locked throws `VaultLockedError`. - [ ] Unit test proves `kin_db` (Inspect) has no `vaultPeople` table; `kin_vault` (Inspect) has no `people` table. **Files touched:** `src/vault/vault-db.ts` (new), `src/vault/VaultStore.ts` (new), `src/db.ts` (untouched, verify no import path), new unit tests under `src/vault/__tests__/`. --- ### P2: Vault enrollment + move-in/move-out plumbing **Goal:** First-launch path enrolls a PIN (or biometric) before the vault is usable. Existing contacts can be moved in/out via PersonDetail. Main DB people flagged vault-only via a foreign-key reference to the vault row id; the main Person row never carries vault data. **Approach candidates:** - Enrollment screen reached on first vault entry: "Set up vault — choose primary factor." Routes to PIN entry or Capacitor Biometrics enrollment. - Stored preference `vault.primaryFactor = "pin" | "biometric"` in app settings. - PersonDetail adds "Move to Little Black Book" and "Remove from Little Black Book" actions which call `VaultStore.moveFromMain(personId)` / `VaultStore.moveToMain(personId)`. Move is a real cross-DB write, not a flag flip. **Outputs:** - Enrollment UI in or adjacent to `Vault.tsx`. - `vault.primaryFactor` setting with default `pin`. - `VaultStore.moveFromMain(personId)` and `moveToMain(personId)`. **Acceptance criteria:** - [ ] First vault entry shows enrollment screen before unlock UI. - [ ] User can pick biometric or PIN as primary; choice persisted. - [ ] "Move to vault" copies the contact (phones, emails, notes) into `kin_vault` and deletes from `kin_db`. - [ ] "Move to main" copies from `kin_vault` to `kin_db` and deletes the vault row. - [ ] Round-trip preserves all fields. - [ ] Test: after move-to-vault, PersonDetail for that person is inaccessible from the main app until vault is unlocked AND user explicitly moves back. **Files touched:** `src/vault/Vault.tsx` (enrollment sub-screen), `src/vault/VaultStore.ts` (move methods), `src/screens/PersonDetail.tsx` (move actions). --- ### P3: Contact isolation — keep vault contacts out of main UI **Goal:** Vault contacts, their interactions, gifts, and dates must not appear in the main app's People page, search, Dashboard, or analytics. No visible hint that the vault exists while locked. **Approach candidates:** - All main-app list / search / aggregate queries are routed through a single `mainPersonQuery()` helper. The helper itself reads only `kin_db`; it cannot reach into `kin_vault` even if the vault is unlocked. - No global search index includes vault-side data. - Dashboard tiles explicitly exclude `interactionCount` / `giftCount` derived from vault tables. **Outputs:** - `src/db.ts` query helpers (or wrapper) that guarantee vault data is unreachable from the main app. - Update to `src/screens/People.tsx`, `src/screens/Search.tsx`, `src/screens/Dashboard.tsx`, `src/screens/Analytics.tsx` if any of them aggregated over interactions. **Acceptance criteria:** - [ ] With vault locked or unlocked, main People page shows zero vault contacts. - [ ] Search results contain zero vault contacts or their interaction text. - [ ] Dashboard tiles (total interactions, gift counts, upcoming dates) reflect only main-DB data. - [ ] Test asserting all four surfaces with a seed of 5 vault contacts + 5 main contacts → main surfaces show 5. **Files touched:** query layer in `src/db.ts`, the four surface screens. --- ### P4: Discreet entry point + auto-lock timer **Goal:** Vault entry is not labeled "Little Black Book" or "Vault." Auto-lock when the app goes to background, with configurable delay. **Approach candidates:** - Settings icon long-press (≥ 800ms) opens vault enrollment/unlock. - App-pause listener (Capacitor `App.addListener("appStateChange", …)`) records background timestamp. On resume, if elapsed > delay and vault is unlocked, lock it. - User setting `vault.autoLockDelay = "immediate" | "1min" | "5min"`. Default `1min`. **Outputs:** - Long-press handler on Settings icon → vault entry. - `App` background/resume wiring in `src/vault/lifecycle.ts`. - Setting UI in `src/screens/Settings.tsx` (delay radio). **Acceptance criteria:** - [ ] Settings icon long-press opens vault entry. Short tap still opens Settings. - [ ] Setting unlock → background the app for 65s → resume → vault is locked. - [ ] Setting `immediate` → background → resume → vault locked. - [ ] Setting `5min` → background 30s → resume → vault still unlocked. - [ ] Test: simulate `appStateChange` events with faked timestamps; verify unlock-state transitions. **Files touched:** `src/screens/Settings.tsx`, `src/App.tsx` (lifecycle wiring), `src/vault/lifecycle.ts` (new). --- ### P5: Forensic-trace verification + close-out **Goal:** Verify and harden the "no trace of vault existence" claim. Close out #26 once all phases P1-P4 land and this verification passes. **Approach candidates:** - Grep-driven test that asserts no `vault`-named key appears in `Preferences` / localStorage while vault is locked. - Test asserts `kin_db` schema does not contain vault tables (re-verifies P1). - Test asserts no console errors leak vault existence to logs. **Outputs:** - `src/vault/__tests__/forensic-trace.test.ts` (new). **Acceptance criteria:** - [ ] Test passes against a fresh dev install. - [ ] Manual QA on Android emulator + iOS simulator (without enrolled vault, then with) — no vault keys visible in Capacitor Preferences UI or Safari web inspector storage panel. - [ ] All of P1-P4 merged on main, each with passing test suite. - [ ] Comment on closed P1-P4 issues linking to P5 verification. Close #26 once all five phases are closed. **Files touched:** new test file only; verification run as a script `scripts/verify-vault-trace.sh`. --- ## Per-phase deliverable shape Each phase ships as one PR with: - Implementation - Tests (Vitest for unit, Playwright for UI where applicable) - No new lint errors (`npm run lint` clean) - One commit per phase, one push to main, one issue close
Author
Owner

⏸ Needs Decision

Blocking factors:

  • Didn't meet auto-approve criteria — scope=medium, risk=medium, spec=aligned

Scope: medium Risk: medium Spec: aligned Type: feature
Summary: Add a PIN/biometric-locked private vault for discreet contacts
Suggestion: Implement a separate IndexedDB database for vault contacts, add auth screens, and isolate vault UI from main app
Rationale: Medium scope due to new auth flow, separate data layer, and UI isolation. Medium risk because it touches core data paths and requires careful isolation. Aligned with project goals of privacy and relationship management. Feature type as it adds new functionality. Suggested approach focuses on data isolation and auth first, then UI integration.

Action: Review each blocking factor above. Remove blocked:needs-decision label to route back to triage.

## ⏸ Needs Decision **Blocking factors:** - **Didn't meet auto-approve criteria** — scope=medium, risk=medium, spec=aligned --- **Scope:** medium **Risk:** medium **Spec:** aligned **Type:** feature **Summary:** Add a PIN/biometric-locked private vault for discreet contacts **Suggestion:** Implement a separate IndexedDB database for vault contacts, add auth screens, and isolate vault UI from main app **Rationale:** Medium scope due to new auth flow, separate data layer, and UI isolation. Medium risk because it touches core data paths and requires careful isolation. Aligned with project goals of privacy and relationship management. Feature type as it adds new functionality. Suggested approach focuses on data isolation and auth first, then UI integration. **Action:** Review each blocking factor above. Remove `blocked:needs-decision` label to route back to triage.
Author
Owner

GSD VERIFY phase found no changes after implementation.
Worker output: ]11;?\API call failed after 3 retries: HTTP 429: Rate limit exceeded for GLM-5. Retry in 11s.

GSD VERIFY phase found no changes after implementation. Worker output: ]11;?\API call failed after 3 retries: HTTP 429: Rate limit exceeded for GLM-5. Retry in 11s.
Author
Owner

Decision (2026-06-28)

Vault unlock method: PIN or biometric, user-pickable. The unlock screen must support both, with whichever the user selects as the primary factor and the other as a fallback if registered.

Existing vault-crypto.ts and Vault.tsx need an unlock UI tied to this decision. Implementation issue to be filed next.

## Decision (2026-06-28) Vault unlock method: PIN or biometric, user-pickable. The unlock screen must support both, with whichever the user selects as the primary factor and the other as a fallback if registered. Existing `vault-crypto.ts` and `Vault.tsx` need an unlock UI tied to this decision. Implementation issue to be filed next.
Mark changed title from Feature: "Little Black Book" mode — locked private vault for discreet contacts to Decompose: Feature: "Little Black Book" mode — locked private vault for discreet contacts 2026-07-14 00:47:52 +00:00
Author
Owner

This issue has been decomposed into the following sub-issues:

# Title Phase Scope
#139 Decompose: Feature: "Little Black Book" mode — locked privat Goal: Stand up kin_vault as a distinct Dexie database. Vault data NEVER li
#140 Decompose: Feature: "Little Black Book" mode — locked privat Goal: First-launch path enrolls a PIN (or biometric) before the vault is usa
#141 Decompose: Feature: "Little Black Book" mode — locked privat Goal: Vault contacts, their interactions, gifts, and dates must not appear i
#142 Decompose: Feature: "Little Black Book" mode — locked privat Goal: Vault entry is not labeled "Little Black Book" or "Vault." Auto-lock w
#143 Decompose: Feature: "Little Black Book" mode — locked privat Goal: Verify and harden the "no trace of vault existence" claim. Close out #

Each sub-issue has been labeled with triage-approved and is ready for queue routing.

This issue has been decomposed into the following sub-issues: | # | Title | Phase Scope | |---|-------|-------------| | #139 | Decompose: Feature: "Little Black Book" mode — locked privat | **Goal:** Stand up `kin_vault` as a distinct Dexie database. Vault data NEVER li | | #140 | Decompose: Feature: "Little Black Book" mode — locked privat | **Goal:** First-launch path enrolls a PIN (or biometric) before the vault is usa | | #141 | Decompose: Feature: "Little Black Book" mode — locked privat | **Goal:** Vault contacts, their interactions, gifts, and dates must not appear i | | #142 | Decompose: Feature: "Little Black Book" mode — locked privat | **Goal:** Vault entry is not labeled "Little Black Book" or "Vault." Auto-lock w | | #143 | Decompose: Feature: "Little Black Book" mode — locked privat | **Goal:** Verify and harden the "no trace of vault existence" claim. Close out # | Each sub-issue has been labeled with `triage-approved` and is ready for queue routing.
Mark closed this issue 2026-07-14 00:48:06 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Mark/kimi_agent_kin_app_sync#26
No description provided.