fix(rules): add capsules + challenges member rules (D-001 P1) — Memory Lane/Challenges were broken
couples/{id}/capsules and /challenges had NO rules -> default-deny -> Memory Lane hung on
loader, Connection Challenges couldn't load (live PERMISSION_DENIED). Added member-read +
ciphertext-enforcing capsules rule (title/content/promptUsed = enc:v1:) and a challenges
rule (catalog-referenced progress). Deployed + verified live: both features load, 0 perm
errors. Found during Round-2 re-verify of A-001 (Memory Lane couple-shared also confirmed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
efe0ddbf29
commit
b05a72605e
|
|
@ -15,9 +15,9 @@ _(Prior games/notifications QA from 2026-06-24 was completed + verified; superse
|
|||
| Severity | Open | Fixed |
|
||||
|---|---|---|
|
||||
| P0 | 0 | 0 |
|
||||
| P1 | 0 | 1 |
|
||||
| P1 | 0 | 2 |
|
||||
| P2 | 0 | 1 |
|
||||
| P3 | 3 | 0 |
|
||||
| P3 | 4 | 0 |
|
||||
|
||||
**Round 1: all P0–P2 found were FIXED** (A-001 premium P1, E-001 notif-routing P2). Open = P3 cosmetics only
|
||||
(A-003 badge, B-001 stale-session guard, E-002 informational notif routing). Deferred for a clean "flawless"
|
||||
|
|
@ -68,7 +68,9 @@ _Follow-ups (not blockers): live **non-member negative test** (D3) needs a fresh
|
|||
|
||||
| ID | Area | Severity | Description | Status |
|
||||
|---|---|---|---|---|
|
||||
| D-OBS | Rules / data load | **P2?** (investigate) | During Pass B, logcat showed `PERMISSION_DENIED` for client listeners on `couples/{id}/outcomes`, `couples/{id}/challenges (where status==active)`, and `couples/{id}/capsules`. Either a rules gap or queries firing for features the (free) user can't read → console errors / possibly broken Connection Challenges + Memory Lane data load. **Round 2: confirm whether these features load correctly + fix the rule or guard the query.** | Open |
|
||||
| D-001 | Rules — missing subcollection rules | **P1** | `couples/{id}/capsules` and `couples/{id}/challenges` had **no `match` block** → default-deny → **Memory Lane hung on its loading heart** and **Connection Challenges** couldn't load (live `PERMISSION_DENIED` confirmed). Two premium features broken. | Sam premium, QA opens Memory Lane → stuck loading heart; logcat `Listen for Query(.../capsules) failed: PERMISSION_DENIED`. | **FIXED + DEPLOYED** — added member-read + ciphertext-enforcing `capsules` rule (title/content/promptUsed must be `enc:v1:`) and a `challenges` rule (catalog-referenced, progress-only). Re-verified live: Memory Lane shows empty state, Connection Challenges shows the series list, **0 permission errors**. |
|
||||
| F-OBS | Resilience (UI) | **P3** | MemoryLaneScreen (and likely others) **hangs on the loading indicator forever** when a Firestore query fails, instead of showing an error/empty state. Masked the D-001 root cause. Add load-failure handling. | Was visible before D-001 fix (stuck heart). | Open (Pass F) |
|
||||
| (outcomes) | Rules | — | The Round-1 `outcomes` list `PERMISSION_DENIED` is **by-design** — the rule restricts reads to specific dayKeys (`day_0/30/60/90`); a bare list query is correctly denied. Not a bug. | — | Closed (by-design) |
|
||||
|
||||
## Pass E — Notifications
|
||||
- **Copy carries no private content:** all function notification bodies are generic ("Tap to read and reply", "Answer together before it expires", etc.); `${title}` refers to public question/game titles, not user answers. ✓ (ties to D6)
|
||||
|
|
|
|||
|
|
@ -607,6 +607,43 @@ service cloud.firestore {
|
|||
allow delete: if false;
|
||||
}
|
||||
|
||||
// Memory Lane capsules: member-readable; author creates with ENCRYPTED content (title/content/
|
||||
// promptUsed are enc:v1:). status flips sealed→unlocked (client or the scheduled unlock fn).
|
||||
match /capsules/{capsuleId} {
|
||||
allow read: if isCouplesMember(coupleId);
|
||||
allow create: if isCouplesMember(coupleId)
|
||||
&& coupleEncryptionEnabled(coupleId)
|
||||
&& request.resource.data.authorId == request.auth.uid
|
||||
&& request.resource.data.keys().hasOnly(
|
||||
['authorId', 'title', 'content', 'promptUsed', 'unlockAt', 'createdAt', 'status'])
|
||||
&& isCiphertext(request.resource.data.title)
|
||||
&& isCiphertext(request.resource.data.content)
|
||||
&& (!('promptUsed' in request.resource.data)
|
||||
|| request.resource.data.promptUsed == null
|
||||
|| isCiphertext(request.resource.data.promptUsed));
|
||||
allow update: if isCouplesMember(coupleId)
|
||||
&& (
|
||||
// Author re-saves encrypted content before unlock
|
||||
(request.resource.data.diff(resource.data).affectedKeys().hasOnly(['title', 'content', 'promptUsed', 'unlockAt'])
|
||||
&& isCiphertext(request.resource.data.title)
|
||||
&& isCiphertext(request.resource.data.content))
|
||||
||
|
||||
// Status transition (e.g. sealed → unlocked)
|
||||
request.resource.data.diff(resource.data).affectedKeys().hasOnly(['status'])
|
||||
);
|
||||
allow delete: if isCouplesMember(coupleId);
|
||||
}
|
||||
|
||||
// Connection Challenges: catalog-referenced (no free-text user content), members track progress.
|
||||
match /challenges/{challengeId} {
|
||||
allow read: if isCouplesMember(coupleId);
|
||||
allow create: if isCouplesMember(coupleId)
|
||||
&& request.resource.data.keys().hasOnly(['challengeId', 'startedAt', 'status', 'completions']);
|
||||
allow update: if isCouplesMember(coupleId)
|
||||
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['completions', 'status']);
|
||||
allow delete: if isCouplesMember(coupleId);
|
||||
}
|
||||
|
||||
// Outcomes: couple-level 30/60/90 day check-ins. Both members can read.
|
||||
// Writes are server-side only via submitOutcomeCallable; direct client writes denied.
|
||||
match /outcomes/{dayKey} {
|
||||
|
|
|
|||
Loading…
Reference in New Issue