2026-07-07 23:36:16 -05:00
|
|
|
// Global v2 options (region pin, instance cap) — MUST be imported before any function module so
|
|
|
|
|
// setGlobalOptions runs before the functions below are defined.
|
|
|
|
|
import './options'
|
2026-06-17 20:26:24 -05:00
|
|
|
import * as admin from 'firebase-admin'
|
|
|
|
|
|
|
|
|
|
// Initialize the Admin SDK once for every function in this codebase.
|
|
|
|
|
// Handlers call admin.firestore()/messaging() lazily at invocation time, so a
|
|
|
|
|
// single idempotent init here is sufficient and avoids "already exists" errors.
|
|
|
|
|
if (admin.apps.length === 0) {
|
|
|
|
|
admin.initializeApp()
|
|
|
|
|
}
|
2026-06-17 01:25:51 -05:00
|
|
|
|
2026-07-09 03:43:59 -05:00
|
|
|
// RevenueCat entitlement webhook. Auth = HMAC-SHA256 over the raw body (see revenueCatWebhook.ts).
|
|
|
|
|
// Binds defineSecret('REVENUECAT_WEBHOOK_SECRET') — the integration's signing secret from the
|
|
|
|
|
// RevenueCat dashboard — which is validated for the whole codebase at deploy time, so the secret
|
|
|
|
|
// must exist before any deploy. Seed it with:
|
|
|
|
|
// firebase functions:secrets:set REVENUECAT_WEBHOOK_SECRET
|
|
|
|
|
export { revenueCatWebhook } from './billing/revenueCatWebhook'
|
2026-06-17 01:25:51 -05:00
|
|
|
export { syncEntitlement } from './billing/syncEntitlement'
|
2026-06-27 16:35:41 -05:00
|
|
|
export { onEntitlementChanged } from './billing/onEntitlementChanged'
|
2026-06-19 23:47:01 -05:00
|
|
|
export { sendGentleReminderCallable } from './notifications/sendGentleReminderCallable'
|
2026-06-30 03:54:01 -05:00
|
|
|
export { sendThinkingOfYouCallable } from './notifications/sendThinkingOfYouCallable'
|
2026-06-19 02:23:52 -05:00
|
|
|
export {
|
|
|
|
|
sendChallengeDayReminders,
|
|
|
|
|
unlockDueMemoryCapsules,
|
|
|
|
|
} from './notifications/gameRetention'
|
2026-06-22 08:34:15 -05:00
|
|
|
export { sendDailyQuestionProactiveReminder } from './notifications/dailyQuestionReminder'
|
2026-06-30 00:38:06 -05:00
|
|
|
export { sendStreakReminder } from './notifications/streakReminder'
|
2026-06-22 08:53:23 -05:00
|
|
|
export { sendReengagementReminder } from './notifications/reengagement'
|
2026-06-17 19:37:19 -05:00
|
|
|
export { checkDeviceIntegrity } from './security/checkDeviceIntegrity'
|
2026-06-23 22:14:36 -05:00
|
|
|
export { notifyOnDateMatch } from './dates/createDateMatch'
|
2026-06-30 18:14:48 -05:00
|
|
|
export { onDateReflectionWritten } from './dates/onDateReflectionWritten'
|
2026-07-01 04:12:58 -05:00
|
|
|
export { onDateReflectionRevealed } from './dates/onDateReflectionRevealed'
|
2026-06-30 18:14:48 -05:00
|
|
|
export { onDateHistoryCreated } from './dates/onDateHistoryCreated'
|
2026-06-30 20:43:22 -05:00
|
|
|
export { onRestoreRequested, onRestoreFulfilled } from './backup/onRestoreRequested'
|
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
|
|
|
export { cleanupExpiredRestoreRequests } from './backup/cleanupRestoreRequests'
|
2026-06-18 00:18:05 -05:00
|
|
|
export {
|
|
|
|
|
assignDailyQuestion,
|
|
|
|
|
assignDailyQuestionCallable,
|
|
|
|
|
} from './questions/assignDailyQuestion'
|
|
|
|
|
export { onAnswerWritten } from './questions/onAnswerWritten'
|
2026-06-26 12:40:36 -05:00
|
|
|
export { onAnswerRevealed } from './questions/onAnswerRevealed'
|
2026-06-20 18:25:05 -05:00
|
|
|
export { onMessageWritten } from './questions/onMessageWritten'
|
2026-06-18 00:25:52 -05:00
|
|
|
export { onCoupleLeave } from './couples/onCoupleLeave'
|
2026-06-19 20:04:18 -05:00
|
|
|
export { leaveCoupleCallable } from './couples/leaveCoupleCallable'
|
2026-06-19 21:46:12 -05:00
|
|
|
export { acceptInviteCallable } from './couples/acceptInviteCallable'
|
2026-06-20 23:28:20 -05:00
|
|
|
export { createInviteCallable } from './couples/createInviteCallable'
|
2026-06-20 23:59:24 -05:00
|
|
|
export { submitOutcomeCallable } from './couples/submitOutcomeCallable'
|
2026-07-06 21:06:57 -05:00
|
|
|
export { aggregateOutcomeStats } from './couples/aggregateOutcomes'
|
2026-06-20 23:59:24 -05:00
|
|
|
export { scheduledOutcomesReminder } from './couples/scheduledOutcomesReminder'
|
2026-06-19 20:04:18 -05:00
|
|
|
export { onUserDelete } from './users/onUserDelete'
|
2026-07-08 00:08:18 -05:00
|
|
|
export {
|
|
|
|
|
onGameSessionUpdate,
|
|
|
|
|
onThisOrThatPartFinished,
|
|
|
|
|
onWheelPartFinished,
|
|
|
|
|
onHowWellPartFinished,
|
|
|
|
|
onDesireSyncPartFinished,
|
|
|
|
|
} from './games/onGameSessionUpdate'
|
2026-06-17 01:25:51 -05:00
|
|
|
|
2026-06-28 17:19:06 -05:00
|
|
|
export { wrapReleaseKeyCallable } from './releaseKey/wrapReleaseKeyCallable'
|
|
|
|
|
|
2026-06-23 22:14:36 -05:00
|
|
|
// NOTE (security review Batch 2): the unauthenticated public `health` HTTP endpoint
|
|
|
|
|
// was removed to shrink attack surface. Deployment can be verified via
|
|
|
|
|
// `firebase functions:list`. If an uptime probe is ever needed, re-add it behind
|
|
|
|
|
// auth / a shared secret rather than as an open endpoint.
|