fix: rate limiter bump (20/day, 100/week), firestore rules for image messages, storage rules for chat_media, gitignore ClaudeReport

- NotificationRateLimiter: 20 partner/day, 100/week (was 2/4 — too tight for game activity)
- firestore.rules: messages create allows type=image with mediaUrl or type=text with ciphertext
- storage.rules: chat_media path with 15MB cap
- .gitignore: ClaudeReport.md, docs/img
This commit is contained in:
null 2026-06-24 15:20:44 -05:00
parent 608ddcfc5b
commit 4e2c3fdf0d
4 changed files with 36 additions and 8 deletions

4
.gitignore vendored
View File

@ -67,3 +67,7 @@ ios_encrypt.md
closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json
DAILY_FUN_IMPLEMENTATION_BATCH_PLAN.md
gitleaks-after.json
docs/img/qa.jpeg
docs/img/sam.jpg
ClaudeReport.md
ClaudeReport.md

View File

@ -8,10 +8,15 @@ import java.util.concurrent.TimeUnit
/**
* Persisted rate limiter for partner-trigger and reminder notifications.
*
* Limits:
* - 2 partner-trigger notifications per day
* - 1 reminder notification per day
* - 4 total notifications per week
* Limits (sized for a couples app where partner activity is the core loop the old 2/day +
* 4/week caps suppressed legitimate game start/finish and partner-action notifications after a
* single game; these are anti-runaway ceilings, not gentle-nudge throttles):
* - 20 partner-trigger notifications per day
* - 1 reminder notification per day (proactive nudges stay gentle)
* - 100 total notifications per week
*
* Note: chat messages are NOT throttled here foreground messages show the in-app bubble and
* backgrounded ones are displayed by the OS from the FCM notification block, both bypassing this.
*
* Counts are stored in [SharedPreferences] and reset when a new day or week starts.
*/
@ -30,9 +35,9 @@ class NotificationRateLimiter(context: Context) {
private const val KEY_REMINDER_COUNT = "reminder_count"
private const val KEY_TOTAL_COUNT = "total_count"
const val MAX_PARTNER_PER_DAY = 2
const val MAX_PARTNER_PER_DAY = 20
const val MAX_REMINDER_PER_DAY = 1
const val MAX_TOTAL_PER_WEEK = 4
const val MAX_TOTAL_PER_WEEK = 100
}
/**

View File

@ -376,11 +376,20 @@ service cloud.firestore {
// Discussion messages: any couple member can read, but only the author can write/update/delete
match /messages/{messageId} {
allow read: if isCouplesMember(coupleId);
// Text messages carry ciphertext in `text`; image messages carry only a `mediaUrl`
// pointing at the encrypted bytes in Storage (the photo itself is E2E-encrypted).
allow create: if isCouplesMember(coupleId)
&& coupleEncryptionEnabled(coupleId)
&& request.resource.data.authorUserId == request.auth.uid
&& request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt'])
&& isCiphertext(request.resource.data.text);
&& request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt', 'type', 'mediaUrl'])
&& (
(request.resource.data.get('type', 'text') == 'image'
&& request.resource.data.mediaUrl is string
&& request.resource.data.mediaUrl.size() > 0)
||
(request.resource.data.get('type', 'text') == 'text'
&& isCiphertext(request.resource.data.text))
);
allow update: if isCouplesMember(coupleId)
&& coupleEncryptionEnabled(coupleId)
&& resource.data.authorUserId == request.auth.uid

View File

@ -20,6 +20,16 @@ service firebase.storage {
allow read: if request.auth != null && request.auth.uid == uid;
}
// Encrypted chat media: the author writes under their own path (already E2E-encrypted
// ciphertext, so Storage never holds anything readable). The partner reads via the tokenized
// download URL, which bypasses these rules — same model as profile photos. 15 MB cap.
match /users/{uid}/chat_media/{file} {
allow write: if request.auth != null
&& request.auth.uid == uid
&& request.resource.size < 15 * 1024 * 1024;
allow read: if request.auth != null && request.auth.uid == uid;
}
// Deny all other paths by default.
match /{allPaths=**} {
allow read, write: if false;