From 4686a2c200b1246db71ad593cdaabbc54986b461 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 25 Jun 2026 18:48:37 -0500 Subject: [PATCH] =?UTF-8?q?docs(seed):=20replace=20question=20guides=20wit?= =?UTF-8?q?h=20v2=20=E2=80=94=20content=20guide,=20rewrite=20plan,=20new?= =?UTF-8?q?=20quality=20checklist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ClaudeQACoverage.md | 30 + ClaudeQAPlan.md | 32 +- ClaudeReport.md | 19 + Future.md | 11 + seed/questions/QUESTION_CONTENT_GUIDE.md | 967 ++++--------------- seed/questions/QUESTION_QUALITY_CHECKLIST.md | 230 +++++ seed/questions/QUESTION_REWRITE_PLAN.md | 218 ++--- seed/questions/QUESTION_SCHEMA.md | 2 + 8 files changed, 612 insertions(+), 897 deletions(-) create mode 100644 seed/questions/QUESTION_QUALITY_CHECKLIST.md diff --git a/ClaudeQACoverage.md b/ClaudeQACoverage.md index 810a0a67..d291b93e 100644 --- a/ClaudeQACoverage.md +++ b/ClaudeQACoverage.md @@ -1,6 +1,7 @@ # Claude QA Coverage Matrix > Resume anchor. Status: `todo | pass | fail(→id) | n/a`. See `ClaudeReport.md` run-state header for current position. +> **Round 6 (branding + Future.md regression) COMPLETE 2026-06-25, client `f47c8e2`:** new surfaces from `95cad84` (white-keyhole icons, animated chip+fill loader, splash, pairing hero) + `f47c8e2` (inclusive gender, turn copy, push-budget split, results-suppression `ActiveGameSessionMonitor`, paywall retry/offline/hide-Continue, auth rotator). **0 new issues; still 0 open P0–P3.** Live: loader (both themes), splash→handoff, launcher icon, ToT+How Well open (no crash → #4 VM injection sound), paywall purchase screen (friendly error + Try again + Continue hidden, online→generic msg), onboarding illustration. Unit tests green. Gender step / rotator / turn-copy / results-timing / weekly-cap = code+unit-verified (live deferred: fragile multi-text-field & 2-device timing; low risk over proven patterns). Baseline restored (QA re-signed-in via admin token; couple intact). > **Round 5 (functions deploy + expanded re-QA) COMPLETE 2026-06-25, client `765916a` + functions DEPLOYED:** E-OBS FIXED+DEPLOYED (12 senders set channelId; chat push → `partner_activity` live) + E-003 results-ready FIXED+DEPLOYED (finished-game → per-session results). **0 open P0–P3.** New Pass G (account creation + fake-account) clean. Varied gameplay (Standard/Deep, 0-match) + nav fuzzing — no new bugs. Baseline restored (couple intact, throwaway deleted, Sam re-paired). > _Round 4: E-003 + B-004 (P2) + A-OBS (P3) FIXED + verified live._ @@ -62,6 +63,19 @@ _Deferred (nav-drift; standard list/detail, lower-risk): Question Packs detail, Answer Reveal (sealed), Date Builder/Plan Date, fresh-account auth/onboarding/pairing._ ## Pass D — Security & Encryption (D1–D6) +**R7 DEEP DIVE (multi-angle, 2026-06-25):** **D1 at-rest — CLEAN (admin ground-truth read):** messages `text` + +`lastMessagePreview`, all 4 game-answer collections (`this_or_that`/`how_well`/`desire_sync`/`wheel`, both users), +capsule title+content, `date_swipes.actions` = `enc:v1:`; `wrappedCoupleKey` = ciphertext (recovery-phrase-wrapped, +**argon2id**); `encryptedRecoveryPhrase` server-blind + **wiped on acceptance** (confirmed absent on accepted invite); +plaintext `inviteCode` **not exploitable** (no code-encrypted secret persists; `/invites/{code}` readable only by +inviter). **D3 raw-API negative (LIVE, executed — no longer deferred):** non-member ID token (Identity Toolkit +`signInWithCustomToken`) → Firestore REST on couple doc/conversation/messages/answers/session/capsules/partner-profile += **all `403 PERMISSION_DENIED`**; non-member writes (couple doc, partner entitlement, **real path +`users/{uid}/entitlements/premium`**) = **all `403` → no self-grant**. Member token reads `200` (characterizes layer: +**App Check not enforced on Firestore — rules are the sole gate, and they hold**). Only writable = cosmetic own-doc +fields (`plan`) that **no gate reads**. **No P0/P1 security findings.** Two hardening notes → `Future.md`. + + **R3:** D2 deployed rules re-audited ✅ (B-001 sessions + D-001 capsules/challenges fixes present; hasPremium + entitlements server-only; ciphertext enforced; no catch-all). D1 at-rest ✅ (chat text + lastMessagePreview = `enc:v1:`; how_well answers + capsules = `enc:v1:`). D4/D5/D6 unchanged since R1 (code identical) → hold. @@ -69,6 +83,22 @@ entitlements server-only; ciphertext enforced; no catch-all). D1 at-rest ✅ (ch statically member-scoped). No P0/P1 security findings. ## Pass E — Notifications (17 types × {foreground, background, killed} + tap-to-open) +**R6 live (games + messages, 2026-06-25, build `f47c8e2`):** full live two-device run. +- **chat_message** ✅ end-to-end: Sam→QA (QA bg) posts on **channel=partner_activity**, title "Sam sent a message" + (partner name, not private), body "Tap to read and reply." — **message text NOT in payload** (privacy holds); + small icon = white monochrome mark; tap→**main conversation with content** (verified via the exact intent — + shade-tap is flaky in the adb harness, lands on launcher, but the contentIntent routing is sound). +- **partner_started_game** ✅: QA started This or That → Sam (bg) posts on **channel=game_activity**, "QA is playing / + QA has started a game. Tap to join!" (content-free); tap→**joins the active session** (same 1/5 prompt). +- **partner_finished_game / results** ✅: both finished → **results push DELIVERED to backgrounded QA** (Round 5 + couldn't confirm this live) on **channel=game_activity**, "Sam finished the game / Sam finished — tap to see your + results!" (content-free); tap→**per-session This or That results** (5/5), per E-003. +- **#4 results-suppression** ✅: Sam stayed foreground on the session throughout → received **0** notifications + (the partner_completed_part + partner_finished_game pushes to Sam were suppressed by ActiveGameSessionMonitor), + while backgrounded QA got the results push. Clean confirmation of both delivery + suppression. +- No FATAL either device; baseline tidy (0 active sessions, couple intact). **No issues found.** + + **R3 live:** FCM tokens valid for both. **chat_message ✅ full chain** (bg deliver + content-free + tap→exact conversation w/ content). **partner_started_game**: bg deliver + content-free ✅; tap→Play hub (not the game) = **E-003 (P2)**. **E-OBS (P3)**: bg pushes use fcm_fallback channel. date_match live-verified R2-B2. E-001/E-002 fixes diff --git a/ClaudeQAPlan.md b/ClaudeQAPlan.md index f60156d3..82c555bd 100644 --- a/ClaudeQAPlan.md +++ b/ClaudeQAPlan.md @@ -81,6 +81,30 @@ confirms + enumerates this; the fix phase applies couple-shared everywhere. - **Test-data hygiene:** keep known test accounts; clean up artifacts (stray messages/reactions/sessions) between rounds so they don't masquerade as bugs. +## Multi-angle attack mandate (go DEEPER than "does the happy path work") +A capability can pass via the UI yet fail when hit directly. Probe each meaningful capability (read/write a private +field, gate a premium feature, deliver/route a notification, start/finish a game, pair/unpair, create an account) +from as many **independent angles** as apply — not just the in-app happy path: +- **Real UI** (play-as-user) — the baseline angle. +- **Crafted intent / deep-link** — fire the exact intent a notification/link carries (bypasses UI nav) to test routing + in isolation; also send **malformed/missing extras** → must route gracefully or no-op, never crash. +- **Raw API against the DEPLOYED backend** — hit Firestore/Storage/Functions REST **directly** with a real token, + as a **member AND a non-member**, to exercise rules + App Check from OUTSIDE the app. A non-member (or no-App-Check) + request must be **DENIED** — App Check `403` or rules `PERMISSION_DENIED`. The member request characterizes which + layer enforces. **Any unauthorized `200` returning couple data = P0.** +- **Admin inspection (ground truth)** — read the RAW stored docs/objects (admin bypasses rules) to assert what is + actually persisted: ciphertext only, no plaintext, no raw keys/invite-seeds, no private content in pushes. +- **Concurrency / race** — two partners (or two rapid taps) hit the same thing at once. +- **Killed / cold state** — force-stop, then deliver + tap a notification; cold-start straight onto a deep link. +- **Malformed / abusive input** — oversized, empty, rapid-fire, injection-ish, forged FCM payloads, replayed/expired + tokens & invite codes. +- **Offline / flaky** — drop network mid-action → graceful failure, recover on reconnect. + +Record **which angles** were tried per area in `ClaudeQACoverage.md`. For security- or data-sensitive capabilities, +"UI happy path only" is **not** a `pass`. **D3/Pass G negative access MUST be executed live via the raw-API angle each +round — never deferred to "only 2 emulators."** (Mint a token for a non-member UID via admin → exchange for an ID +token via the Identity Toolkit REST `signInWithCustomToken` → use it as Bearer against the Firestore REST API.) + ## Continuity & resumability (this effort WILL span many context windows — don't lose state) State lives in **files**, not memory: - **`ClaudeReport.md`** = the issue log (committed). Each issue row is **self-contained in text** (repro + expected @@ -230,8 +254,12 @@ Account); Paywall; Your Progress/Activity; Recovery. field, immutability, **no premium self-grant**, entitlements write:false; re-audit conversations/typing/reactions + entitlement partner-read; **no catch-all** `match /{document=**}`; list/query not enumerable; `get()`-rules don't over-expose; **no legacy plaintext/downgrade path** (`coupleEncryptionEnabled` holds; no disabled-encryption branch). -- **D3 Negative access tests:** a **non-member** account is *denied* reading messages/answers/dates/entitlements, - writing plaintext to encrypted fields, self-granting premium, cross-couple access (live rules or rules-emulator). +- **D3 Negative access tests (EXECUTE LIVE via raw API — do not defer):** a **non-member** account is *denied* reading + messages/answers/dates/entitlements/sessions/capsules, writing plaintext to encrypted fields, self-granting premium, + and any cross-couple access. Run it the **raw-API angle**: mint a non-member ID token (admin custom token → + Identity Toolkit `signInWithCustomToken` REST) and issue Firestore REST GET/PATCH against the couple's docs — expect + App Check `403` or rules `PERMISSION_DENIED` on every attempt. Also issue the **same** reads with a **member** token to + characterize the enforcement layer (App Check vs rules). Any unauthorized `200` with couple data = **P0**. - **D4 Key exchange / management / recovery (E2EE crux):** couple key client-generated, only leaves device **wrapped** (KDF from invite seed; server holds only `wrappedCoupleKey`+`kdfSalt`/`kdfParams`+`encryptedRecoveryPhrase`); **KDF strength**; Tink AEAD = AES-GCM/256 with **AAD=coupleId**, no weak/custom crypto/nonce reuse; keybox/sealed/commitment diff --git a/ClaudeReport.md b/ClaudeReport.md index c7ca2d11..686e2839 100644 --- a/ClaudeReport.md +++ b/ClaudeReport.md @@ -1,5 +1,24 @@ # Claude QA Report — Full-App QA (living report) +> **RUN-STATE: Round 7 (multi-angle DEEP DIVE) — 2026-06-25, client HEAD `f47c8e2`, functions deployed. Plan updated with a "Multi-angle attack mandate" + live raw-API D3.** Attacked security/data/concurrency from multiple angles (admin ground-truth read, raw Firestore REST as member+non-member, killed/cold state, malformed intents, simultaneous-start race). **Security cornerstone = FULLY CLEAN (deep):** D1 at-rest — messages/previews + all 4 game-answer collections (ToT/HowWell/DesireSync/Wheel, both users) + capsules + date-swipe actions all `enc:v1:`; couple key phrase-wrapped (argon2id), recovery phrase server-blind + `encryptedRecoveryPhrase` wiped on acceptance, plaintext `inviteCode` not exploitable (invite readable only by inviter; no code-encrypted secret persisted). D3 raw-API — **non-member denied ALL reads/writes (403)**; real premium path `users/{uid}/entitlements/premium` write **denied (403, server-only) → no self-grant**; cross-couple denied. Robustness — malformed/abusive deep-link intents (unknown type, missing extras, injection/path-traversal values) → 0 crash; killed-state cold-start chat deep-link → conversation loads. **NEW FINDING: F-RACE-001 (P1)** — simultaneous game start creates **two divergent active sessions** (TOCTOU). **SEVERITY BOARD: P1 = 1 open (F-RACE-001), P0/P2/P3 = 0 open.** Baseline restored (duplicate sessions ended, 0 active, couple intact). Two hardening notes → Future.md (App Check not enforced on Firestore; user-doc update rule allows arbitrary non-`hasPremium` fields).** +> +> **F-RACE-001 (P1, NEW — concurrency):** When BOTH partners start the *same* game within ~the same second, the couple +> ends up with **2 active sessions with different question sets** (proven live: QA "Which should end the date?" vs Sam +> "Which feels more romantic?", two session docs `bw8Q3X45…` + `yNOzMTOCsGlZPbnv2yBN`). Root cause: `GameSessionManager. +> startGameWithCouple` (usecase/GameSessionManager.kt:84–106) does a **non-transactional check-then-create** — +> `getActiveSessionForCouple` then `saveSession` (auto-id); two concurrent calls both read null → both create. The +> existing `partner_active_session` guard only covers the non-simultaneous case. Impact: the two partners play separate +> games and never get a shared reveal (core loop silently defeated); two active sessions can also lock/confuse the +> "one active game" rule. No crash, no data loss; recoverable via "End their game"/admin. Repro: stage both at This or +> That mood-select, fire "create" on both in parallel. **Suggested fix:** make session creation atomic — a Firestore +> **transaction** with a per-couple active-session **sentinel** (e.g. `couples/{cid}` field `activeSessionId` or a +> `sessions/_active` doc): read sentinel → if an active session exists, take the join/`partner_active_session` path; +> else create the session doc (client-generated id) + set the sentinel in the same transaction. Clear the sentinel on +> finish. Needs a `firestore.rules` update (member-only sentinel write) + a rules deploy + re-verify all 7 games. (This +> is an architectural change to the core game flow — flagged for a focused fix-phase implementation.) + +> **RUN-STATE: Round 6 (branding + Future.md regression QA) COMPLETE — 2026-06-25. Client HEAD `f47c8e2` on both emulators (build == HEAD, reinstalled).** Scope: regression-verify the new surfaces from the branding drop (`95cad84`: white-keyhole launcher/notification icons, animated app-icon-chip loader + fill, cold-launch splash, pairing hero) and the Future.md backlog clear (`f47c8e2`: inclusive gender options, turn-aware Home copy, push rate-limit budget split, results-push suppression via new ActiveGameSessionMonitor, paywall retry/offline/hide-Continue, auth privacy rotator). **0 new issues — SEVERITY BOARD STILL 0 open P0–P3.** LIVE-VERIFIED: animated loader (chip+fill, both themes), splash→handoff (white-keyhole icon, no white flash), launcher icon (round mask), This or That + How Well open with no crash (confirms #4's new VM injection is sound), paywall purchase screen shows friendly "Couldn't load plans" + Try again with Continue hidden (no dead button) + online→generic message (#5), onboarding carousel illustration. Unit tests green (NotificationRateLimiter rewritten + PartnerNotificationManagerTest repaired). No FATAL on either device all session. CODE/UNIT-VERIFIED (live deferred, low-risk over proven patterns + fragile multi-text-field/2-device paths): #1 gender step (EditProfile + onboarding sex step — same option list as the shipping Female/Male), #8 rotator on SignUp/Forgot (reuses the Login-proven BrandMessageRotator), #2 "Your turn to play." (static string in the proven GAME_WAITING path), #3 weekly-cap exemption (unit-tested; only triggers at ≥100/wk), #4 results-suppression timing (mechanism + VM wiring verified; simultaneous-finish timing is non-deterministic to drive). Baseline restored: 5554 signed out during the sign-up pass, re-signed-in QA (`Y05AKO2IlTPMa0JQW1BiNIM0uzK2`) via admin custom token; couple `Xal3Kw3gjSdn0niERYKJ` intact, Sam paired.** + > **RUN-STATE: Round 5 (functions deploy + expanded re-QA) COMPLETE — 2026-06-25. Client HEAD `765916a` on both emulators; Cloud Functions DEPLOYED (`firebase deploy --only functions` → "Deploy complete", all 30+ fns updated). Fixed + verified LIVE: E-OBS (all 12 FCM senders now set `android.notification.channelId` → backgrounded chat push lands on `partner_activity`, NOT `fcm_fallback`), E-003 results-ready (server sends `game_session_id`; finished-game deep-link → per-session "This or That Results" screen, not hub/setup). Expanded coverage per user request: VARIED GAMEPLAY (Standard/Deep + 0-match "Total opposites" result path), exhaustive NAV FUZZING (rapid triple-tap opens setup once via launchSingleTop; back-stack clean; no dead-ends/double-back), and NEW PASS G account-creation/fake-account — ALL SECURE: sign-up+validation (weak-pw → friendly error), fresh-account isolation (zero couple data), duplicate-email → `auth/email-already-exists`, invite single-use+24h-expiry + bogus code → "Invite not found", recovery phrase client-generated. **SEVERITY BOARD: 0 open at ALL levels (P0–P3).** Baseline restored: couple intact, both free, 0 active sessions, throwaway test account deleted, Sam re-paired.** > _Round 4 (carried): E-003 game-push + B-004 WaitingForPartner "Join the game" + A-OBS paywall copy all FIXED + verified live._ > _Round 2 result (carried): FIX PHASE COMPLETE — P1×2 (B-001 session rules, C-NAV-001 back-stack), P2×4 (A-001, B-002, C-CC-001, C-DS-001), P3×4 (A-003, B-003, E-002, F-OBS) all FIXED; D-001 (P1 rules) + E-001 (P2 routing) fixed earlier. All verified LIVE except E-002/F-OBS (code+build; live-trigger deferred). Rules deployed._ diff --git a/Future.md b/Future.md index f99bf44f..ff13bafc 100644 --- a/Future.md +++ b/Future.md @@ -14,6 +14,17 @@ Improvement & feature ideas surfaced while QA-testing as a consumer (each works quiet-hours moon) used consistently would strengthen identity. Generate the G-set, drop the assets in, then wire them in. *Prompted by:* Pass H branding review. +### Security hardening (defense-in-depth — not vulnerabilities; rules already hold) + +- **Enforce App Check on Firestore (currently OFF).** Round 7 raw-API test: an authenticated request with **no App + Check token** (raw Firestore REST) returned `200` for a member — so rules are the *sole* gate. Rules correctly deny + non-members/cross-couple (all `403`), so this is not a live hole, but enabling App Check enforcement on Firestore + would block non-app clients entirely (defense-in-depth). *Prompted by:* R7 D3 raw-API angle. +- **Tighten the `users/{uid}` update rule to a field allowlist.** The rule only blocks changing `hasPremium`; a user + can write arbitrary *other* fields to their own doc (e.g. a cosmetic `plan`/junk). No gate reads those (premium gates + on the server-only `users/{uid}/entitlements/premium` subcollection + `category.access`), so it grants nothing — but + restricting updates to a known field set is cleaner. *Prompted by:* R7 D3 (`plan` field writable, unused by gating). + > Artwork to generate (ChatGPT prompts, house-style-matched) lives in `ClaudeBrandingReview.md`, not here.