Closer/functions
null 8496bbedac feat(functions): hourly cleanup of expired restore requests (Future.md security #1)
A restore_requests doc left behind — partner wrapped the couple key but the
recipient never completed, or nobody ever answered — keeps its ECIES keybox
forever. It's sealed to the recipient alone, so it's not exploitable, but key
material has no business lying around, and expiry was enforced only client-side
(fulfil-time check, delete-before-re-request, delete-on-complete): a request
whose device disappeared simply lived forever.

cleanupExpiredRestoreRequests runs hourly (requests expire in 30 min, so a
stranded keybox now lives ~1.5 h at most) over a collectionGroup query on
expiresAt — chosen over iterating couples deliberately, because it also reaps
requests orphaned under already-deleted couple docs, which a parent iteration
can never see. Backed by a new COLLECTION_GROUP fieldOverride on
restore_requests.expiresAt (expiresAt is epoch millis, not a Timestamp, which
also rules out native Firestore TTL).

Deletes only on positive evidence: a pure predicate re-verifies every query hit
(a real expiresAt past a 5-min grace so a mid-completion restore is never raced;
a day-old createdAt as the defensive fallback when expiresAt is unusable;
neither → leave it and log). Status is deliberately irrelevant — a
DECLINED-after-READY doc still carries the keybox.

Requests that expired recently while still waiting on someone (REQUESTED/READY)
nudge the requester — "start a new one whenever you're ready" — through the
house pipeline (notification_queue + sendPushToUser, quiet hours respected).
The doc is deleted BEFORE the nudge, so a notify failure costs a nudge, never a
duplicate; a 2-h notify window keeps the first deploy from blasting the ancient
backlog. queueAndPush moves from being file-local in onRestoreRequested.ts to a
shared notifications/queueAndPush.ts — the cleanup needed identical semantics,
and two copies of notification plumbing is how the same bug ends up existing
twice.

Sweep never throws (a scheduled-function throw retries in a storm; the next
hourly run IS the retry): per-doc Promise.allSettled, one summary log line via
the structured logger.

15 new tests; grace-window mutation check kills exactly the guard test.
Functions suite 98/98, tsc clean. Deploy (scoped — the RevenueCat webhook must
stay undeployed): firebase deploy --only firestore:indexes, then
--only functions:cleanupExpiredRestoreRequests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 02:32:59 -05:00
..
dist feat(daily): wildcard-day picker branch + authoring spec (Option A) 2026-07-11 16:39:59 -05:00
scripts feat(dates): Firestore-backed date catalog + sponsored ideas 2026-07-06 21:02:22 -05:00
src feat(functions): hourly cleanup of expired restore requests (Future.md security #1) 2026-07-15 02:32:59 -05:00
jest.config.js fix: test infrastructure and entitlement logic updates 2026-06-17 21:08:13 -05:00
package-lock.json build(functions): B6a bump firebase-functions to v7 (latest) 2026-07-08 00:03:17 -05:00
package.json build(functions): bump runtime Node 20 → 22 (Node 20 decommissioned 2026-10-30) 2026-07-08 03:42:18 -05:00
tsconfig.json feat(billing): server-side entitlement sync — RevenueCat webhook handler, entitlement logic, Firestore EntitlementChecker, Hilt DI, callable sync function (batch 10) 2026-06-17 01:25:51 -05:00