diff --git a/.gitignore b/.gitignore index 624b026a..7a5b0ab4 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,8 @@ __pycache__/ # Asset-db backups must never live under assets/ — everything there ships in the APK app/src/main/assets/database/*.bak* + +# Compiled Cloud Functions output — generated from functions/src by tsc. +# NEVER commit: it is what deploys (main: dist/index.js), and a stale committed copy +# silently ships old code (FUNCTIONS-DIST-STALE). The predeploy hook + CI build it. +functions/dist/ diff --git a/app/schemas/com.couplesconnect.app.data.local.AppDatabase/1.json b/app/schemas/com.couplesconnect.app.data.local.AppDatabase/1.json deleted file mode 100644 index 3431bb1d..00000000 --- a/app/schemas/com.couplesconnect.app.data.local.AppDatabase/1.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "4c0a60329b23e0bc0526d7cb7e7269b9", - "entities": [ - { - "tableName": "question", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `text` TEXT NOT NULL, `category_id` TEXT NOT NULL, `depth_level` INTEGER NOT NULL, `is_premium` INTEGER NOT NULL, `type` TEXT NOT NULL, `tags` TEXT NOT NULL, `answer_config` TEXT NOT NULL, `pack_id` TEXT, `created_at` INTEGER NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "categoryId", - "columnName": "category_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "depthLevel", - "columnName": "depth_level", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isPremium", - "columnName": "is_premium", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "tags", - "columnName": "tags", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answerConfig", - "columnName": "answer_config", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "packId", - "columnName": "pack_id", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "createdAt", - "columnName": "created_at", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "question_category", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `display_name` TEXT NOT NULL, `description` TEXT NOT NULL, `access` TEXT NOT NULL, `icon_name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "displayName", - "columnName": "display_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "access", - "columnName": "access", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "iconName", - "columnName": "icon_name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4c0a60329b23e0bc0526d7cb7e7269b9')" - ] - } -} \ No newline at end of file diff --git a/docs/Engineering_Reference_Manual.md b/docs/Engineering_Reference_Manual.md index 249a883f..aa9ce190 100644 --- a/docs/Engineering_Reference_Manual.md +++ b/docs/Engineering_Reference_Manual.md @@ -1171,7 +1171,20 @@ npm run serve # local emulator firebase deploy --only functions ``` -`dist/` is committed so the deployed function code is reproducible without running `npm run build` at deploy time. +**`functions/dist/` is generated, NOT committed** (changed 2026-07-16). `src` is the source of truth; +`dist` is `tsc` output that ships (`main: dist/index.js`). It was committed for "reproducibility without +building at deploy" — that rationale is dead: the `firebase.json` `predeploy` hook (`npm run build`) now +compiles on every deploy, and `backend-ci` builds it for tests. Committing it only hid drift (a stale +`dist` shipped twice — FUNCTIONS-DIST-STALE) behind 104 noisy `.js`/`.js.map` diffs. Rule: **src is +truth, dist is built by deploy/CI, never committed.** When verifying "is X deployed", compare against a +build of `src` (or the deploy-provenance SHA), never a committed artifact. + +Same class, elsewhere in the repo: `app/schemas/**` (Room-exported schema, generated by KSP) stays +committed — it's legitimate migration provenance and `AssetDatabaseVerifyTest` validates the live one — +but a stale `com.couplesconnect.app…` dir from the package rename was deleted (2026-07-16), proof the +class bites. `app/src/main/assets/database/app.db` (output of `seed/build_db.py`) also stays committed +(it ships in the APK) and is guarded by `AssetDatabaseVerifyTest`; editing `seed/questions/*.json` +requires re-running the generator + that test. --- diff --git a/functions/dist/backup/cleanupRestoreRequests.js b/functions/dist/backup/cleanupRestoreRequests.js deleted file mode 100644 index 24a3185d..00000000 --- a/functions/dist/backup/cleanupRestoreRequests.js +++ /dev/null @@ -1,159 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.cleanupExpiredRestoreRequests = exports.NOTIFY_WINDOW_MS = exports.FALLBACK_TTL_MS = exports.GRACE_MS = void 0; -exports.shouldReapRestoreRequest = shouldReapRestoreRequest; -exports.shouldNotifyExpiry = shouldNotifyExpiry; -exports.sweepExpiredRestoreRequests = sweepExpiredRestoreRequests; -const admin = __importStar(require("firebase-admin")); -const scheduler_1 = require("firebase-functions/v2/scheduler"); -const queueAndPush_1 = require("../notifications/queueAndPush"); -const log_1 = require("../log"); -/** - * Cloud Function: cleanupExpiredRestoreRequests - * - * A `restore_requests` doc left behind (partner wrapped the couple key but the recipient never - * completed, or nobody ever answered) keeps its ECIES `keybox` — ciphertext sealed only to the - * recipient, useless to anyone else, but key material has no business lying around. Expiry is - * otherwise enforced only client-side (fulfil-time check, delete-before-re-request, delete-on- - * complete), so a request whose device disappeared lives forever. - * - * Hourly sweep: requests expire 30 minutes after creation, so this bounds a stranded keybox's life - * to ~1.5 h. Uses a collectionGroup query on purpose — it also reaps requests orphaned under - * already-deleted couple docs, which an iterate-the-couples sweep can never see (deleted parents - * don't list). Needs the COLLECTION_GROUP fieldOverride on `restore_requests.expiresAt` in - * `firestore.indexes.json`. - * - * Admin SDK bypasses the recipient-only delete rule, which is the point: nobody else CAN clean these. - */ -/** Never race a restore that is mid-completion: the client treats these as expired well before us. */ -exports.GRACE_MS = 5 * 60 * 1000; -/** Reap docs with no usable `expiresAt` once they are unambiguously ancient. */ -exports.FALLBACK_TTL_MS = 24 * 60 * 60 * 1000; -/** - * Only tell the requester about expiries that just happened (~2 sweep cycles). Without this bound, - * the first deploy would blast a notification for every ancient doc in the backlog at once. - */ -exports.NOTIFY_WINDOW_MS = 2 * 60 * 60 * 1000; -const asMillis = (v) => (typeof v === 'number' && v > 0 ? v : 0); -/** - * Pure reap decision, re-verified per doc even though the query already filtered — the query is an - * index scan, this is the contract. Deletes only on positive evidence of staleness: - * a real `expiresAt` past the grace window, or (defensively — the create rule doesn't validate field - * values) no usable `expiresAt` but a `createdAt` at least a day old. A doc with neither is left - * alone for a human to wonder about. - */ -function shouldReapRestoreRequest(data, nowMs) { - const expiresAt = asMillis(data.expiresAt); - if (expiresAt > 0) - return nowMs > expiresAt + exports.GRACE_MS; - const createdAt = asMillis(data.createdAt); - return createdAt > 0 && nowMs > createdAt + exports.FALLBACK_TTL_MS; -} -/** - * Pure notify decision: only for a request that expired *recently* while still waiting on someone - * (REQUESTED: partner never approved; READY: recipient never completed). Terminal states are debris — - * RESTORED already succeeded and DECLINED was answered; telling anyone again is noise. - */ -function shouldNotifyExpiry(data, nowMs) { - const expiresAt = asMillis(data.expiresAt); - if (expiresAt <= 0 || nowMs <= expiresAt + exports.GRACE_MS) - return false; - if (nowMs - expiresAt > exports.NOTIFY_WINDOW_MS) - return false; - return data.status === 'REQUESTED' || data.status === 'READY'; -} -/** - * The sweep, with `db` injectable for tests. Per-doc failures are isolated (one bad doc can't stop - * the pass); the doc is deleted BEFORE the requester is notified so a notify failure costs a nudge, - * never a duplicate (the doc it would re-trigger on is already gone). - */ -async function sweepExpiredRestoreRequests(db, nowMs) { - const snap = await db - .collectionGroup('restore_requests') - .where('expiresAt', '<=', nowMs - exports.GRACE_MS) - .limit(200) - .get(); - const counts = { scanned: snap.size, reaped: 0, notified: 0, skipped: 0, failed: 0 }; - const results = await Promise.allSettled(snap.docs.map(async (doc) => { - var _a; - const data = ((_a = doc.data()) !== null && _a !== void 0 ? _a : {}); - if (!shouldReapRestoreRequest(data, nowMs)) { - counts.skipped++; - log_1.logger.warn('[cleanupRestoreRequests] query hit failed re-verify; leaving doc', { path: doc.ref.path }); - return; - } - const notify = shouldNotifyExpiry(data, nowMs); - await doc.ref.delete(); - counts.reaped++; - if (!notify) - return; - // Best-effort: a missed nudge is fine, a failed delete is not — hence delete-first above. - // Skip when the couple itself is gone (an orphan reap has nobody meaningful to notify). - try { - const coupleRef = doc.ref.parent.parent; - const recipientUid = typeof data.recipientUid === 'string' && data.recipientUid ? data.recipientUid : doc.id; - if (!coupleRef) - return; - const coupleDoc = await coupleRef.get(); - if (!coupleDoc.exists) - return; - await (0, queueAndPush_1.queueAndPush)(db, recipientUid, { - type: 'restore_request_expired', - title: 'Your restore request expired', - body: 'No worries — start a new one whenever you’re ready.', - coupleId: coupleRef.id, - }); - counts.notified++; - } - catch (e) { - log_1.logger.warn('[cleanupRestoreRequests] expiry notice failed', { path: doc.ref.path, error: String(e) }); - } - })); - counts.failed = results.filter((r) => r.status === 'rejected').length; - return counts; -} -exports.cleanupExpiredRestoreRequests = (0, scheduler_1.onSchedule)({ schedule: 'every 1 hours', timeoutSeconds: 180 }, async () => { - try { - const counts = await sweepExpiredRestoreRequests(admin.firestore(), Date.now()); - log_1.logger.log(`[cleanupRestoreRequests] scanned ${counts.scanned}; reaped ${counts.reaped}; ` + - `notified ${counts.notified}; skipped ${counts.skipped}; failed ${counts.failed}`); - } - catch (e) { - // Never rethrow: a scheduled-function failure retries in a storm, and the next hourly run IS the retry. - log_1.logger.error('[cleanupRestoreRequests] sweep failed', { error: String(e) }); - } -}); -//# sourceMappingURL=cleanupRestoreRequests.js.map \ No newline at end of file diff --git a/functions/dist/backup/cleanupRestoreRequests.js.map b/functions/dist/backup/cleanupRestoreRequests.js.map deleted file mode 100644 index 406ad111..00000000 --- a/functions/dist/backup/cleanupRestoreRequests.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"cleanupRestoreRequests.js","sourceRoot":"","sources":["../../src/backup/cleanupRestoreRequests.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/backup/cleanupRestoreRequests.test.js b/functions/dist/backup/cleanupRestoreRequests.test.js deleted file mode 100644 index c85bb2ba..00000000 --- a/functions/dist/backup/cleanupRestoreRequests.test.js +++ /dev/null @@ -1,129 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const cleanupRestoreRequests_1 = require("./cleanupRestoreRequests"); -const queueAndPush_1 = require("../notifications/queueAndPush"); -jest.mock('../notifications/queueAndPush', () => ({ queueAndPush: jest.fn().mockResolvedValue(undefined) })); -const NOW = 1800000000000; -describe('shouldReapRestoreRequest', () => { - it('keeps a fresh request', () => { - expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ status: 'REQUESTED', expiresAt: NOW + 60000 }, NOW)).toBe(false); - }); - it('keeps an expired request still inside the grace window', () => { - // The client already treats it as expired; we wait out the grace so we can never race a - // mid-completion restore. - expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ status: 'READY', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS + 1000 }, NOW)).toBe(false); - }); - it('reaps expired requests regardless of status — DECLINED-after-READY still carries the keybox', () => { - const expiresAt = NOW - cleanupRestoreRequests_1.GRACE_MS - 1000; - for (const status of ['REQUESTED', 'READY', 'DECLINED', 'RESTORED', 'EXPIRED']) { - expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ status, expiresAt }, NOW)).toBe(true); - } - }); - it('falls back to createdAt when expiresAt is unusable', () => { - expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ createdAt: NOW - cleanupRestoreRequests_1.FALLBACK_TTL_MS - 1 }, NOW)).toBe(true); - expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ expiresAt: 0, createdAt: NOW - cleanupRestoreRequests_1.FALLBACK_TTL_MS - 1 }, NOW)).toBe(true); - expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ createdAt: NOW - 60000 }, NOW)).toBe(false); - }); - it('leaves a doc with neither field — delete only on positive evidence', () => { - expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({}, NOW)).toBe(false); - expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ expiresAt: 'soon', createdAt: null }, NOW)).toBe(false); - }); -}); -describe('shouldNotifyExpiry', () => { - const justExpired = NOW - cleanupRestoreRequests_1.GRACE_MS - 60000; - it('notifies for a recently expired request still waiting on someone', () => { - expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: justExpired }, NOW)).toBe(true); - expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'READY', expiresAt: justExpired }, NOW)).toBe(true); - }); - it('stays silent for terminal states — RESTORED succeeded, DECLINED was answered', () => { - expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'RESTORED', expiresAt: justExpired }, NOW)).toBe(false); - expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'DECLINED', expiresAt: justExpired }, NOW)).toBe(false); - }); - it('stays silent for backlog debris — first deploy must not blast ancient expiries', () => { - expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: NOW - cleanupRestoreRequests_1.NOTIFY_WINDOW_MS - cleanupRestoreRequests_1.GRACE_MS - 1000 }, NOW)).toBe(false); - }); - it('stays silent for the createdAt-fallback branch (no real expiry to report)', () => { - expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', createdAt: NOW - cleanupRestoreRequests_1.FALLBACK_TTL_MS - 1 }, NOW)).toBe(false); - }); - it('never notifies for something it would not reap', () => { - expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: NOW + 60000 }, NOW)).toBe(false); - expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS + 1000 }, NOW)).toBe(false); - }); -}); -describe('sweepExpiredRestoreRequests', () => { - const mockQueueAndPush = queueAndPush_1.queueAndPush; - function fakeDoc(opts) { - var _a; - const coupleRef = { - id: `couple_${opts.id}`, - get: jest.fn().mockResolvedValue({ exists: (_a = opts.coupleExists) !== null && _a !== void 0 ? _a : true }), - }; - return { - id: opts.id, - data: () => opts.data, - ref: { - path: `couples/couple_${opts.id}/restore_requests/${opts.id}`, - parent: { parent: coupleRef }, - delete: opts.deleteError - ? jest.fn().mockRejectedValue(opts.deleteError) - : jest.fn().mockResolvedValue(undefined), - }, - }; - } - function fakeDb(docs) { - return { - collectionGroup: jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockReturnValue({ - get: jest.fn().mockResolvedValue({ size: docs.length, docs }), - }), - }), - }), - }; - } - beforeEach(() => mockQueueAndPush.mockClear()); - it('deletes the expired doc and nudges the requester', async () => { - const doc = fakeDoc({ id: 'uidA', data: { status: 'READY', recipientUid: 'uidA', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 } }); - const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW); - expect(doc.ref.delete).toHaveBeenCalledTimes(1); - expect(mockQueueAndPush).toHaveBeenCalledWith(expect.anything(), 'uidA', expect.objectContaining({ type: 'restore_request_expired' })); - expect(counts).toMatchObject({ scanned: 1, reaped: 1, notified: 1, failed: 0 }); - }); - it('leaves a query hit that fails re-verify', async () => { - // The index says expired; the doc says otherwise (clock skew, weird data). The predicate wins. - const doc = fakeDoc({ id: 'uidB', data: { status: 'REQUESTED', expiresAt: NOW + 60000 } }); - const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW); - expect(doc.ref.delete).not.toHaveBeenCalled(); - expect(counts).toMatchObject({ reaped: 0, skipped: 1 }); - }); - it('skips the nudge for an orphan reap (couple gone) but still deletes', async () => { - const doc = fakeDoc({ - id: 'uidC', - data: { status: 'READY', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 }, - coupleExists: false, - }); - const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW); - expect(doc.ref.delete).toHaveBeenCalledTimes(1); - expect(mockQueueAndPush).not.toHaveBeenCalled(); - expect(counts).toMatchObject({ reaped: 1, notified: 0 }); - }); - it('one failing doc never stops the sweep', async () => { - const bad = fakeDoc({ - id: 'uidD', - data: { status: 'READY', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 }, - deleteError: new Error('firestore unavailable'), - }); - const good = fakeDoc({ id: 'uidE', data: { status: 'REQUESTED', recipientUid: 'uidE', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 } }); - const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([bad, good]), NOW); - expect(good.ref.delete).toHaveBeenCalledTimes(1); - expect(counts).toMatchObject({ scanned: 2, reaped: 1, failed: 1 }); - }); - it('a notify failure costs the nudge, never the reap or the pass', async () => { - mockQueueAndPush.mockRejectedValueOnce(new Error('fcm down')); - const doc = fakeDoc({ id: 'uidF', data: { status: 'READY', recipientUid: 'uidF', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 } }); - const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW); - expect(doc.ref.delete).toHaveBeenCalledTimes(1); - expect(counts).toMatchObject({ reaped: 1, notified: 0, failed: 0 }); - }); -}); -//# sourceMappingURL=cleanupRestoreRequests.test.js.map \ No newline at end of file diff --git a/functions/dist/backup/cleanupRestoreRequests.test.js.map b/functions/dist/backup/cleanupRestoreRequests.test.js.map deleted file mode 100644 index b84af779..00000000 --- a/functions/dist/backup/cleanupRestoreRequests.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"cleanupRestoreRequests.test.js","sourceRoot":"","sources":["../../src/backup/cleanupRestoreRequests.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/backup/onRestoreRequested.js b/functions/dist/backup/onRestoreRequested.js deleted file mode 100644 index 5b5da694..00000000 --- a/functions/dist/backup/onRestoreRequested.js +++ /dev/null @@ -1,158 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onRestoreFulfilled = exports.onRestoreRequested = exports.SELF_ALERT_DEDUPE_MS = void 0; -exports.selfAlertAllowed = selfAlertAllowed; -exports.isRestoreReadyTransition = isRestoreReadyTransition; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const queueAndPush_1 = require("../notifications/queueAndPush"); -const log_1 = require("../log"); -// Suppress duplicate pushes when a request doc is rapidly deleted+recreated (a compromised account -// could loop that to spam both partners) and when an event is redelivered. The in-app queue entry is -// still written every time. A genuine re-request after the window still notifies — so this is a time -// window, NOT a permanent per-recipient marker (which would block legitimate re-requests). -exports.SELF_ALERT_DEDUPE_MS = 60 * 1000; -/** Pure dedupe decision: allow an alert push only if none was sent within the window. */ -function selfAlertAllowed(lastAlertAt, now) { - if (typeof lastAlertAt !== 'number') - return true; - return now - lastAlertAt >= exports.SELF_ALERT_DEDUPE_MS; -} -/** Pure guard: fire the completion alert only on the single REQUESTED→READY status transition. */ -function isRestoreReadyTransition(beforeStatus, afterStatus) { - return afterStatus === 'READY' && beforeStatus !== 'READY'; -} -/** - * Fires when a member starts a partner-assisted restore - * (`couples/{coupleId}/restore_requests/{recipientUid}` created on a new/wiped device). Sends TWO - * notifications, isolated so one failing never blocks the other: - * 1. To the OTHER partner — "help them restore" (high-signal; only quiet hours suppress it). - * 2. To the RECIPIENT'S OWN devices — a security "was this you?" alert. If the real owner still holds a - * device (a phished password without device loss), this is how they learn a restore is happening. - * No key material is read or logged — the request carries only a public key + a nonce. - */ -exports.onRestoreRequested = (0, firestore_1.onDocumentCreated)('couples/{coupleId}/restore_requests/{recipientUid}', async (event) => { - var _a, _b, _c, _d, _e, _f; - const { coupleId, recipientUid } = event.params; - const db = admin.firestore(); - // The device that created the request identifies itself (create-only field, best-effort on the - // client). Used ONLY to spare that device its own "was this you?" — all other devices still get it. - const requesterToken = (_b = (_a = event.data) === null || _a === void 0 ? void 0 : _a.data()) === null || _b === void 0 ? void 0 : _b.requesterFcmToken; - const excludeTokens = typeof requesterToken === 'string' && requesterToken ? [requesterToken] : undefined; - const coupleRef = db.collection('couples').doc(coupleId); - const coupleDoc = await coupleRef.get(); - if (!coupleDoc.exists) - return; - const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []); - // The recipient must be a member; notify the OTHER member (the partner who can help). - if (!userIds.includes(recipientUid)) - return; - const partnerId = userIds.find((u) => u !== recipientUid); - if (!partnerId) - return; - // Audit trail (no key material — actor/recipient/timestamp only). - log_1.logger.log(`[onRestoreRequested] couple=${coupleId} recipient=${recipientUid} partner=${partnerId}`); - // 1) Partner "help them restore" — routine quiet hours apply. Deduped against redelivery and - // request recreate-loops with the same window used for the self-alert. - try { - const now = Date.now(); - if (selfAlertAllowed((_e = coupleDoc.data()) === null || _e === void 0 ? void 0 : _e.lastRestorePartnerAlertAt, now)) { - await coupleRef.set({ lastRestorePartnerAlertAt: now }, { merge: true }); - await (0, queueAndPush_1.queueAndPush)(db, partnerId, { - type: 'restore_requested', - title: 'Help your partner restore 💜', - body: 'They’re setting up on a new device. Tap to help.', - coupleId, - }); - } - } - catch (e) { - log_1.logger.warn('[onRestoreRequested] partner notify failed', { error: String(e) }); - } - // 2) Recipient self-alert — security signal, bypasses quiet hours, deduped against create-loops. - try { - const now = Date.now(); - if (selfAlertAllowed((_f = coupleDoc.data()) === null || _f === void 0 ? void 0 : _f.lastRestoreSelfAlertAt, now)) { - await coupleRef.set({ lastRestoreSelfAlertAt: now }, { merge: true }); - await (0, queueAndPush_1.queueAndPush)(db, recipientUid, { - type: 'restore_self_alert', - title: 'New device is restoring your history', - body: 'If this wasn’t you, secure your account now.', - coupleId, - bypassQuietHours: true, - excludeTokens, - }); - } - } - catch (e) { - log_1.logger.warn('[onRestoreRequested] self-alert failed', { error: String(e) }); - } -}); -/** - * Fires when the partner writes the keybox (status flips to READY) — the moment the couple key actually - * transfers. Alerts the RECIPIENT'S OWN devices that a restore just completed, the strongest "it happened" - * signal for the real owner. Guarded to the single REQUESTED→READY transition (ignores decline/expire). - */ -exports.onRestoreFulfilled = (0, firestore_1.onDocumentUpdated)('couples/{coupleId}/restore_requests/{recipientUid}', async (event) => { - var _a, _b, _c, _d; - const before = (_a = event.data) === null || _a === void 0 ? void 0 : _a.before.data(); - const after = (_b = event.data) === null || _b === void 0 ? void 0 : _b.after.data(); - if (!isRestoreReadyTransition(before === null || before === void 0 ? void 0 : before.status, after === null || after === void 0 ? void 0 : after.status)) - return; - const requesterToken = after === null || after === void 0 ? void 0 : after.requesterFcmToken; - const excludeTokens = typeof requesterToken === 'string' && requesterToken ? [requesterToken] : undefined; - const { coupleId, recipientUid } = event.params; - const db = admin.firestore(); - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []); - if (!userIds.includes(recipientUid)) - return; - log_1.logger.log(`[onRestoreFulfilled] couple=${coupleId} recipient=${recipientUid}`); - try { - await (0, queueAndPush_1.queueAndPush)(db, recipientUid, { - type: 'restore_self_alert', - title: 'Your history was just restored', - body: 'A new device now has access. If this wasn’t you, secure your account now.', - coupleId, - bypassQuietHours: true, - excludeTokens, - }); - } - catch (e) { - log_1.logger.warn('[onRestoreFulfilled] self-alert failed', { error: String(e) }); - } -}); -//# sourceMappingURL=onRestoreRequested.js.map \ No newline at end of file diff --git a/functions/dist/backup/onRestoreRequested.js.map b/functions/dist/backup/onRestoreRequested.js.map deleted file mode 100644 index f6c5295d..00000000 --- a/functions/dist/backup/onRestoreRequested.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onRestoreRequested.js","sourceRoot":"","sources":["../../src/backup/onRestoreRequested.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/backup/onRestoreRequested.test.js b/functions/dist/backup/onRestoreRequested.test.js deleted file mode 100644 index e41dd43e..00000000 --- a/functions/dist/backup/onRestoreRequested.test.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const onRestoreRequested_1 = require("./onRestoreRequested"); -// Both helpers are pure decisions extracted from the restore triggers, so no firebase-admin mock is needed. -describe('selfAlertAllowed (self-alert spam dedupe)', () => { - const now = 1000000000000; - it('allows the alert when no previous alert timestamp exists', () => { - expect((0, onRestoreRequested_1.selfAlertAllowed)(undefined, now)).toBe(true); - expect((0, onRestoreRequested_1.selfAlertAllowed)(null, now)).toBe(true); - expect((0, onRestoreRequested_1.selfAlertAllowed)('not-a-number', now)).toBe(true); - }); - it('suppresses a second alert inside the dedupe window', () => { - expect((0, onRestoreRequested_1.selfAlertAllowed)(now - (onRestoreRequested_1.SELF_ALERT_DEDUPE_MS - 1), now)).toBe(false); - expect((0, onRestoreRequested_1.selfAlertAllowed)(now - 1, now)).toBe(false); - }); - it('allows again once the window has elapsed', () => { - expect((0, onRestoreRequested_1.selfAlertAllowed)(now - onRestoreRequested_1.SELF_ALERT_DEDUPE_MS, now)).toBe(true); - expect((0, onRestoreRequested_1.selfAlertAllowed)(now - (onRestoreRequested_1.SELF_ALERT_DEDUPE_MS + 5000), now)).toBe(true); - }); -}); -describe('isRestoreReadyTransition (completion-alert guard)', () => { - it('fires only on the REQUESTED→READY edge', () => { - expect((0, onRestoreRequested_1.isRestoreReadyTransition)('REQUESTED', 'READY')).toBe(true); - }); - it('does not fire when already READY (no repeat on unrelated updates)', () => { - expect((0, onRestoreRequested_1.isRestoreReadyTransition)('READY', 'READY')).toBe(false); - }); - it('does not fire for non-READY outcomes', () => { - expect((0, onRestoreRequested_1.isRestoreReadyTransition)('REQUESTED', 'DECLINED')).toBe(false); - expect((0, onRestoreRequested_1.isRestoreReadyTransition)('REQUESTED', 'EXPIRED')).toBe(false); - expect((0, onRestoreRequested_1.isRestoreReadyTransition)('READY', 'RESTORED')).toBe(false); - expect((0, onRestoreRequested_1.isRestoreReadyTransition)(undefined, undefined)).toBe(false); - }); -}); -//# sourceMappingURL=onRestoreRequested.test.js.map \ No newline at end of file diff --git a/functions/dist/backup/onRestoreRequested.test.js.map b/functions/dist/backup/onRestoreRequested.test.js.map deleted file mode 100644 index 5c281973..00000000 --- a/functions/dist/backup/onRestoreRequested.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onRestoreRequested.test.js","sourceRoot":"","sources":["../../src/backup/onRestoreRequested.test.ts"],"names":[],"mappings":";;AAAA,6DAAuG;AAEvG,4GAA4G;AAC5G,QAAQ,CAAC,2CAA2C,EAAE,GAAG,EAAE;IACzD,MAAM,GAAG,GAAG,aAAiB,CAAA;IAE7B,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,CAAC,IAAA,qCAAgB,EAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACnD,MAAM,CAAC,IAAA,qCAAgB,EAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9C,MAAM,CAAC,IAAA,qCAAgB,EAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC1D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,IAAA,qCAAgB,EAAC,GAAG,GAAG,CAAC,yCAAoB,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3E,MAAM,CAAC,IAAA,qCAAgB,EAAC,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,IAAA,qCAAgB,EAAC,GAAG,GAAG,yCAAoB,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACpE,MAAM,CAAC,IAAA,qCAAgB,EAAC,GAAG,GAAG,CAAC,yCAAoB,GAAG,IAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAChF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,mDAAmD,EAAE,GAAG,EAAE;IACjE,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,IAAA,6CAAwB,EAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,MAAM,CAAC,IAAA,6CAAwB,EAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,IAAA,6CAAwB,EAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACrE,MAAM,CAAC,IAAA,6CAAwB,EAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACpE,MAAM,CAAC,IAAA,6CAAwB,EAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjE,MAAM,CAAC,IAAA,6CAAwB,EAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/functions/dist/billing/entitlementLogic.js b/functions/dist/billing/entitlementLogic.js deleted file mode 100644 index 6fdaf2f4..00000000 --- a/functions/dist/billing/entitlementLogic.js +++ /dev/null @@ -1,162 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PREMIUM_REVOKED_TYPES = exports.PREMIUM_ACTIVE_TYPES = void 0; -exports.isPremiumEntitlement = isPremiumEntitlement; -exports.applyEntitlementEvent = applyEntitlementEvent; -exports.applyEntitlementSync = applyEntitlementSync; -const admin = __importStar(require("firebase-admin")); -const log_1 = require("../log"); -// Events that should grant or keep premium access active. -exports.PREMIUM_ACTIVE_TYPES = new Set([ - 'INITIAL_PURCHASE', - 'RENEWAL', - 'PRODUCT_CHANGE', - 'TRANSFER', - 'UNCANCELLATION', -]); -// Events that remove premium access. -exports.PREMIUM_REVOKED_TYPES = new Set([ - 'EXPIRATION', - 'CANCELLATION', - 'BILLING_ISSUE', - 'SUBSCRIBER_ALIAS', -]); -// Premium entitlement identifier used by the app. -const PREMIUM_ENTITLEMENT_ID = 'closer_premium'; -function getDb() { - return admin.firestore(); -} -function entitlementsRef(userId) { - return getDb().collection('users').doc(userId).collection('entitlements').doc('premium'); -} -function now() { - return admin.firestore.Timestamp.now(); -} -function isPremiumEntitlement(event) { - var _a; - const entitlementId = event.entitlement_id; - const entitlementIds = (_a = event.entitlement_ids) !== null && _a !== void 0 ? _a : []; - if (entitlementId === PREMIUM_ENTITLEMENT_ID) - return true; - if (entitlementIds.includes(PREMIUM_ENTITLEMENT_ID)) - return true; - return false; -} -/** - * Apply a RevenueCat entitlement event to Firestore. - * Writes state to users/{userId}/entitlements. - */ -async function applyEntitlementEvent(event) { - var _a; - const { type, app_user_id: userId, id: eventId, product_id: productId } = event; - // Idempotency: create a processed event marker; if it exists, skip. - const eventRef = getDb().collection('entitlement_events').doc(eventId); - try { - await eventRef.create({ processedAt: now() }); - } - catch (err) { - if ((err === null || err === void 0 ? void 0 : err.code) === 6 || ((_a = err === null || err === void 0 ? void 0 : err.message) === null || _a === void 0 ? void 0 : _a.includes('ALREADY_EXISTS'))) { - log_1.logger.log(`[entitlement] skipping duplicate event: ${eventId}`); - return; - } - throw err; - } - if (!isPremiumEntitlement(event)) { - log_1.logger.log(`[entitlement] ignored event for non-premium entitlement: ${eventId}`); - return; - } - const ref = entitlementsRef(userId); - if (exports.PREMIUM_ACTIVE_TYPES.has(type)) { - const expiresAt = event.expiration_at_ms - ? admin.firestore.Timestamp.fromMillis(event.expiration_at_ms) - : null; - await ref.set({ - premium: true, - expiresAt, - updatedAt: now(), - productId, - eventType: type, - }); - log_1.logger.log(`[entitlement] premium=true for ${userId} (${type})`); - return; - } - if (exports.PREMIUM_REVOKED_TYPES.has(type)) { - await ref.set({ - premium: false, - expiresAt: null, - updatedAt: now(), - productId, - eventType: type, - }); - log_1.logger.log(`[entitlement] premium=false for ${userId} (${type})`); - return; - } - log_1.logger.log(`[entitlement] ignored event type: ${type}`); -} -/** - * Recompute and rewrite the entitlement document for a user. - * - * In production this should query RevenueCat; for now it ensures the - * Firestore document is consistent and reflects the latest stored values. - */ -async function applyEntitlementSync(userId) { - var _a; - const ref = entitlementsRef(userId); - const snap = await ref.get(); - const data = snap.data(); - const updatedAt = now(); - let premium = false; - let expiresAt = null; - if ((data === null || data === void 0 ? void 0 : data.premium) === true) { - const storedExpiresAt = (_a = data.expiresAt) !== null && _a !== void 0 ? _a : null; - if (storedExpiresAt instanceof admin.firestore.Timestamp) { - premium = storedExpiresAt.toMillis() > Date.now(); - expiresAt = storedExpiresAt; - } - else { - // No expiration means a currently active, non-expiring entitlement. - premium = true; - } - } - const state = { - premium, - expiresAt: premium ? expiresAt : null, - updatedAt, - }; - await ref.set(state, { merge: true }); - return state; -} -//# sourceMappingURL=entitlementLogic.js.map \ No newline at end of file diff --git a/functions/dist/billing/entitlementLogic.js.map b/functions/dist/billing/entitlementLogic.js.map deleted file mode 100644 index a5556780..00000000 --- a/functions/dist/billing/entitlementLogic.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"entitlementLogic.js","sourceRoot":"","sources":["../../src/billing/entitlementLogic.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/billing/entitlementLogic.test.js b/functions/dist/billing/entitlementLogic.test.js deleted file mode 100644 index 46e1886b..00000000 --- a/functions/dist/billing/entitlementLogic.test.js +++ /dev/null @@ -1,137 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -// Mock firebase-admin before any module under test is imported. -// `admin.firestore` must work as both a callable (admin.firestore()) and a -// namespace (admin.firestore.Timestamp). Object.assign achieves both. -jest.mock('firebase-admin', () => { - const firestoreFn = jest.fn(); - firestoreFn.Timestamp = { - now: jest.fn(() => ({ toMillis: () => 1000000000000 })), - fromMillis: jest.fn((ms) => ({ toMillis: () => ms })), - }; - return { - firestore: firestoreFn, - apps: [], - initializeApp: jest.fn(), - }; -}); -const admin = __importStar(require("firebase-admin")); -const entitlementLogic_1 = require("./entitlementLogic"); -// ── Helpers ─────────────────────────────────────────────────────────────────── -function event(overrides = {}) { - return Object.assign({ id: 'evt-001', type: 'INITIAL_PURCHASE', app_user_id: 'user-123', product_id: 'closer_premium_monthly', entitlement_id: 'closer_premium' }, overrides); -} -function buildFirestoreMock(opts = {}) { - const mockSet = jest.fn().mockResolvedValue(undefined); - const mockCreate = jest.fn().mockImplementation(() => opts.createShouldFail - ? Promise.reject(Object.assign(new Error('ALREADY_EXISTS'), { code: 6 })) - : Promise.resolve(undefined)); - // Leaf doc ref (e.g. users/{uid}/entitlements/premium) - const deepDocRef = { set: mockSet }; - const deepDocFn = jest.fn().mockReturnValue(deepDocRef); - const subCollectionRef = { doc: deepDocFn }; - const subCollectionFn = jest.fn().mockReturnValue(subCollectionRef); - // Top-level doc ref: supports create (idempotency marker) and subcollection access - const topDocRef = { create: mockCreate, set: mockSet, collection: subCollectionFn }; - const mockDoc = jest.fn().mockReturnValue(topDocRef); - const mockCollection = jest.fn().mockReturnValue({ doc: mockDoc }); - const mockInstance = { collection: mockCollection }; - admin.firestore.mockReturnValue(mockInstance); - return { mockSet, mockCreate }; -} -// ── isPremiumEntitlement ────────────────────────────────────────────────────── -describe('isPremiumEntitlement', () => { - it('returns true when entitlement_id matches', () => { - expect((0, entitlementLogic_1.isPremiumEntitlement)(event({ entitlement_id: 'closer_premium' }))).toBe(true); - }); - it('returns true when entitlement_ids array includes the id', () => { - expect((0, entitlementLogic_1.isPremiumEntitlement)(event({ entitlement_id: undefined, entitlement_ids: ['closer_premium', 'other'] }))).toBe(true); - }); - it('returns false for a different entitlement_id', () => { - expect((0, entitlementLogic_1.isPremiumEntitlement)(event({ entitlement_id: 'other_entitlement', entitlement_ids: [] }))).toBe(false); - }); - it('returns false when neither id nor ids reference the premium entitlement', () => { - expect((0, entitlementLogic_1.isPremiumEntitlement)(event({ entitlement_id: undefined, entitlement_ids: ['not_premium'] }))).toBe(false); - }); -}); -// ── Event type sets ─────────────────────────────────────────────────────────── -describe('PREMIUM_ACTIVE_TYPES', () => { - it.each(['INITIAL_PURCHASE', 'RENEWAL', 'PRODUCT_CHANGE', 'TRANSFER', 'UNCANCELLATION'])('contains %s', (type) => expect(entitlementLogic_1.PREMIUM_ACTIVE_TYPES.has(type)).toBe(true)); - it.each(['EXPIRATION', 'CANCELLATION', 'BILLING_ISSUE'])('does not contain revocation event %s', (type) => expect(entitlementLogic_1.PREMIUM_ACTIVE_TYPES.has(type)).toBe(false)); -}); -describe('PREMIUM_REVOKED_TYPES', () => { - it.each(['EXPIRATION', 'CANCELLATION', 'BILLING_ISSUE', 'SUBSCRIBER_ALIAS'])('contains %s', (type) => expect(entitlementLogic_1.PREMIUM_REVOKED_TYPES.has(type)).toBe(true)); - it.each(['INITIAL_PURCHASE', 'RENEWAL'])('does not contain active event %s', (type) => expect(entitlementLogic_1.PREMIUM_REVOKED_TYPES.has(type)).toBe(false)); -}); -// ── applyEntitlementEvent ───────────────────────────────────────────────────── -describe('applyEntitlementEvent', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it('grants premium on INITIAL_PURCHASE', async () => { - const { mockSet } = buildFirestoreMock(); - await (0, entitlementLogic_1.applyEntitlementEvent)(event({ type: 'INITIAL_PURCHASE' })); - expect(mockSet).toHaveBeenCalledWith(expect.objectContaining({ premium: true })); - }); - it('revokes premium on EXPIRATION', async () => { - const { mockSet } = buildFirestoreMock(); - await (0, entitlementLogic_1.applyEntitlementEvent)(event({ type: 'EXPIRATION' })); - expect(mockSet).toHaveBeenCalledWith(expect.objectContaining({ premium: false, expiresAt: null })); - }); - it('revokes premium on CANCELLATION', async () => { - const { mockSet } = buildFirestoreMock(); - await (0, entitlementLogic_1.applyEntitlementEvent)(event({ type: 'CANCELLATION' })); - expect(mockSet).toHaveBeenCalledWith(expect.objectContaining({ premium: false })); - }); - it('is idempotent — skips duplicate event ids', async () => { - const { mockSet } = buildFirestoreMock({ createShouldFail: true }); - await (0, entitlementLogic_1.applyEntitlementEvent)(event()); - // The idempotency guard fires (create threw ALREADY_EXISTS), so set is never called. - expect(mockSet).not.toHaveBeenCalled(); - }); - it('stores expiresAt when expiration_at_ms is present', async () => { - const { mockSet } = buildFirestoreMock(); - const expiresAtMs = Date.now() + 86400000; - await (0, entitlementLogic_1.applyEntitlementEvent)(event({ type: 'RENEWAL', expiration_at_ms: expiresAtMs })); - expect(mockSet).toHaveBeenCalledWith(expect.objectContaining({ premium: true })); - }); - it('ignores non-premium entitlement events', async () => { - const { mockSet } = buildFirestoreMock(); - await (0, entitlementLogic_1.applyEntitlementEvent)(event({ entitlement_id: 'some_other_entitlement', entitlement_ids: [] })); - expect(mockSet).not.toHaveBeenCalled(); - }); -}); -//# sourceMappingURL=entitlementLogic.test.js.map \ No newline at end of file diff --git a/functions/dist/billing/entitlementLogic.test.js.map b/functions/dist/billing/entitlementLogic.test.js.map deleted file mode 100644 index d0e1912d..00000000 --- a/functions/dist/billing/entitlementLogic.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"entitlementLogic.test.js","sourceRoot":"","sources":["../../src/billing/entitlementLogic.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/billing/onEntitlementChanged.js b/functions/dist/billing/onEntitlementChanged.js deleted file mode 100644 index c7a073a8..00000000 --- a/functions/dist/billing/onEntitlementChanged.js +++ /dev/null @@ -1,122 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onEntitlementChanged = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const push_1 = require("../notifications/push"); -const idempotency_1 = require("../notifications/idempotency"); -const log_1 = require("../log"); -/** - * Notifies the OTHER partner when a user GAINS premium, so couple-shared premium unlock isn't silent - * for the non-subscriber. (Future.md: `subscription_entitlement_changed`.) - * - * Path: users/{userId}/entitlements/premium (onWrite) - * Edge-triggered: fires only on the inactive→active transition, so renewals / repeated writes don't - * re-notify. Skips if the partner already had premium (the couple was already unlocked). - */ -function isActive(data) { - if (!data) - return false; - if (data.premium !== true) - return false; - const expiresAt = data.expiresAt; - if (expiresAt && expiresAt.toMillis() <= Date.now()) - return false; - return true; -} -exports.onEntitlementChanged = (0, firestore_1.onDocumentWritten)('users/{userId}/entitlements/premium', async (event) => { - var _a, _b, _c, _d; - const { userId } = event.params; - const before = isActive((_a = event.data) === null || _a === void 0 ? void 0 : _a.before.data()); - const after = isActive((_b = event.data) === null || _b === void 0 ? void 0 : _b.after.data()); - if (before || !after) - return; // only a genuine inactive→active gain - const db = admin.firestore(); - // Resolve this user's couple + partner. - const coupleSnap = await db - .collection('couples') - .where('userIds', 'array-contains', userId) - .limit(1) - .get(); - if (coupleSnap.empty) { - log_1.logger.log(`[onEntitlementChanged] no couple for ${userId}`); - return; - } - const coupleDoc = coupleSnap.docs[0]; - const coupleId = coupleDoc.id; - const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []); - const partnerId = userIds.find((id) => id !== userId); - if (!partnerId) { - log_1.logger.log(`[onEntitlementChanged] no partner for ${userId} in ${coupleId}`); - return; - } - // If the partner already has premium the couple was already unlocked — nothing new to announce. - const partnerEnt = await db.doc(`users/${partnerId}/entitlements/premium`).get(); - if (isActive(partnerEnt.data())) { - log_1.logger.log(`[onEntitlementChanged] partner ${partnerId} already premium; skip`); - return; - } - // Dedupe redelivery of this onWrite (at-least-once) so the partner isn't double-notified and - // the in-app record isn't duplicated. - if (!(await (0, idempotency_1.claimOnce)((0, idempotency_1.notifMark)(db, coupleId, `entitlement-${userId}`)))) { - log_1.logger.log(`[onEntitlementChanged] already announced premium for ${userId}; skip`); - return; - } - // displayName is E2EE in users/{uid}, so use a generic label (this copy is stored server-side in - // notification_queue + the push, neither of which can decrypt). - const payload = { - type: 'subscription_entitlement_changed', - title: 'Premium unlocked ✨', - body: 'Your partner upgraded — you both have Premium now.', - }; - // In-app record for the partner. - await db - .collection('users') - .doc(partnerId) - .collection('notification_queue') - .add(Object.assign(Object.assign({}, payload), { read: false, createdAt: admin.firestore.FieldValue.serverTimestamp() })); - const res = await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { title: payload.title, body: payload.body }, - android: { notification: { channelId: 'partner_activity' } }, - data: { type: payload.type, couple_id: coupleId }, - }); - if (res.sent === 0 && res.failed === 0) { - log_1.logger.log(`[onEntitlementChanged] no FCM tokens for ${partnerId}`); - return; - } - log_1.logger.log(`[onEntitlementChanged] notified ${partnerId} of couple premium (${coupleId})`); -}); -//# sourceMappingURL=onEntitlementChanged.js.map \ No newline at end of file diff --git a/functions/dist/billing/onEntitlementChanged.js.map b/functions/dist/billing/onEntitlementChanged.js.map deleted file mode 100644 index f23696bd..00000000 --- a/functions/dist/billing/onEntitlementChanged.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onEntitlementChanged.js","sourceRoot":"","sources":["../../src/billing/onEntitlementChanged.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/billing/revenueCatWebhook.js b/functions/dist/billing/revenueCatWebhook.js deleted file mode 100644 index 37c4114b..00000000 --- a/functions/dist/billing/revenueCatWebhook.js +++ /dev/null @@ -1,179 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.AuthError = exports.ConfigError = exports.revenueCatWebhook = void 0; -exports.verifyWebhookSignature = verifyWebhookSignature; -const crypto = __importStar(require("crypto")); -const https_1 = require("firebase-functions/v2/https"); -const params_1 = require("firebase-functions/params"); -const entitlementLogic_1 = require("./entitlementLogic"); -const log_1 = require("../log"); -/** - * RevenueCat webhook handler. - * - * Path: POST /revenueCatWebhook (registered as an HTTPS function) - * Triggered by RevenueCat server-to-server events. - * - * Authentication — HMAC-SHA256 (RevenueCat's webhook "signature verification"): - * - RevenueCat sends `X-RevenueCat-Webhook-Signature: t=,v1=`. - * - v1 = HMAC-SHA256(secret, "."), hex-encoded, computed over the RAW request - * body bytes exactly as received (before any JSON parsing / re-serialization). - * - REVENUECAT_WEBHOOK_SECRET is the integration's signing secret from the RevenueCat - * dashboard (Integrations → Webhooks → enable signature verification). It is bound as a - * Secret Manager secret (below) and injected into process.env at runtime. - * - Missing secret → 500 (our config error). Missing/invalid signature, or a timestamp - * outside the tolerance window (replay guard) → 401. - * - * NOTE: RevenueCat does NOT offer an Ed25519 / public-key signing scheme — HMAC-SHA256 with a - * shared secret is the only cryptographic option (plus the optional static Authorization header). - * - * Processing: - * - Parses the RevenueCat event payload. - * - Writes entitlement state to Firestore at users/{userId}/entitlements. - */ -const revenueCatWebhookSecret = (0, params_1.defineSecret)('REVENUECAT_WEBHOOK_SECRET'); -/** Reject signatures whose timestamp is further than this from now (replay protection). */ -const SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000; -exports.revenueCatWebhook = (0, https_1.onRequest)({ secrets: [revenueCatWebhookSecret] }, async (req, res) => { - var _a; - if (req.method !== 'POST') { - res.status(405).json({ error: 'method_not_allowed' }); - return; - } - try { - verifyRequest(req, Date.now()); - } - catch (err) { - if (err instanceof ConfigError) { - log_1.logger.error('[revenueCatWebhook] configuration error', { error: err.message }); - res.status(500).json({ error: 'internal_error' }); - return; - } - res.status(401).json({ error: 'unauthorized' }); - return; - } - const event = (_a = req.body) === null || _a === void 0 ? void 0 : _a.event; - if (!event || !event.type || !event.app_user_id) { - res.status(400).json({ error: 'malformed_payload' }); - return; - } - // Security review Batch 2: process BEFORE acking. Previously we returned 200 up front - // and only logged failures, so a failed entitlement sync was silently dropped (no retry). - // RevenueCat retries on non-2xx, and applyEntitlementEvent is idempotent (it sets state), - // so returning 500 on failure recovers the event safely. - try { - await (0, entitlementLogic_1.applyEntitlementEvent)(event); - } - catch (err) { - log_1.logger.error('[revenueCatWebhook] entitlement sync failed', { error: String(err) }); - res.status(500).json({ error: 'processing_failed' }); - return; - } - res.status(200).json({ received: true }); -}); -class ConfigError extends Error { -} -exports.ConfigError = ConfigError; -class AuthError extends Error { -} -exports.AuthError = AuthError; -/** - * Reads the secret + signature header off the request and delegates to the pure verifier. - * Throws ConfigError when our secret is unset (→500); AuthError for any auth failure (→401). - */ -function verifyRequest(req, nowMs) { - const secret = process.env.REVENUECAT_WEBHOOK_SECRET; - if (!secret) { - throw new ConfigError('REVENUECAT_WEBHOOK_SECRET environment variable is not set'); - } - // Firebase Functions expose the raw request body as req.rawBody (Buffer). - const rawBody = req.rawBody; - const sigHeader = req.headers['x-revenuecat-webhook-signature']; - const header = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader; - verifyWebhookSignature({ rawBody, header, secret, nowMs }); -} -/** - * Pure, testable HMAC-SHA256 verification of a RevenueCat webhook signature. - * - * Parses `t=,v1=` from the header, recomputes HMAC-SHA256 over "." - * with the signing secret, constant-time-compares against v1, and rejects timestamps outside - * the tolerance window. Throws AuthError on any failure (caller maps to 401). - */ -function verifyWebhookSignature(opts) { - var _a; - const { rawBody, header, secret, nowMs } = opts; - const toleranceMs = (_a = opts.toleranceMs) !== null && _a !== void 0 ? _a : SIGNATURE_TOLERANCE_MS; - if (!rawBody || rawBody.length === 0) { - throw new AuthError('request body missing'); - } - if (!header) { - throw new AuthError('signature header missing'); - } - // Parse "t=,v1=" — order-independent, tolerant of extra fields. - let t; - let v1; - for (const part of header.split(',')) { - const seg = part.trim(); - const eq = seg.indexOf('='); - if (eq < 0) - continue; - const key = seg.slice(0, eq); - const val = seg.slice(eq + 1); - if (key === 't') - t = val; - else if (key === 'v1') - v1 = val; - } - if (!t || !v1) { - throw new AuthError('signature header malformed'); - } - const tsMs = Number(t) * 1000; - if (!Number.isFinite(tsMs)) { - throw new AuthError('signature timestamp invalid'); - } - if (Math.abs(nowMs - tsMs) > toleranceMs) { - throw new AuthError('signature timestamp outside tolerance'); - } - // HMAC over the RAW body bytes, prefixed with "." — never re-serialize the JSON. - const signedPayload = Buffer.concat([Buffer.from(`${t}.`, 'utf8'), rawBody]); - const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex'); - // Constant-time compare. timingSafeEqual throws on length mismatch, so guard the length first. - const expectedBuf = Buffer.from(expected, 'utf8'); - const providedBuf = Buffer.from(v1, 'utf8'); - if (expectedBuf.length !== providedBuf.length || !crypto.timingSafeEqual(expectedBuf, providedBuf)) { - throw new AuthError('signature mismatch'); - } -} -//# sourceMappingURL=revenueCatWebhook.js.map \ No newline at end of file diff --git a/functions/dist/billing/revenueCatWebhook.js.map b/functions/dist/billing/revenueCatWebhook.js.map deleted file mode 100644 index 3a9b4301..00000000 --- a/functions/dist/billing/revenueCatWebhook.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"revenueCatWebhook.js","sourceRoot":"","sources":["../../src/billing/revenueCatWebhook.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/billing/revenueCatWebhook.test.js b/functions/dist/billing/revenueCatWebhook.test.js deleted file mode 100644 index 84a98b6c..00000000 --- a/functions/dist/billing/revenueCatWebhook.test.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -// The verifier under test is pure (crypto only), so we stub the Firebase side-effect -// imports that run at module load rather than exercising them. -jest.mock('firebase-functions/v2/https', () => ({ onRequest: jest.fn() })); -jest.mock('firebase-functions/params', () => ({ defineSecret: jest.fn(() => ({})) })); -jest.mock('../log', () => ({ logger: { log: jest.fn(), error: jest.fn(), warn: jest.fn() } })); -jest.mock('./entitlementLogic', () => ({ applyEntitlementEvent: jest.fn() })); -const crypto = __importStar(require("crypto")); -const revenueCatWebhook_1 = require("./revenueCatWebhook"); -const SECRET = 'whsec_test_shared_secret'; -const NOW = 1700000000000; // fixed "now" in ms -const nowSec = Math.floor(NOW / 1000); -const BODY = JSON.stringify({ event: { type: 'INITIAL_PURCHASE', app_user_id: 'uid1' } }); -/** Build the header RevenueCat would send: `t=,v1=.">`. */ -function sign(body, tsSec, secret = SECRET) { - const raw = Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf8'); - const payload = Buffer.concat([Buffer.from(`${tsSec}.`, 'utf8'), raw]); - const v1 = crypto.createHmac('sha256', secret).update(payload).digest('hex'); - return `t=${tsSec},v1=${v1}`; -} -function verify(overrides = {}) { - return (0, revenueCatWebhook_1.verifyWebhookSignature)(Object.assign({ rawBody: Buffer.from(BODY), header: sign(BODY, nowSec), secret: SECRET, nowMs: NOW }, overrides)); -} -describe('verifyWebhookSignature', () => { - it('accepts a valid signature', () => { - expect(() => verify()).not.toThrow(); - }); - it('accepts a timestamp within the tolerance window', () => { - expect(() => verify({ header: sign(BODY, nowSec - 4 * 60) })).not.toThrow(); - }); - it('rejects a tampered body', () => { - expect(() => verify({ rawBody: Buffer.from(BODY + ' ') })).toThrow(revenueCatWebhook_1.AuthError); - }); - it('rejects a signature made with the wrong secret', () => { - expect(() => verify({ header: sign(BODY, nowSec, 'wrong-secret') })).toThrow(revenueCatWebhook_1.AuthError); - }); - it('rejects a missing header', () => { - expect(() => verify({ header: undefined })).toThrow(revenueCatWebhook_1.AuthError); - }); - it('rejects a missing body', () => { - expect(() => verify({ rawBody: undefined })).toThrow(revenueCatWebhook_1.AuthError); - }); - it('rejects a header missing v1', () => { - expect(() => verify({ header: `t=${nowSec}` })).toThrow(revenueCatWebhook_1.AuthError); - }); - it('rejects a stale timestamp (replay guard)', () => { - expect(() => verify({ header: sign(BODY, nowSec - 10 * 60) })).toThrow(revenueCatWebhook_1.AuthError); - }); - it('rejects a far-future timestamp', () => { - expect(() => verify({ header: sign(BODY, nowSec + 10 * 60) })).toThrow(revenueCatWebhook_1.AuthError); - }); - it('rejects a wrong-length signature without crashing (timingSafeEqual guard)', () => { - expect(() => verify({ header: `t=${nowSec},v1=deadbeef` })).toThrow(revenueCatWebhook_1.AuthError); - }); -}); -//# sourceMappingURL=revenueCatWebhook.test.js.map \ No newline at end of file diff --git a/functions/dist/billing/revenueCatWebhook.test.js.map b/functions/dist/billing/revenueCatWebhook.test.js.map deleted file mode 100644 index cc8acb20..00000000 --- a/functions/dist/billing/revenueCatWebhook.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"revenueCatWebhook.test.js","sourceRoot":"","sources":["../../src/billing/revenueCatWebhook.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/billing/syncEntitlement.js b/functions/dist/billing/syncEntitlement.js deleted file mode 100644 index faa3a4f7..00000000 --- a/functions/dist/billing/syncEntitlement.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.syncEntitlement = void 0; -const https_1 = require("firebase-functions/v2/https"); -const entitlementLogic_1 = require("./entitlementLogic"); -const log_1 = require("../log"); -/** - * Callable function that forces a re-sync of entitlements for the - * authenticated caller. - * - * Request body: { } (auth context supplies uid) - * Response: EntitlementState - * - * The client can invoke this after a purchase/restore or when local - * entitlement state appears stale. In production this should fetch the - * latest entitlement state from RevenueCat; for now it computes the - * state from the existing Firestore document and rewrites it, ensuring - * consistent field shapes. - */ -exports.syncEntitlement = (0, https_1.onCall)(async (request) => { - var _a; - if (!((_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid)) { - throw new https_1.HttpsError('unauthenticated', 'Caller must be authenticated.'); - } - const userId = request.auth.uid; - try { - const state = await (0, entitlementLogic_1.applyEntitlementSync)(userId); - return state; - } - catch (err) { - log_1.logger.error('[syncEntitlement] failed', { error: String(err) }); - throw new https_1.HttpsError('internal', 'Entitlement sync failed.'); - } -}); -//# sourceMappingURL=syncEntitlement.js.map \ No newline at end of file diff --git a/functions/dist/billing/syncEntitlement.js.map b/functions/dist/billing/syncEntitlement.js.map deleted file mode 100644 index 06ac9781..00000000 --- a/functions/dist/billing/syncEntitlement.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"syncEntitlement.js","sourceRoot":"","sources":["../../src/billing/syncEntitlement.ts"],"names":[],"mappings":";;;AAAA,uDAAgE;AAChE,yDAA2E;AAC3E,gCAA+B;AAE/B;;;;;;;;;;;;GAYG;AACU,QAAA,eAAe,GAAG,IAAA,cAAM,EAAC,KAAK,EAAE,OAAO,EAAE,EAAE;;IACtD,IAAI,CAAC,CAAA,MAAA,OAAO,CAAC,IAAI,0CAAE,GAAG,CAAA,EAAE,CAAC;QACvB,MAAM,IAAI,kBAAU,CAAC,iBAAiB,EAAE,+BAA+B,CAAC,CAAA;IAC1E,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAA;IAE/B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,IAAA,uCAAoB,EAAC,MAAM,CAAC,CAAA;QAChD,OAAO,KAAgC,CAAA;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,YAAM,CAAC,KAAK,CAAC,0BAA0B,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAChE,MAAM,IAAI,kBAAU,CAAC,UAAU,EAAE,0BAA0B,CAAC,CAAA;IAC9D,CAAC;AACH,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/functions/dist/couples/acceptInviteCallable.js b/functions/dist/couples/acceptInviteCallable.js deleted file mode 100644 index 1f30e6db..00000000 --- a/functions/dist/couples/acceptInviteCallable.js +++ /dev/null @@ -1,213 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.acceptInviteCallable = void 0; -const admin = __importStar(require("firebase-admin")); -const https_1 = require("firebase-functions/v2/https"); -const push_1 = require("../notifications/push"); -const log_1 = require("../log"); -/** - * HTTPS callable that mediates invite acceptance. - * - * Issue #9 fix: clients are no longer allowed to read invites directly, because - * the 6-character document ID was enumerable. The invite is looked up by code - * server-side, validated, and accepted atomically here. - * - * Request body: { code: string } - * - code: the 6-character invite code the partner shared. - * - * The recovery phrase is stored on the invite document by the inviter and returned - * directly — the acceptor never needs to type it manually. - * - * Response: { coupleId, inviterUserId, wrappedCoupleKey, kdfSalt, kdfParams, encryptedRecoveryPhrase } - * - encryptedRecoveryPhrase: the Argon2id+AES-GCM blob stored by the inviter. The acceptor - * decrypts it client-side using the invite code they entered. The server never sees - * the plaintext phrase. - * - * Operations (all via Admin SDK, so Firestore rules are bypassed): - * 1. Verify caller is authenticated and not already paired. - * 2. Rate-limit accept attempts per UID. - * 3. Look up the invite document by code. - * 4. Validate status == 'pending' and not expired. - * 5. Prevent self-acceptance. - * 6. Create the couple document with the wrapped couple key from the invite. - * 7. Update both user documents with the new coupleId. - * 8. Mark the invite as accepted and wipe the encrypted phrase from the doc. - */ -const ACCEPT_RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; // 1 hour -const ACCEPT_RATE_LIMIT_MAX = 10; // 10 attempts per hour per user -const ACCEPT_ATTEMPT_TTL_MS = 25 * 60 * 60 * 1000; // 25h: rate window + 1h buffer; Firestore TTL cleans up after -exports.acceptInviteCallable = (0, https_1.onCall)(async (request) => { - var _a, _b, _c, _d; - const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); - } - if (!request.app) { - throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); - } - const code = (_b = request.data) === null || _b === void 0 ? void 0 : _b.code; - if (!code || typeof code !== 'string') { - throw new https_1.HttpsError('invalid-argument', 'code is required.'); - } - const db = admin.firestore(); - // Rate-limit accept attempts per caller to prevent brute-forcing 6-char codes. - const windowStart = admin.firestore.Timestamp.fromMillis(admin.firestore.Timestamp.now().toMillis() - ACCEPT_RATE_LIMIT_WINDOW_MS); - const recentAttempts = await db - .collection('users').doc(callerId) - .collection('invite_attempts') - .where('attemptedAt', '>=', windowStart) - .count() - .get(); - if (recentAttempts.data().count >= ACCEPT_RATE_LIMIT_MAX) { - throw new https_1.HttpsError('resource-exhausted', 'Too many code attempts. Try again later.'); - } - // Record this attempt before doing any work, so failures also count. - // expiresAt is the Firestore TTL field — see firestore.indexes.json fieldOverrides. - const attemptExpiresAt = admin.firestore.Timestamp.fromMillis(admin.firestore.Timestamp.now().toMillis() + ACCEPT_ATTEMPT_TTL_MS); - // The code is the KDF seed for the couple's recovery phrase — never store it raw. - await db.collection('users').doc(callerId).collection('invite_attempts').add({ - attemptedAt: admin.firestore.FieldValue.serverTimestamp(), - expiresAt: attemptExpiresAt, - }); - // Caller must not already be paired. - const callerDoc = await db.collection('users').doc(callerId).get(); - if (callerDoc.exists && ((_c = callerDoc.data()) === null || _c === void 0 ? void 0 : _c.coupleId) != null) { - throw new https_1.HttpsError('failed-precondition', 'Caller is already paired.'); - } - const inviteRef = db.collection('invites').doc(code); - const inviteDoc = await inviteRef.get(); - if (!inviteDoc.exists) { - throw new https_1.HttpsError('not-found', 'Invite not found.'); - } - const invite = (_d = inviteDoc.data()) !== null && _d !== void 0 ? _d : {}; - const inviterUserId = invite.inviterUserId; - const status = invite.status; - const expiresAt = invite.expiresAt; - const wrappedCoupleKey = invite.wrappedCoupleKey; - const kdfSalt = invite.kdfSalt; - const kdfParams = invite.kdfParams; - const encryptedRecoveryPhrase = invite.encryptedRecoveryPhrase; - if (status !== 'pending') { - throw new https_1.HttpsError('failed-precondition', 'Invite has already been used.'); - } - const now = admin.firestore.Timestamp.now(); - if (expiresAt != null && expiresAt.toMillis() <= now.toMillis()) { - throw new https_1.HttpsError('failed-precondition', 'Invite has expired.'); - } - if (!inviterUserId) { - throw new https_1.HttpsError('failed-precondition', 'Invite is missing inviterUserId.'); - } - if (inviterUserId === callerId) { - throw new https_1.HttpsError('permission-denied', 'Cannot accept your own invite.'); - } - const coupleId = db.collection('couples').doc().id; - const coupleRef = db.collection('couples').doc(coupleId); - // Strict E2EE only: a valid invite always carries a wrapped couple key. If it doesn't, - // the invite is malformed (or pre-dates strict E2EE) — reject rather than create a - // broken plaintext couple the client can't use. - if (wrappedCoupleKey == null || kdfSalt == null || kdfParams == null) { - throw new https_1.HttpsError('failed-precondition', 'Invite is missing encryption material. Ask your partner to create a new invite.'); - } - const encryptionVersion = 2; - const batch = db.batch(); - batch.set(coupleRef, { - id: coupleId, - userIds: [inviterUserId, callerId], - inviteCode: code, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - streakCount: 0, - encryptionVersion, - wrappedCoupleKey: wrappedCoupleKey !== null && wrappedCoupleKey !== void 0 ? wrappedCoupleKey : null, - kdfSalt: kdfSalt !== null && kdfSalt !== void 0 ? kdfSalt : null, - kdfParams: kdfParams !== null && kdfParams !== void 0 ? kdfParams : null, - }); - batch.update(db.collection('users').doc(inviterUserId), { coupleId }); - batch.update(db.collection('users').doc(callerId), { coupleId }); - // Wipe the encrypted phrase blob on acceptance — it has been returned to the acceptor - // who will decrypt it client-side. No reason to keep it in Firestore after use. - batch.update(inviteRef, { - status: 'accepted', - acceptedByUserId: callerId, - acceptedAt: admin.firestore.FieldValue.serverTimestamp(), - coupleId, - encryptedRecoveryPhrase: admin.firestore.FieldValue.delete(), - // Don't let the plaintext inviter name linger once the couple exists (the profile name is E2EE - // from here on). It was only needed for the pre-accept "X invited you" preview. - inviterDisplayName: admin.firestore.FieldValue.delete(), - }); - try { - await batch.commit(); - } - catch (e) { - if (e instanceof https_1.HttpsError) - throw e; - log_1.logger.error('[acceptInviteCallable] couple creation failed', { error: String(e) }); - throw new https_1.HttpsError('internal', 'Failed to accept invite. Please try again.'); - } - log_1.logger.log(`[acceptInviteCallable] ${callerId} accepted an invite; created couple ${coupleId}`); - // Notify the inviter that their partner joined. Await so gen 2 doesn't freeze the instance - // before the push completes; swallow errors so a failed push never fails the accept itself. - await notifyPartnerJoined(db, inviterUserId, coupleId).catch((e) => log_1.logger.warn('[acceptInviteCallable] partner_joined FCM failed', { error: String(e) })); - return { - coupleId, - inviterUserId, - wrappedCoupleKey: wrappedCoupleKey !== null && wrappedCoupleKey !== void 0 ? wrappedCoupleKey : null, - kdfSalt: kdfSalt !== null && kdfSalt !== void 0 ? kdfSalt : null, - kdfParams: kdfParams !== null && kdfParams !== void 0 ? kdfParams : null, - encryptedRecoveryPhrase: encryptedRecoveryPhrase !== null && encryptedRecoveryPhrase !== void 0 ? encryptedRecoveryPhrase : null, - }; -}); -async function notifyPartnerJoined(db, inviterUserId, coupleId) { - await db.collection('users').doc(inviterUserId).collection('notification_queue').add({ - type: 'partner_joined', - title: 'Your partner joined!', - body: "You're connected. Time to answer tonight's question together.", - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - await (0, push_1.sendPushToUser)(db, admin.messaging(), inviterUserId, { - notification: { - title: 'Your partner joined!', - body: "You're connected. Time to answer tonight's question together.", - }, - android: { notification: { channelId: 'partner_activity' } }, // E-OBS - data: { - type: 'partner_joined', - couple_id: coupleId, - }, - }); -} -//# sourceMappingURL=acceptInviteCallable.js.map \ No newline at end of file diff --git a/functions/dist/couples/acceptInviteCallable.js.map b/functions/dist/couples/acceptInviteCallable.js.map deleted file mode 100644 index 9f5c3660..00000000 --- a/functions/dist/couples/acceptInviteCallable.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"acceptInviteCallable.js","sourceRoot":"","sources":["../../src/couples/acceptInviteCallable.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAAuC;AACvC,uDAAiF;AACjF,gDAAsD;AACtD,gCAA+B;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,2BAA2B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA,CAAO,SAAS;AAClE,MAAM,qBAAqB,GAAG,EAAE,CAAA,CAA0B,gCAAgC;AAC1F,MAAM,qBAAqB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA,CAAQ,8DAA8D;AAE1G,QAAA,oBAAoB,GAAG,IAAA,cAAM,EAAC,KAAK,EAAE,OAAiD,EAAE,EAAE;;IACrG,MAAM,QAAQ,GAAG,MAAA,OAAO,CAAC,IAAI,0CAAE,GAAG,CAAA;IAClC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,kBAAU,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,CAAA;IAC/D,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACjB,MAAM,IAAI,kBAAU,CAAC,qBAAqB,EAAE,kCAAkC,CAAC,CAAA;IACjF,CAAC;IAED,MAAM,IAAI,GAAG,MAAA,OAAO,CAAC,IAAI,0CAAE,IAAI,CAAA;IAC/B,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtC,MAAM,IAAI,kBAAU,CAAC,kBAAkB,EAAE,mBAAmB,CAAC,CAAA;IAC/D,CAAC;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,+EAA+E;IAC/E,MAAM,WAAW,GAAG,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,UAAU,CACtD,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,GAAG,2BAA2B,CACzE,CAAA;IACD,MAAM,cAAc,GAAG,MAAM,EAAE;SAC5B,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;SACjC,UAAU,CAAC,iBAAiB,CAAC;SAC7B,KAAK,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC;SACvC,KAAK,EAAE;SACP,GAAG,EAAE,CAAA;IACR,IAAI,cAAc,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,qBAAqB,EAAE,CAAC;QACzD,MAAM,IAAI,kBAAU,CAAC,oBAAoB,EAAE,0CAA0C,CAAC,CAAA;IACxF,CAAC;IACD,qEAAqE;IACrE,oFAAoF;IACpF,MAAM,gBAAgB,GAAG,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,UAAU,CAC3D,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,GAAG,qBAAqB,CACnE,CAAA;IACD,kFAAkF;IAClF,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,GAAG,CAAC;QAC3E,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;QACzD,SAAS,EAAE,gBAAgB;KAC5B,CAAC,CAAA;IAEF,qCAAqC;IACrC,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IAClE,IAAI,SAAS,CAAC,MAAM,IAAI,CAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,QAAQ,KAAI,IAAI,EAAE,CAAC;QAC3D,MAAM,IAAI,kBAAU,CAAC,qBAAqB,EAAE,2BAA2B,CAAC,CAAA;IAC1E,CAAC;IAED,MAAM,SAAS,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACpD,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,CAAA;IAEvC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACtB,MAAM,IAAI,kBAAU,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAA;IACxD,CAAC;IAED,MAAM,MAAM,GAAG,MAAA,SAAS,CAAC,IAAI,EAAE,mCAAI,EAAE,CAAA;IACrC,MAAM,aAAa,GAAG,MAAM,CAAC,aAAmC,CAAA;IAChE,MAAM,MAAM,GAAG,MAAM,CAAC,MAA4B,CAAA;IAClD,MAAM,SAAS,GAAG,MAAM,CAAC,SAAkD,CAAA;IAC3E,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAsC,CAAA;IACtE,MAAM,OAAO,GAAG,MAAM,CAAC,OAA6B,CAAA;IACpD,MAAM,SAAS,GAAG,MAAM,CAAC,SAA+B,CAAA;IACxD,MAAM,uBAAuB,GAAG,MAAM,CAAC,uBAA6C,CAAA;IAEpF,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,kBAAU,CAAC,qBAAqB,EAAE,+BAA+B,CAAC,CAAA;IAC9E,CAAC;IAED,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,CAAA;IAC3C,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,CAAC,QAAQ,EAAE,IAAI,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC;QAChE,MAAM,IAAI,kBAAU,CAAC,qBAAqB,EAAE,qBAAqB,CAAC,CAAA;IACpE,CAAC;IAED,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,IAAI,kBAAU,CAAC,qBAAqB,EAAE,kCAAkC,CAAC,CAAA;IACjF,CAAC;IAED,IAAI,aAAa,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,kBAAU,CAAC,mBAAmB,EAAE,gCAAgC,CAAC,CAAA;IAC7E,CAAC;IAED,MAAM,QAAQ,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,CAAA;IAClD,MAAM,SAAS,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAExD,uFAAuF;IACvF,mFAAmF;IACnF,gDAAgD;IAChD,IAAI,gBAAgB,IAAI,IAAI,IAAI,OAAO,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;QACrE,MAAM,IAAI,kBAAU,CAClB,qBAAqB,EACrB,iFAAiF,CAClF,CAAA;IACH,CAAC;IACD,MAAM,iBAAiB,GAAG,CAAC,CAAA;IAE3B,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,EAAE,CAAA;IAExB,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE;QACnB,EAAE,EAAE,QAAQ;QACZ,OAAO,EAAE,CAAC,aAAa,EAAE,QAAQ,CAAC;QAClC,UAAU,EAAE,IAAI;QAChB,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;QACvD,WAAW,EAAE,CAAC;QACd,iBAAiB;QACjB,gBAAgB,EAAE,gBAAgB,aAAhB,gBAAgB,cAAhB,gBAAgB,GAAI,IAAI;QAC1C,OAAO,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,IAAI;QACxB,SAAS,EAAE,SAAS,aAAT,SAAS,cAAT,SAAS,GAAI,IAAI;KAC7B,CAAC,CAAA;IAEF,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAA;IACrE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAA;IAEhE,sFAAsF;IACtF,gFAAgF;IAChF,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE;QACtB,MAAM,EAAE,UAAU;QAClB,gBAAgB,EAAE,QAAQ;QAC1B,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;QACxD,QAAQ;QACR,uBAAuB,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE;QAC5D,+FAA+F;QAC/F,gFAAgF;QAChF,kBAAkB,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE;KACxD,CAAC,CAAA;IAEF,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,MAAM,EAAE,CAAA;IACtB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,YAAY,kBAAU;YAAE,MAAM,CAAC,CAAA;QACpC,YAAM,CAAC,KAAK,CAAC,+CAA+C,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QACnF,MAAM,IAAI,kBAAU,CAAC,UAAU,EAAE,4CAA4C,CAAC,CAAA;IAChF,CAAC;IAED,YAAM,CAAC,GAAG,CAAC,0BAA0B,QAAQ,uCAAuC,QAAQ,EAAE,CAAC,CAAA;IAE/F,2FAA2F;IAC3F,4FAA4F;IAC5F,MAAM,mBAAmB,CAAC,EAAE,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CACjE,YAAM,CAAC,IAAI,CAAC,kDAAkD,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CACtF,CAAA;IAED,OAAO;QACL,QAAQ;QACR,aAAa;QACb,gBAAgB,EAAE,gBAAgB,aAAhB,gBAAgB,cAAhB,gBAAgB,GAAI,IAAI;QAC1C,OAAO,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,IAAI;QACxB,SAAS,EAAE,SAAS,aAAT,SAAS,cAAT,SAAS,GAAI,IAAI;QAC5B,uBAAuB,EAAE,uBAAuB,aAAvB,uBAAuB,cAAvB,uBAAuB,GAAI,IAAI;KACzD,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,KAAK,UAAU,mBAAmB,CAChC,EAA6B,EAC7B,aAAqB,EACrB,QAAgB;IAEhB,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC;QACnF,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,sBAAsB;QAC7B,IAAI,EAAE,+DAA+D;QACrE,IAAI,EAAE,KAAK;QACX,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KACxD,CAAC,CAAA;IAEF,MAAM,IAAA,qBAAc,EAAC,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE;QACzD,YAAY,EAAE;YACZ,KAAK,EAAE,sBAAsB;YAC7B,IAAI,EAAE,+DAA+D;SACtE;QACD,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,EAAE,QAAQ;QACtE,IAAI,EAAE;YACJ,IAAI,EAAE,gBAAgB;YACtB,SAAS,EAAE,QAAQ;SACpB;KACF,CAAC,CAAA;AACJ,CAAC"} \ No newline at end of file diff --git a/functions/dist/couples/aggregateOutcomes.js b/functions/dist/couples/aggregateOutcomes.js deleted file mode 100644 index 0bbe2aa9..00000000 --- a/functions/dist/couples/aggregateOutcomes.js +++ /dev/null @@ -1,116 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.aggregateOutcomeStats = exports.MIN_COHORT = exports.FOLLOWUP_DAYS = exports.SCORE_KEYS = void 0; -exports.aggregate = aggregate; -exports.extractCoupleOutcome = extractCoupleOutcome; -const admin = __importStar(require("firebase-admin")); -const scheduler_1 = require("firebase-functions/v2/scheduler"); -const log_1 = require("../log"); -exports.SCORE_KEYS = ['connection', 'communication', 'intimacy', 'happiness']; -exports.FOLLOWUP_DAYS = ['day_30', 'day_60', 'day_90']; -exports.MIN_COHORT = 50; -function pct(n, total) { - return total === 0 ? 0 : Math.round((n / total) * 100); -} -/** - * Pure aggregation — no I/O, unit-tested. A window below [minCohort] couples is suppressed. - */ -function aggregate(couples, minCohort = exports.MIN_COHORT) { - const windows = {}; - for (const day of exports.FOLLOWUP_DAYS) { - const eligible = couples.filter((c) => c.deltas[day] != null); - const cohort = eligible.length; - if (cohort < minCohort) { - windows[day] = { cohort, suppressed: true }; - continue; - } - const feltCloser = eligible.filter((c) => { var _a, _b; return ((_b = (_a = c.deltas[day]) === null || _a === void 0 ? void 0 : _a.connection) !== null && _b !== void 0 ? _b : 0) > 0; }).length; - const byMetric = {}; - for (const metric of exports.SCORE_KEYS) { - const improved = eligible.filter((c) => { var _a, _b; return ((_b = (_a = c.deltas[day]) === null || _a === void 0 ? void 0 : _a[metric]) !== null && _b !== void 0 ? _b : 0) > 0; }).length; - byMetric[metric] = pct(improved, cohort); - } - windows[day] = { - cohort, - suppressed: false, - feltCloserPct: pct(feltCloser, cohort), - improvedPctByMetric: byMetric, - }; - } - return { minCohort, windows }; -} -/** Extracts the follow-up deltas from a couple's outcomes subcollection docs. */ -function extractCoupleOutcome(outcomeDocs) { - const deltas = {}; - for (const doc of outcomeDocs) { - if (exports.FOLLOWUP_DAYS.includes(doc.id)) { - const delta = doc.data.delta; - if (delta && typeof delta === 'object') - deltas[doc.id] = delta; - } - } - return { deltas }; -} -exports.aggregateOutcomeStats = (0, scheduler_1.onSchedule)({ schedule: 'every 24 hours', memory: '512MiB', timeoutSeconds: 300 }, async () => { - const db = admin.firestore(); - // Paginate the couple scan (was an unbounded couples.get()) and read each page's outcomes in - // parallel (was one serial outcomes.get() per couple → O(couples) round-trips). couples.length - // still counts every couple, so the aggregate + totalCouples are unchanged. - const couples = []; - const PAGE_SIZE = 200; - let lastDoc; - for (;;) { - let query = db.collection('couples').orderBy('__name__').limit(PAGE_SIZE); - if (lastDoc) - query = query.startAfter(lastDoc); - const page = await query.get(); - if (page.empty) - break; - const pageOutcomes = await Promise.all(page.docs.map(async (couple) => { - const outcomesSnap = await couple.ref.collection('outcomes').get(); - return extractCoupleOutcome(outcomesSnap.docs.map((d) => ({ id: d.id, data: d.data() }))); - })); - couples.push(...pageOutcomes); - lastDoc = page.docs[page.docs.length - 1]; - if (page.size < PAGE_SIZE) - break; - } - const stats = aggregate(couples); - await db.collection('aggregate_stats').doc('outcomes').set(Object.assign(Object.assign({}, stats), { totalCouples: couples.length, generatedAt: admin.firestore.Timestamp.now() })); - log_1.logger.log(`[aggregateOutcomeStats] ${couples.length} couples → ` + - exports.FOLLOWUP_DAYS.map((d) => `${d}:${stats.windows[d].suppressed ? 'suppressed' : stats.windows[d].feltCloserPct + '%'}`).join(' ')); -}); -//# sourceMappingURL=aggregateOutcomes.js.map \ No newline at end of file diff --git a/functions/dist/couples/aggregateOutcomes.js.map b/functions/dist/couples/aggregateOutcomes.js.map deleted file mode 100644 index cc70a62b..00000000 --- a/functions/dist/couples/aggregateOutcomes.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"aggregateOutcomes.js","sourceRoot":"","sources":["../../src/couples/aggregateOutcomes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/aggregateOutcomes.test.js b/functions/dist/couples/aggregateOutcomes.test.js deleted file mode 100644 index e8d2954a..00000000 --- a/functions/dist/couples/aggregateOutcomes.test.js +++ /dev/null @@ -1,69 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const aggregateOutcomes_1 = require("./aggregateOutcomes"); -/** Builds N couples that completed day_30 with the given connection delta. */ -function couplesWithConnectionDelta(n, connectionDelta) { - return Array.from({ length: n }, () => ({ - deltas: { day_30: { connection: connectionDelta, communication: 0, intimacy: 0, happiness: 0 } }, - })); -} -describe('aggregate', () => { - it('suppresses a window below the minimum cohort', () => { - const stats = (0, aggregateOutcomes_1.aggregate)(couplesWithConnectionDelta(aggregateOutcomes_1.MIN_COHORT - 1, 3)); - expect(stats.windows.day_30.suppressed).toBe(true); - expect(stats.windows.day_30.feltCloserPct).toBeUndefined(); - expect(stats.windows.day_30.cohort).toBe(aggregateOutcomes_1.MIN_COHORT - 1); - }); - it('reports percentages once the cohort meets the minimum', () => { - // 60 couples: 45 improved connection, 15 did not. - const improved = couplesWithConnectionDelta(45, 2); - const flat = couplesWithConnectionDelta(15, 0); - const stats = (0, aggregateOutcomes_1.aggregate)([...improved, ...flat]); - expect(stats.windows.day_30.suppressed).toBe(false); - expect(stats.windows.day_30.cohort).toBe(60); - expect(stats.windows.day_30.feltCloserPct).toBe(75); // 45/60 - }); - it('counts only couples eligible for each window', () => { - // day_60 has zero couples → suppressed; day_30 has enough. - const stats = (0, aggregateOutcomes_1.aggregate)(couplesWithConnectionDelta(aggregateOutcomes_1.MIN_COHORT, 1)); - expect(stats.windows.day_30.suppressed).toBe(false); - expect(stats.windows.day_60.suppressed).toBe(true); - expect(stats.windows.day_60.cohort).toBe(0); - }); - it('computes improved percentages per metric', () => { - var _a, _b, _c, _d; - const couples = Array.from({ length: aggregateOutcomes_1.MIN_COHORT }, (_, i) => ({ - deltas: { - day_30: { - connection: 1, - communication: i < aggregateOutcomes_1.MIN_COHORT / 2 ? 1 : 0, // exactly half improved - intimacy: 0, - happiness: -1, - }, - }, - })); - const stats = (0, aggregateOutcomes_1.aggregate)(couples); - expect((_a = stats.windows.day_30.improvedPctByMetric) === null || _a === void 0 ? void 0 : _a.connection).toBe(100); - expect((_b = stats.windows.day_30.improvedPctByMetric) === null || _b === void 0 ? void 0 : _b.communication).toBe(50); - expect((_c = stats.windows.day_30.improvedPctByMetric) === null || _c === void 0 ? void 0 : _c.intimacy).toBe(0); - expect((_d = stats.windows.day_30.improvedPctByMetric) === null || _d === void 0 ? void 0 : _d.happiness).toBe(0); - }); - it('emits no couple-identifying data — only counts and percentages', () => { - const stats = (0, aggregateOutcomes_1.aggregate)(couplesWithConnectionDelta(aggregateOutcomes_1.MIN_COHORT, 2)); - const json = JSON.stringify(stats); - expect(json).not.toMatch(/couple|uid|userId|score/i); - }); -}); -describe('extractCoupleOutcome', () => { - it('pulls only the follow-up deltas, ignoring day_0 and stray docs', () => { - const out = (0, aggregateOutcomes_1.extractCoupleOutcome)([ - { id: 'day_0', data: { baseline: { connection: 5 } } }, - { id: 'day_30', data: { delta: { connection: 2, communication: 1 } } }, - { id: 'random', data: { delta: { connection: 9 } } }, - ]); - expect(out.deltas.day_0).toBeUndefined(); - expect(out.deltas.day_30).toEqual({ connection: 2, communication: 1 }); - expect(out.deltas.random).toBeUndefined(); - }); -}); -//# sourceMappingURL=aggregateOutcomes.test.js.map \ No newline at end of file diff --git a/functions/dist/couples/aggregateOutcomes.test.js.map b/functions/dist/couples/aggregateOutcomes.test.js.map deleted file mode 100644 index 60fa06da..00000000 --- a/functions/dist/couples/aggregateOutcomes.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"aggregateOutcomes.test.js","sourceRoot":"","sources":["../../src/couples/aggregateOutcomes.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/createInviteCallable.js b/functions/dist/couples/createInviteCallable.js deleted file mode 100644 index 4f5dd0b9..00000000 --- a/functions/dist/couples/createInviteCallable.js +++ /dev/null @@ -1,190 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createInviteCallable = void 0; -const admin = __importStar(require("firebase-admin")); -const https_1 = require("firebase-functions/v2/https"); -const log_1 = require("../log"); -/** - * HTTPS callable that creates a secure invite code. - * - * Issue #9 / review2.md Risk #1 fix: clients are no longer allowed to create - * invites directly. 6-character document IDs are enumerable, so a direct client - * write would expose pending invites to scanning. - * - * Request body: { code?: string, wrappedCoupleKey?: string, kdfSalt?: string, kdfParams?: string, encryptedRecoveryPhrase?: string } - * - code: client-supplied 6-character code (Android). The server validates uniqueness and - * returns an error if taken so the client can retry with a new code. If omitted (iOS), - * the server generates the code. - * - wrappedCoupleKey: base64-encoded couple key wrapped by the inviter's KDF - * - kdfSalt: base64 KDF salt - * - kdfParams: KDF parameter tag (e.g. argon2id;v=19;m=47104;t=3;p=1) - * - encryptedRecoveryPhrase: Argon2id+AES-GCM blob produced by the client using the invite - * code as the KDF input. The server stores it opaquely and never sees the plaintext phrase. - * - * Strict E2EE: code, wrappedCoupleKey, kdfSalt, kdfParams, and encryptedRecoveryPhrase are - * all required. There is no plaintext-couple path. - * - * Response: { code: string, expiresAt: Timestamp } - * - * Operations (all via Admin SDK, so Firestore rules are bypassed): - * 1. Verify caller is authenticated and not already paired. - * 2. Rate-limit the caller to 5 invite creations per rolling hour. - * 3. Use client-supplied code or generate one server-side; validate uniqueness via transaction. - * 4. Write the invite document with a 24-hour TTL. - */ -const CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; -const CODE_LENGTH = 6; -const INVITE_TTL_MS = 24 * 60 * 60 * 1000; -const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; -const RATE_LIMIT_MAX = 5; -function generateCode() { - let code = ''; - const randomValues = Buffer.alloc(CODE_LENGTH); - // crypto.randomBytes is synchronous and suitable for Cloud Functions. - require('crypto').randomFillSync(randomValues); - for (let i = 0; i < CODE_LENGTH; i++) { - code += CODE_CHARS[randomValues[i] % CODE_CHARS.length]; - } - return code; -} -exports.createInviteCallable = (0, https_1.onCall)(async (request) => { - var _a, _b, _c; - const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); - } - if (!request.app) { - throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); - } - const data = request.data; - const db = admin.firestore(); - // Caller must not already be paired. - const callerDoc = await db.collection('users').doc(callerId).get(); - if (callerDoc.exists && ((_b = callerDoc.data()) === null || _b === void 0 ? void 0 : _b.coupleId) != null) { - throw new https_1.HttpsError('failed-precondition', 'Caller is already paired.'); - } - const callerDisplayName = (_c = callerDoc.data()) === null || _c === void 0 ? void 0 : _c.displayName; - // Rate limit: count invites created by this user in the last hour. - const now = admin.firestore.Timestamp.now(); - const windowStart = admin.firestore.Timestamp.fromMillis(now.toMillis() - RATE_LIMIT_WINDOW_MS); - const recentInvitesQuery = db - .collection('invites') - .where('inviterUserId', '==', callerId) - .where('createdAt', '>=', windowStart) - .orderBy('createdAt', 'desc') - .limit(RATE_LIMIT_MAX + 1); - const recentInvites = await recentInvitesQuery.get(); - if (recentInvites.size >= RATE_LIMIT_MAX) { - throw new https_1.HttpsError('resource-exhausted', 'Too many invites created. Try again later.'); - } - const clientCode = data === null || data === void 0 ? void 0 : data.code; - const wrappedCoupleKey = data === null || data === void 0 ? void 0 : data.wrappedCoupleKey; - const kdfSalt = data === null || data === void 0 ? void 0 : data.kdfSalt; - const kdfParams = data === null || data === void 0 ? void 0 : data.kdfParams; - const encryptedRecoveryPhrase = data === null || data === void 0 ? void 0 : data.encryptedRecoveryPhrase; - // Strict E2EE: every couple must be created with a wrapped couple key. The client-supplied - // code, wrapped key, KDF salt/params, and encrypted recovery phrase are all required. - if (!clientCode) { - throw new https_1.HttpsError('invalid-argument', 'code is required.'); - } - // Security review Batch 2: validate the code is exactly the 6-char Crockford-style - // alphabet the client generates (CODE_CHARS, no I/O/0/1). Rejects malformed/oversized - // codes and anything that could be abused as the document id. - if (!/^[A-HJ-NP-Z2-9]{6}$/.test(clientCode)) { - throw new https_1.HttpsError('invalid-argument', 'code must be 6 valid characters.'); - } - if (wrappedCoupleKey == null || kdfSalt == null || kdfParams == null || encryptedRecoveryPhrase == null) { - throw new https_1.HttpsError('invalid-argument', 'E2EE fields (wrappedCoupleKey, kdfSalt, kdfParams, encryptedRecoveryPhrase) are required.'); - } - const expiresAt = admin.firestore.Timestamp.fromMillis(now.toMillis() + INVITE_TTL_MS); - // Android supplies its own code (used as the KDF input for phrase encryption, so the server - // must use it as-is). iOS omits the code; the server generates one in that case. - // Either way, validate uniqueness via transaction and return an error on collision so the - // client can retry with a fresh code. - let inviteRef = null; - let code = null; - const candidates = clientCode ? [clientCode] : Array.from({ length: 10 }, generateCode); - for (const candidate of candidates) { - const candidateRef = db.collection('invites').doc(candidate); - // eslint-disable-next-line no-await-in-loop - const created = await db.runTransaction(async (tx) => { - const snap = await tx.get(candidateRef); - if (snap.exists) - return false; - tx.set(candidateRef, { - code: candidate, - inviterUserId: callerId, - inviterDisplayName: callerDisplayName !== null && callerDisplayName !== void 0 ? callerDisplayName : null, - status: 'pending', - createdAt: admin.firestore.FieldValue.serverTimestamp(), - expiresAt, - usedAt: null, - usedByUserId: null, - wrappedCoupleKey: wrappedCoupleKey !== null && wrappedCoupleKey !== void 0 ? wrappedCoupleKey : null, - kdfSalt: kdfSalt !== null && kdfSalt !== void 0 ? kdfSalt : null, - kdfParams: kdfParams !== null && kdfParams !== void 0 ? kdfParams : null, - encryptedRecoveryPhrase: encryptedRecoveryPhrase !== null && encryptedRecoveryPhrase !== void 0 ? encryptedRecoveryPhrase : null, - }); - return true; - }); - if (created) { - code = candidate; - inviteRef = candidateRef; - break; - } - } - if (!code || !inviteRef) { - // Client-supplied code collided; the Android client will retry with a new code. - throw new https_1.HttpsError('already-exists', 'Invite code is already taken. Please try again.'); - } - // Write a server-side audit log entry for the inviter. This is not read by - // clients and supports the rate-limit count as well as future abuse review. - try { - // Do not store the raw code — it is the KDF seed for the couple's recovery phrase. - await db.collection('users').doc(callerId).collection('notification_queue').add({ - type: 'invite_created', - createdAt: admin.firestore.FieldValue.serverTimestamp(), - read: true, - }); - } - catch (err) { - // Audit write is best-effort; do not fail the invite if it errors. - log_1.logger.warn(`[createInviteCallable] audit log failed for ${callerId}`, { error: String(err) }); - } - log_1.logger.log(`[createInviteCallable] ${callerId} created an invite; expires ${expiresAt.toDate().toISOString()}`); - return { code, expiresAt }; -}); -//# sourceMappingURL=createInviteCallable.js.map \ No newline at end of file diff --git a/functions/dist/couples/createInviteCallable.js.map b/functions/dist/couples/createInviteCallable.js.map deleted file mode 100644 index 4d324595..00000000 --- a/functions/dist/couples/createInviteCallable.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"createInviteCallable.js","sourceRoot":"","sources":["../../src/couples/createInviteCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/leaveCoupleCallable.js b/functions/dist/couples/leaveCoupleCallable.js deleted file mode 100644 index 43a6a4b2..00000000 --- a/functions/dist/couples/leaveCoupleCallable.js +++ /dev/null @@ -1,125 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.leaveCoupleCallable = void 0; -const admin = __importStar(require("firebase-admin")); -const https_1 = require("firebase-functions/v2/https"); -const log_1 = require("../log"); -/** - * HTTPS callable that atomically unlinks a couple. - * - * The client cannot do this directly because Firestore rules prevent a user - * from writing the partner's user document. The Admin SDK bypasses those rules. - * - * Steps: - * 1. Verify the caller is a member of their current couple. - * 2. Clear coupleId on both user docs (batch — atomic). - * 3. Recursively delete the couple doc and all its subcollections. - * - * The existing onCoupleLeave Firestore trigger fires after step 2 and handles - * partner notification, so we don't duplicate that here. - */ -exports.leaveCoupleCallable = (0, https_1.onCall)(async (request) => { - var _a, _b; - const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); - } - if (!request.app) { - throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); - } - const db = admin.firestore(); - const userDoc = await db.collection('users').doc(callerId).get(); - const coupleId = (_b = userDoc.data()) === null || _b === void 0 ? void 0 : _b.coupleId; - if (!coupleId) { - // Already unpaired — idempotent success. - return { success: true }; - } - const coupleRef = db.collection('couples').doc(coupleId); - // Security review Batch 2: do the membership check, member-clearing, and couple-doc - // delete in one transaction so two partners leaving concurrently can't clobber state. - // Critically, only clear a member's coupleId if it STILL points at this couple — a - // stale concurrent call must never wipe a coupleId set by a fresh re-pair. - let result; - try { - result = await db.runTransaction(async (tx) => { - var _a, _b, _c; - const coupleSnap = await tx.get(coupleRef); - if (!coupleSnap.exists) { - const callerRef = db.collection('users').doc(callerId); - const callerSnap = await tx.get(callerRef); - if (((_a = callerSnap.data()) === null || _a === void 0 ? void 0 : _a.coupleId) === coupleId) { - tx.update(callerRef, { coupleId: null }); - } - return { membership: true }; - } - const userIds = ((_c = (_b = coupleSnap.data()) === null || _b === void 0 ? void 0 : _b.userIds) !== null && _c !== void 0 ? _c : []); - if (!userIds.includes(callerId)) { - return { membership: false }; - } - // Reads must precede writes in a transaction: snapshot every member first. - const memberSnaps = await Promise.all(userIds.map((uid) => tx.get(db.collection('users').doc(uid)))); - memberSnaps.forEach((snap, i) => { - var _a; - if (((_a = snap.data()) === null || _a === void 0 ? void 0 : _a.coupleId) === coupleId) { - tx.update(db.collection('users').doc(userIds[i]), { coupleId: null }); - } - }); - tx.delete(coupleRef); - return { membership: true }; - }); - } - catch (e) { - if (e instanceof https_1.HttpsError) - throw e; - log_1.logger.error('[leaveCoupleCallable] leave transaction failed', { error: String(e) }); - throw new https_1.HttpsError('internal', 'Failed to leave couple. Please try again.'); - } - if (!result.membership) { - throw new https_1.HttpsError('permission-denied', 'Not a member of this couple.'); - } - // Couple doc is deleted in the transaction; sweep any subcollections left behind. - // Best-effort + idempotent — the leave already succeeded, so a failed sweep must not - // surface as an error to the caller (orphaned subcollections can be reaped later). - try { - await db.recursiveDelete(coupleRef); - } - catch (e) { - log_1.logger.warn('[leaveCoupleCallable] subcollection sweep failed (ignored)', { error: String(e) }); - } - log_1.logger.log(`[leaveCoupleCallable] user ${callerId} left couple ${coupleId}`); - return { success: true }; -}); -//# sourceMappingURL=leaveCoupleCallable.js.map \ No newline at end of file diff --git a/functions/dist/couples/leaveCoupleCallable.js.map b/functions/dist/couples/leaveCoupleCallable.js.map deleted file mode 100644 index e874a0f2..00000000 --- a/functions/dist/couples/leaveCoupleCallable.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"leaveCoupleCallable.js","sourceRoot":"","sources":["../../src/couples/leaveCoupleCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/onCoupleKeyRotated.js b/functions/dist/couples/onCoupleKeyRotated.js deleted file mode 100644 index d635b681..00000000 --- a/functions/dist/couples/onCoupleKeyRotated.js +++ /dev/null @@ -1,110 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onCoupleKeyRotated = void 0; -exports.isKeyGenerationIncrease = isKeyGenerationIncrease; -exports.isPhrasePublished = isPhrasePublished; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const queueAndPush_1 = require("../notifications/queueAndPush"); -const log_1 = require("../log"); -/** - * Fires when a couple's key generation increases — one member rotated the couple key. - * - * Two jobs in one push, sent to BOTH members (every device): - * 1. Security signal ("was this you?" philosophy, same class as the restore self-alerts — bypasses - * quiet hours, no preference toggle): a key rotation is worth knowing about even when benign. - * 2. Functional pickup nudge: the partner's closed app still holds only the old key and cannot - * decrypt anything written under the new one until it next loads Home — the tap gets them there, - * where the existing adoption path unwraps the rotated keyset. No key material is read or logged. - */ -/** Pure edge guard: fire only when keyGeneration strictly increases (ignores every other update). */ -function isKeyGenerationIncrease(before, after) { - const b = typeof before === 'number' ? before : 0; - const a = typeof after === 'number' ? after : 0; - return a > b; -} -/** - * Pure edge guard for a newly PUBLISHED recovery phrase (phase 1 of the change handshake). - * - * Deliberately keyed on `phraseGeneration`, not `phraseWrapGeneration`: the publish is the moment the - * partner needs to act on, and their ack is what lets the change finish at all. Same shape as - * isKeyGenerationIncrease so the two read alike. - */ -function isPhrasePublished(before, after) { - const b = typeof before === 'number' ? before : 0; - const a = typeof after === 'number' ? after : 0; - return a > b; -} -exports.onCoupleKeyRotated = (0, firestore_1.onDocumentUpdated)('couples/{coupleId}', async (event) => { - var _a, _b, _c; - const before = (_a = event.data) === null || _a === void 0 ? void 0 : _a.before.data(); - const after = (_b = event.data) === null || _b === void 0 ? void 0 : _b.after.data(); - const rotated = isKeyGenerationIncrease(before === null || before === void 0 ? void 0 : before.keyGeneration, after === null || after === void 0 ? void 0 : after.keyGeneration); - const phrasePublished = isPhrasePublished(before === null || before === void 0 ? void 0 : before.phraseGeneration, after === null || after === void 0 ? void 0 : after.phraseGeneration); - if (!rotated && !phrasePublished) - return; - const { coupleId } = event.params; - const db = admin.firestore(); - const userIds = ((_c = after === null || after === void 0 ? void 0 : after.userIds) !== null && _c !== void 0 ? _c : []); - if (userIds.length === 0) - return; - // A rotation and a phrase publish are separate write shapes (the rules forbid combining them), so - // exactly one of these is true per event. - const alert = rotated - ? { - type: 'couple_key_rotated', - title: '🔑 Security update', - body: 'Your shared key was rotated. Open the app and everything continues as normal.', - } - : { - // Not merely informational: the change CANNOT complete until the partner's app opens and - // confirms it can read the new phrase. This push is what gets them there. And if they didn't - // expect a phrase change, this is how they find out — the was-this-you philosophy again. - type: 'recovery_phrase_changed', - title: '🔑 Security update', - body: 'Your recovery phrase was changed. Open the app to finish saving the new one.', - }; - log_1.logger.log(`[onCoupleKeyRotated] couple=${coupleId} ` + - `${rotated ? `generation=${after === null || after === void 0 ? void 0 : after.keyGeneration}` : `phraseGeneration=${after === null || after === void 0 ? void 0 : after.phraseGeneration}`}`); - // Per-member isolation: one failed send must not cost the other member their alert. - const results = await Promise.allSettled(userIds.map((uid) => (0, queueAndPush_1.queueAndPush)(db, uid, Object.assign(Object.assign({}, alert), { coupleId, bypassQuietHours: true })))); - results.forEach((r, i) => { - if (r.status === 'rejected') { - log_1.logger.warn('[onCoupleKeyRotated] alert failed', { uid: userIds[i], error: String(r.reason) }); - } - }); -}); -//# sourceMappingURL=onCoupleKeyRotated.js.map \ No newline at end of file diff --git a/functions/dist/couples/onCoupleKeyRotated.js.map b/functions/dist/couples/onCoupleKeyRotated.js.map deleted file mode 100644 index 5b2724ec..00000000 --- a/functions/dist/couples/onCoupleKeyRotated.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onCoupleKeyRotated.js","sourceRoot":"","sources":["../../src/couples/onCoupleKeyRotated.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/onCoupleKeyRotated.test.js b/functions/dist/couples/onCoupleKeyRotated.test.js deleted file mode 100644 index f805db99..00000000 --- a/functions/dist/couples/onCoupleKeyRotated.test.js +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const onCoupleKeyRotated_1 = require("./onCoupleKeyRotated"); -describe('isKeyGenerationIncrease', () => { - it('fires on a genuine rotation (0 → 1, and every later bump)', () => { - expect((0, onCoupleKeyRotated_1.isKeyGenerationIncrease)(undefined, 1)).toBe(true); - expect((0, onCoupleKeyRotated_1.isKeyGenerationIncrease)(0, 1)).toBe(true); - expect((0, onCoupleKeyRotated_1.isKeyGenerationIncrease)(3, 4)).toBe(true); - }); - it('ignores every non-rotation update to the couple doc', () => { - // The trigger watches the whole couple doc — streaks, rhythm, recovery re-wraps all pass through here. - expect((0, onCoupleKeyRotated_1.isKeyGenerationIncrease)(undefined, undefined)).toBe(false); - expect((0, onCoupleKeyRotated_1.isKeyGenerationIncrease)(2, 2)).toBe(false); - expect((0, onCoupleKeyRotated_1.isKeyGenerationIncrease)(0, undefined)).toBe(false); - }); - it('never fires on a downgrade — rules forbid it, but a redelivered stale event must not alert either', () => { - expect((0, onCoupleKeyRotated_1.isKeyGenerationIncrease)(5, 4)).toBe(false); - }); - it('treats junk as zero rather than alerting on garbage', () => { - expect((0, onCoupleKeyRotated_1.isKeyGenerationIncrease)('x', 'y')).toBe(false); - expect((0, onCoupleKeyRotated_1.isKeyGenerationIncrease)(null, 'high')).toBe(false); - }); -}); -describe('isPhrasePublished', () => { - it('fires when a new recovery phrase is published (phase 1)', () => { - expect((0, onCoupleKeyRotated_1.isPhrasePublished)(undefined, 1)).toBe(true); - expect((0, onCoupleKeyRotated_1.isPhrasePublished)(0, 1)).toBe(true); - expect((0, onCoupleKeyRotated_1.isPhrasePublished)(2, 3)).toBe(true); - }); - it('ignores the acks and the completion — only the publish needs the partner in the app', () => { - // Phase 2 (acks) and phase 3 (the re-wrap) leave phraseGeneration alone, so they must not alert: - // the partner is already acting, and re-alerting them would be noise on a security channel. - expect((0, onCoupleKeyRotated_1.isPhrasePublished)(1, 1)).toBe(false); - expect((0, onCoupleKeyRotated_1.isPhrasePublished)(undefined, undefined)).toBe(false); - }); - it('never fires on a downgrade or a redelivered stale event', () => { - expect((0, onCoupleKeyRotated_1.isPhrasePublished)(5, 4)).toBe(false); - expect((0, onCoupleKeyRotated_1.isPhrasePublished)(1, undefined)).toBe(false); - }); -}); -//# sourceMappingURL=onCoupleKeyRotated.test.js.map \ No newline at end of file diff --git a/functions/dist/couples/onCoupleKeyRotated.test.js.map b/functions/dist/couples/onCoupleKeyRotated.test.js.map deleted file mode 100644 index 6e649119..00000000 --- a/functions/dist/couples/onCoupleKeyRotated.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onCoupleKeyRotated.test.js","sourceRoot":"","sources":["../../src/couples/onCoupleKeyRotated.test.ts"],"names":[],"mappings":";;AAAA,6DAAiF;AAEjF,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,IAAA,4CAAuB,EAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACxD,MAAM,CAAC,IAAA,4CAAuB,EAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAChD,MAAM,CAAC,IAAA,4CAAuB,EAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAClD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,uGAAuG;QACvG,MAAM,CAAC,IAAA,4CAAuB,EAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjE,MAAM,CAAC,IAAA,4CAAuB,EAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,MAAM,CAAC,IAAA,4CAAuB,EAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mGAAmG,EAAE,GAAG,EAAE;QAC3G,MAAM,CAAC,IAAA,4CAAuB,EAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,IAAA,4CAAuB,EAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACrD,MAAM,CAAC,IAAA,4CAAuB,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,IAAA,sCAAiB,EAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClD,MAAM,CAAC,IAAA,sCAAiB,EAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC1C,MAAM,CAAC,IAAA,sCAAiB,EAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qFAAqF,EAAE,GAAG,EAAE;QAC7F,iGAAiG;QACjG,4FAA4F;QAC5F,MAAM,CAAC,IAAA,sCAAiB,EAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAA,sCAAiB,EAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC7D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,IAAA,sCAAiB,EAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAA,sCAAiB,EAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACrD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/functions/dist/couples/onCoupleLeave.js b/functions/dist/couples/onCoupleLeave.js deleted file mode 100644 index 1c737508..00000000 --- a/functions/dist/couples/onCoupleLeave.js +++ /dev/null @@ -1,118 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onCoupleLeave = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const push_1 = require("../notifications/push"); -const idempotency_1 = require("../notifications/idempotency"); -const log_1 = require("../log"); -/** - * Firestore trigger that notifies the remaining partner when a user's coupleId - * field is cleared (i.e. the user left the couple or was removed). - * - * Path: users/{userId} - * Condition: previous coupleId was non-empty and new coupleId is null/missing. - */ -exports.onCoupleLeave = (0, firestore_1.onDocumentUpdated)('users/{userId}', async (event) => { - var _a, _b, _c, _d, _e, _f; - const { userId } = event.params; - const previousData = (_b = (_a = event.data) === null || _a === void 0 ? void 0 : _a.before.data()) !== null && _b !== void 0 ? _b : {}; - const currentData = (_d = (_c = event.data) === null || _c === void 0 ? void 0 : _c.after.data()) !== null && _d !== void 0 ? _d : {}; - const previousCoupleId = previousData.coupleId; - const currentCoupleId = currentData.coupleId; - // Only act when coupleId transitions from a real value to null/empty. - if (!previousCoupleId || typeof previousCoupleId !== 'string') { - return; - } - if (currentCoupleId) { - return; - } - const db = admin.firestore(); - const coupleDoc = await db.collection('couples').doc(previousCoupleId).get(); - if (!coupleDoc.exists) { - log_1.logger.warn(`[onCoupleLeave] couple ${previousCoupleId} not found`); - return; - } - const userIds = ((_f = (_e = coupleDoc.data()) === null || _e === void 0 ? void 0 : _e.userIds) !== null && _f !== void 0 ? _f : []); - const partnerId = userIds.find((uid) => uid !== userId); - if (!partnerId) { - log_1.logger.warn(`[onCoupleLeave] no partner found for couple ${previousCoupleId}`); - return; - } - // Make sure the partner is still paired in this couple. - // If both users are leaving simultaneously, avoid duplicate/phantom notifications. - const partnerUserDoc = await db.collection('users').doc(partnerId).get(); - const partnerData = partnerUserDoc.data(); - const partnerCoupleId = partnerData === null || partnerData === void 0 ? void 0 : partnerData.coupleId; - if (partnerCoupleId !== previousCoupleId) { - log_1.logger.log(`[onCoupleLeave] partner ${partnerId} is no longer in couple ${previousCoupleId}; skipping notification`); - return; - } - // Dedupe redelivery of this leave (at-least-once) so the partner isn't double-notified and the - // in-app record isn't duplicated. - if (!(await (0, idempotency_1.claimOnce)((0, idempotency_1.notifMark)(db, previousCoupleId, `leave-${userId}`)))) { - log_1.logger.log(`[onCoupleLeave] already notified partner of ${userId} leaving; skipping`); - return; - } - const notificationPayload = { - type: 'partner_left', - title: 'Your partner has left', - body: 'You are no longer paired. Tap to create a new invite.', - }; - // Write an in-app notification record for the partner. - // This is read-only denied for clients; the Cloud Function writes it. - await db - .collection('users') - .doc(partnerId) - .collection('notification_queue') - .add(Object.assign(Object.assign({}, notificationPayload), { read: false, createdAt: admin.firestore.FieldValue.serverTimestamp() })); - const res = await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { - title: notificationPayload.title, - body: notificationPayload.body, - }, - android: { notification: { channelId: 'partner_activity' } }, // E-OBS - data: { - type: notificationPayload.type, - }, - }, partnerData); - if (res.sent === 0 && res.failed === 0) { - log_1.logger.log(`[onCoupleLeave] no FCM tokens for partner ${partnerId}`); - return; - } - log_1.logger.log(`[onCoupleLeave] notified partner ${partnerId} that user ${userId} left couple ${previousCoupleId}`); -}); -//# sourceMappingURL=onCoupleLeave.js.map \ No newline at end of file diff --git a/functions/dist/couples/onCoupleLeave.js.map b/functions/dist/couples/onCoupleLeave.js.map deleted file mode 100644 index cecfd2c3..00000000 --- a/functions/dist/couples/onCoupleLeave.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onCoupleLeave.js","sourceRoot":"","sources":["../../src/couples/onCoupleLeave.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/scheduledOutcomesReminder.js b/functions/dist/couples/scheduledOutcomesReminder.js deleted file mode 100644 index 1c2d940b..00000000 --- a/functions/dist/couples/scheduledOutcomesReminder.js +++ /dev/null @@ -1,123 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.scheduledOutcomesReminder = void 0; -const admin = __importStar(require("firebase-admin")); -const scheduler_1 = require("firebase-functions/v2/scheduler"); -const quietHours_1 = require("../notifications/quietHours"); -const push_1 = require("../notifications/push"); -const time_1 = require("../notifications/time"); -const log_1 = require("../log"); -const DAY_MS = 24 * 60 * 60 * 1000; -const REMINDER_DAYS = [30, 60, 90]; -const DAY_KEY_MAP = { 30: 'day_30', 60: 'day_60', 90: 'day_90' }; -exports.scheduledOutcomesReminder = (0, scheduler_1.onSchedule)({ schedule: 'every 24 hours', timeoutSeconds: 180 }, async () => { - var _a, _b; - const db = admin.firestore(); - const messaging = admin.messaging(); - const now = Date.now(); - const couplesSnap = await db.collection('couples').limit(200).get(); - const notifications = []; - for (const coupleDoc of couplesSnap.docs) { - const coupleId = coupleDoc.id; - const data = (_a = coupleDoc.data()) !== null && _a !== void 0 ? _a : {}; - const createdAt = (0, time_1.toMillis)(data.createdAt); - if (createdAt <= 0) - continue; - const ageDays = Math.floor((now - createdAt) / DAY_MS); - const dueDays = REMINDER_DAYS.filter((day) => ageDays >= day && ageDays <= day + 2); - if (dueDays.length === 0) - continue; - const userIds = ((_b = data.userIds) !== null && _b !== void 0 ? _b : []); - if (userIds.length === 0) - continue; - // Check each due checkpoint; only remind for the first one without an outcome. - let remindedDay = null; - for (const day of dueDays) { - const dayKey = DAY_KEY_MAP[day]; - const outcomeSnap = await coupleDoc.ref.collection('outcomes').doc(dayKey).get(); - if (!outcomeSnap.exists) { - remindedDay = day; - break; - } - } - if (remindedDay == null) - continue; - const dayLabel = remindedDay; - const title = 'How are you feeling together?'; - const body = `You’ve been connected for ${dayLabel} days. Take a quick check-in to see how things have changed.`; - for (const userId of userIds) { - notifications.push({ userId, coupleId, day: dayLabel, title, body }); - } - } - // Per-recipient failures are isolated so one bad send can't abort the batch. - await Promise.allSettled(notifications.map((notification) => sendOutcomeReminder(db, messaging, notification))); - log_1.logger.log(`[scheduledOutcomesReminder] scanned ${couplesSnap.size}; notified ${notifications.length}`); -}); -async function sendOutcomeReminder(db, messaging, notification) { - const userDoc = await db.collection('users').doc(notification.userId).get(); - const userData = userDoc.data(); - // Honor the recipient's quiet hours (outcome check-ins are genuine, so no promotional gate). - if ((0, quietHours_1.recipientInQuietHours)(userData)) { - log_1.logger.log(`[sendOutcomeReminder] skip ${notification.userId} — quiet hours`); - return; - } - await db - .collection('users') - .doc(notification.userId) - .collection('notification_queue') - .add({ - type: 'outcome_reminder', - title: notification.title, - body: notification.body, - coupleId: notification.coupleId, - day: notification.day, - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - await (0, push_1.sendPushToUser)(db, messaging, notification.userId, { - notification: { - title: notification.title, - body: notification.body, - }, - android: { notification: { channelId: 'reminders' } }, // E-OBS - data: { - type: 'outcome_reminder', - coupleId: notification.coupleId, - day: String(notification.day), - }, - }, userData); -} -//# sourceMappingURL=scheduledOutcomesReminder.js.map \ No newline at end of file diff --git a/functions/dist/couples/scheduledOutcomesReminder.js.map b/functions/dist/couples/scheduledOutcomesReminder.js.map deleted file mode 100644 index 997b0b3f..00000000 --- a/functions/dist/couples/scheduledOutcomesReminder.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"scheduledOutcomesReminder.js","sourceRoot":"","sources":["../../src/couples/scheduledOutcomesReminder.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/submitOutcomeCallable.js b/functions/dist/couples/submitOutcomeCallable.js deleted file mode 100644 index 43bea593..00000000 --- a/functions/dist/couples/submitOutcomeCallable.js +++ /dev/null @@ -1,155 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.submitOutcomeCallable = void 0; -const admin = __importStar(require("firebase-admin")); -const https_1 = require("firebase-functions/v2/https"); -const log_1 = require("../log"); -const DAY_KEYS = ['day_0', 'day_30', 'day_60', 'day_90']; -const SCORE_KEYS = ['connection', 'communication', 'intimacy', 'happiness']; -const MIN_SCORE = 1; -const MAX_SCORE = 10; -function isValidDayKey(value) { - return typeof value === 'string' && DAY_KEYS.includes(value); -} -function isValidScoreMap(value) { - if (typeof value !== 'object' || value === null || Array.isArray(value)) - return false; - const map = value; - for (const key of SCORE_KEYS) { - const num = map[key]; - if (typeof num !== 'number' || Number.isNaN(num)) - return false; - if (num < MIN_SCORE || num > MAX_SCORE) - return false; - } - return true; -} -exports.submitOutcomeCallable = (0, https_1.onCall)(async (request) => { - var _a, _b, _c; - const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); - } - if (!request.app) { - throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); - } - const data = request.data; - const coupleId = data === null || data === void 0 ? void 0 : data.coupleId; - if (typeof coupleId !== 'string' || coupleId.length === 0) { - throw new https_1.HttpsError('invalid-argument', 'coupleId is required.'); - } - const dayKey = data === null || data === void 0 ? void 0 : data.dayKey; - if (!isValidDayKey(dayKey)) { - throw new https_1.HttpsError('invalid-argument', `dayKey must be one of: ${DAY_KEYS.join(', ')}.`); - } - const scores = data === null || data === void 0 ? void 0 : data.scores; - if (!isValidScoreMap(scores)) { - throw new https_1.HttpsError('invalid-argument', `scores must contain ${SCORE_KEYS.join(', ')} with values ${MIN_SCORE}-${MAX_SCORE}.`); - } - const db = admin.firestore(); - const coupleRef = db.collection('couples').doc(coupleId); - // Caller must be a member of the couple. - const coupleDoc = await coupleRef.get(); - if (!coupleDoc.exists) { - throw new https_1.HttpsError('not-found', 'Couple not found.'); - } - const userIds = ((_c = (_b = coupleDoc.data()) === null || _b === void 0 ? void 0 : _b.userIds) !== null && _c !== void 0 ? _c : []); - if (!userIds.includes(callerId)) { - throw new https_1.HttpsError('permission-denied', 'Caller is not a member of this couple.'); - } - const now = admin.firestore.Timestamp.now(); - const outcomeRef = coupleRef.collection('outcomes').doc(dayKey); - let result; - try { - result = await db.runTransaction(async (tx) => { - var _a, _b, _c, _d, _e, _f, _g, _h; - const existing = await tx.get(outcomeRef); - const existingData = existing.exists ? ((_a = existing.data()) !== null && _a !== void 0 ? _a : {}) : {}; - // If this is a follow-up (non-baseline), a baseline must exist to compute delta. - if (dayKey !== 'day_0') { - const baselineRef = coupleRef.collection('outcomes').doc('day_0'); - const baselineSnap = await tx.get(baselineRef); - if (!baselineSnap.exists) { - throw new https_1.HttpsError('failed-precondition', 'Baseline (day_0) outcome must be submitted before follow-up outcomes.'); - } - } - const answeredBy = ((_b = existingData.answeredBy) !== null && _b !== void 0 ? _b : []); - if (!answeredBy.includes(callerId)) { - answeredBy.push(callerId); - } - const payload = { - dayKey, - answeredBy, - updatedAt: now, - createdAt: (_c = existingData.createdAt) !== null && _c !== void 0 ? _c : now, - }; - if (dayKey === 'day_0') { - payload.baseline = scores; - } - else { - payload.scores = scores; - } - // Delta is recalculated every time so repeated submissions stay consistent. - if (dayKey !== 'day_0') { - const baselineRef = coupleRef.collection('outcomes').doc('day_0'); - const baselineSnap = await tx.get(baselineRef); - if (baselineSnap.exists) { - const baseline = ((_e = (_d = baselineSnap.data()) === null || _d === void 0 ? void 0 : _d.baseline) !== null && _e !== void 0 ? _e : {}); - const delta = {}; - for (const key of SCORE_KEYS) { - delta[key] = ((_f = scores[key]) !== null && _f !== void 0 ? _f : 0) - ((_g = baseline[key]) !== null && _g !== void 0 ? _g : 0); - } - payload.delta = delta; - } - } - tx.set(outcomeRef, payload, { merge: true }); - // Per-user mirror for cross-relationship stats. - const userOutcomeRef = db.collection('users').doc(callerId).collection('outcomes').doc(dayKey); - tx.set(userOutcomeRef, Object.assign(Object.assign({ dayKey, - coupleId }, (dayKey === 'day_0' ? { baseline: scores } : { scores, delta: payload.delta })), { answeredBy: [callerId], createdAt: (_h = existingData.createdAt) !== null && _h !== void 0 ? _h : now, updatedAt: now }), { merge: true }); - return { dayKey, answeredBy }; - }); - } - catch (e) { - if (e instanceof https_1.HttpsError) - throw e; - log_1.logger.error('[submitOutcomeCallable] outcome transaction failed', { error: String(e) }); - throw new https_1.HttpsError('internal', 'Failed to submit outcome. Please try again.'); - } - log_1.logger.log(`[submitOutcomeCallable] ${callerId} submitted ${dayKey} for couple ${coupleId}`); - return Object.assign({ success: true }, result); -}); -//# sourceMappingURL=submitOutcomeCallable.js.map \ No newline at end of file diff --git a/functions/dist/couples/submitOutcomeCallable.js.map b/functions/dist/couples/submitOutcomeCallable.js.map deleted file mode 100644 index e9154a71..00000000 --- a/functions/dist/couples/submitOutcomeCallable.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"submitOutcomeCallable.js","sourceRoot":"","sources":["../../src/couples/submitOutcomeCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/dates/createDateMatch.js b/functions/dist/dates/createDateMatch.js deleted file mode 100644 index 7cdbe90e..00000000 --- a/functions/dist/dates/createDateMatch.js +++ /dev/null @@ -1,98 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.notifyOnDateMatch = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const push_1 = require("../notifications/push"); -/** - * Fires the "It's a match!" notification when a date match is created. - * - * Trigger: couples/{coupleId}/date_matches/{dateIdeaId} (onCreate) - * - * Date swipes are E2E-encrypted, so the server can no longer detect mutual love. - * Mutual-match detection now happens client-side (whichever partner records the - * second LOVE writes the match marker, validated by Firestore rules). This trigger - * only sends the push to both partners — it never reads swipe content. - * - * Idempotency: `fcmNotified` is claimed in a transaction so concurrent invocations - * (or a redelivered event) never double-send. The match doc id is the date idea id, so - * the marker itself is already de-duplicated by the client transaction + rules. - */ -exports.notifyOnDateMatch = (0, firestore_1.onDocumentCreated)('couples/{coupleId}/date_matches/{dateIdeaId}', async (event) => { - var _a, _b; - const snap = event.data; - if (!snap || !snap.exists) - return; - const { coupleId, dateIdeaId } = event.params; - const db = admin.firestore(); - const matchRef = snap.ref; - // Atomically claim the FCM send so concurrent invocations don't double-send. - const shouldSend = await db.runTransaction(async (tx) => { - var _a; - const doc = await tx.get(matchRef); - if (!doc.exists || ((_a = doc.data()) === null || _a === void 0 ? void 0 : _a.fcmNotified) === true) - return false; - tx.update(matchRef, { fcmNotified: true }); - return true; - }); - if (!shouldSend) - return; - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []); - await Promise.allSettled(userIds.map((uid) => notifyDateMatch(db, uid, coupleId, dateIdeaId))); -}); -async function notifyDateMatch(db, userId, coupleId, dateIdeaId) { - await db.collection('users').doc(userId).collection('notification_queue').add({ - type: 'date_match', - title: "It's a match!", - body: "You both want to go on this date. Time to make it happen.", - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - await (0, push_1.sendPushToUser)(db, admin.messaging(), userId, { - notification: { - title: "It's a match!", - body: "You both want to go on this date. Time to make it happen.", - }, - android: { notification: { channelId: 'partner_activity' } }, // E-OBS - data: { - type: 'date_match', - couple_id: coupleId, - date_idea_id: dateIdeaId, - }, - }); -} -//# sourceMappingURL=createDateMatch.js.map \ No newline at end of file diff --git a/functions/dist/dates/createDateMatch.js.map b/functions/dist/dates/createDateMatch.js.map deleted file mode 100644 index 449df46a..00000000 --- a/functions/dist/dates/createDateMatch.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"createDateMatch.js","sourceRoot":"","sources":["../../src/dates/createDateMatch.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAAuC;AACvC,+DAAmE;AACnE,gDAAsD;AAEtD;;;;;;;;;;;;;GAaG;AACU,QAAA,iBAAiB,GAAG,IAAA,6BAAiB,EAChD,8CAA8C,EAC9C,KAAK,EAAE,KAAK,EAAE,EAAE;;IACd,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAA;IACvB,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAM;IAEjC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC,MAAM,CAAA;IAE7C,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAA;IAEzB,6EAA6E;IAC7E,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;;QACtD,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAClC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAA,MAAA,GAAG,CAAC,IAAI,EAAE,0CAAE,WAAW,MAAK,IAAI;YAAE,OAAO,KAAK,CAAA;QACjE,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;QAC1C,OAAO,IAAI,CAAA;IACb,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,UAAU;QAAE,OAAM;IAEvB,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IACpE,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;AAChG,CAAC,CACF,CAAA;AAED,KAAK,UAAU,eAAe,CAC5B,EAA6B,EAC7B,MAAc,EACd,QAAgB,EAChB,UAAkB;IAElB,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC;QAC5E,IAAI,EAAE,YAAY;QAClB,KAAK,EAAE,eAAe;QACtB,IAAI,EAAE,2DAA2D;QACjE,IAAI,EAAE,KAAK;QACX,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KACxD,CAAC,CAAA;IAEF,MAAM,IAAA,qBAAc,EAAC,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE;QAClD,YAAY,EAAE;YACZ,KAAK,EAAE,eAAe;YACtB,IAAI,EAAE,2DAA2D;SAClE;QACD,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,EAAE,QAAQ;QACtE,IAAI,EAAE;YACJ,IAAI,EAAE,YAAY;YAClB,SAAS,EAAE,QAAQ;YACnB,YAAY,EAAE,UAAU;SACzB;KACF,CAAC,CAAA;AACJ,CAAC"} \ No newline at end of file diff --git a/functions/dist/dates/onDateHistoryCreated.js b/functions/dist/dates/onDateHistoryCreated.js deleted file mode 100644 index d965eb6f..00000000 --- a/functions/dist/dates/onDateHistoryCreated.js +++ /dev/null @@ -1,85 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onDateHistoryCreated = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const quietHours_1 = require("../notifications/quietHours"); -const push_1 = require("../notifications/push"); -const idempotency_1 = require("../notifications/idempotency"); -const log_1 = require("../log"); -/** - * Fires when a date is logged as completed (`couples/{coupleId}/date_history/{dateId}` created). Nudges - * the OTHER partner (not the one who logged it) to add their reflection — so the reflect→reveal loop - * starts even if the logger doesn't reflect right away. Gated on `notifPartnerAnswered` + quiet hours. - */ -exports.onDateHistoryCreated = (0, firestore_1.onDocumentCreated)('couples/{coupleId}/date_history/{dateId}', async (event) => { - var _a, _b, _c; - const { coupleId, dateId } = event.params; - const snap = event.data; - if (!snap) - return; - const db = admin.firestore(); - const addedBy = (_a = snap.data()) === null || _a === void 0 ? void 0 : _a.addedBy; - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) - return; - const userIds = ((_c = (_b = coupleDoc.data()) === null || _b === void 0 ? void 0 : _b.userIds) !== null && _c !== void 0 ? _c : []); - const partnerId = userIds.find((u) => u !== addedBy); - if (!partnerId) - return; - const partnerUserDoc = await db.collection('users').doc(partnerId).get(); - const partnerData = partnerUserDoc.data(); - if ((partnerData === null || partnerData === void 0 ? void 0 : partnerData.notifPartnerAnswered) === false) - return; - // Dedupe redelivery (at-least-once) before writing the in-app record or sending. - if (!(await (0, idempotency_1.claimOnce)((0, idempotency_1.notifMark)(db, coupleId, `date-logged-${dateId}`)))) { - log_1.logger.log(`[onDateHistoryCreated] already notified for date ${dateId}; skipping`); - return; - } - const title = 'You went on a date 💜'; - const body = 'Reflect on it together while it’s fresh.'; - await db.collection('users').doc(partnerId).collection('notification_queue').add({ - type: 'date_logged', title, body, read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - if ((0, quietHours_1.recipientInQuietHours)(partnerData)) - return; - await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { title, body }, - data: { type: 'date_logged', couple_id: coupleId, date_id: dateId }, - android: { notification: { channelId: 'partner_activity' } }, - }, partnerData); -}); -//# sourceMappingURL=onDateHistoryCreated.js.map \ No newline at end of file diff --git a/functions/dist/dates/onDateHistoryCreated.js.map b/functions/dist/dates/onDateHistoryCreated.js.map deleted file mode 100644 index 583784c6..00000000 --- a/functions/dist/dates/onDateHistoryCreated.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onDateHistoryCreated.js","sourceRoot":"","sources":["../../src/dates/onDateHistoryCreated.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAAuC;AACvC,+DAAmE;AACnE,4DAAmE;AACnE,gDAAsD;AACtD,8DAAmE;AACnE,gCAA+B;AAE/B;;;;GAIG;AACU,QAAA,oBAAoB,GAAG,IAAA,6BAAiB,EACnD,0CAA0C,EAC1C,KAAK,EAAE,KAAK,EAAE,EAAE;;IACd,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAA;IACzC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAA;IACvB,IAAI,CAAC,IAAI;QAAE,OAAM;IACjB,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,MAAM,OAAO,GAAG,MAAA,IAAI,CAAC,IAAI,EAAE,0CAAE,OAA6B,CAAA;IAC1D,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IACpE,IAAI,CAAC,SAAS,CAAC,MAAM;QAAE,OAAM;IAC7B,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,CAAA;IACpD,IAAI,CAAC,SAAS;QAAE,OAAM;IAEtB,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IACxE,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,EAAE,CAAA;IACzC,IAAI,CAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,oBAAoB,MAAK,KAAK;QAAE,OAAM;IAEvD,iFAAiF;IACjF,IAAI,CAAC,CAAC,MAAM,IAAA,uBAAS,EAAC,IAAA,uBAAS,EAAC,EAAE,EAAE,QAAQ,EAAE,eAAe,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,YAAM,CAAC,GAAG,CAAC,oDAAoD,MAAM,YAAY,CAAC,CAAA;QAClF,OAAM;IACR,CAAC;IAED,MAAM,KAAK,GAAG,uBAAuB,CAAA;IACrC,MAAM,IAAI,GAAG,0CAA0C,CAAA;IAEvD,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC;QAC/E,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KACvG,CAAC,CAAA;IAEF,IAAI,IAAA,kCAAqB,EAAC,WAAW,CAAC;QAAE,OAAM;IAE9C,MAAM,IAAA,qBAAc,EAClB,EAAE,EACF,KAAK,CAAC,SAAS,EAAE,EACjB,SAAS,EACT;QACE,YAAY,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;QAC7B,IAAI,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE;QACnE,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE;KAC7D,EACD,WAAW,CACZ,CAAA;AACH,CAAC,CACF,CAAA"} \ No newline at end of file diff --git a/functions/dist/dates/onDateReflectionRevealed.js b/functions/dist/dates/onDateReflectionRevealed.js deleted file mode 100644 index f846cbca..00000000 --- a/functions/dist/dates/onDateReflectionRevealed.js +++ /dev/null @@ -1,99 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onDateReflectionRevealed = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const quietHours_1 = require("../notifications/quietHours"); -const push_1 = require("../notifications/push"); -const idempotency_1 = require("../notifications/idempotency"); -const log_1 = require("../log"); -/** - * Fires when a partner OPENS (reveals) the shared date reflections — their own reflection metadata doc - * flips `isRevealed` false → true (`couples/{coupleId}/date_reflections/{dateId}/answers/{userId}`). - * Notifies the OTHER partner that their reflection was read, mirroring daily's `onAnswerRevealed`: - * generic copy (no decrypted content, no name — the app renders the real name in-app), gated on - * `notifPartnerAnswered` + quiet hours (push suppressed in quiet hours, but the in-app record is kept). - */ -exports.onDateReflectionRevealed = (0, firestore_1.onDocumentUpdated)('couples/{coupleId}/date_reflections/{dateId}/answers/{userId}', async (event) => { - var _a, _b, _c, _d, _e, _f, _g; - const { coupleId, dateId, userId } = event.params; - const before = ((_b = (_a = event.data) === null || _a === void 0 ? void 0 : _a.before.data()) !== null && _b !== void 0 ? _b : {}); - const after = ((_d = (_c = event.data) === null || _c === void 0 ? void 0 : _c.after.data()) !== null && _d !== void 0 ? _d : {}); - // Only on the false → true reveal transition. - if (before.isRevealed === true || after.isRevealed !== true) - return; - const db = admin.firestore(); - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) - return; - const userIds = ((_f = (_e = coupleDoc.data()) === null || _e === void 0 ? void 0 : _e.userIds) !== null && _f !== void 0 ? _f : []); - if (!userIds.includes(userId)) - return; - const partnerId = userIds.find((u) => u !== userId); - if (!partnerId) - return; - const partnerUserDoc = await db.collection('users').doc(partnerId).get(); - const partnerData = partnerUserDoc.data(); - if ((partnerData === null || partnerData === void 0 ? void 0 : partnerData.notifPartnerAnswered) === false) { - log_1.logger.log(`[onDateReflectionRevealed] ${partnerId} has partner notifications off`); - return; - } - // Dedupe redelivery (at-least-once) before writing the in-app record or sending. - if (!(await (0, idempotency_1.claimOnce)((0, idempotency_1.notifMark)(db, coupleId, `date-reveal-${dateId}-${userId}`)))) { - log_1.logger.log(`[onDateReflectionRevealed] already notified for ${userId} on ${dateId}; skipping`); - return; - } - const title = 'Your partner opened your reflection ✨'; - const body = 'Open to see what you each wrote.'; - const type = 'date_reflection_opened'; - // In-app record (→ Together feed) — written regardless of quiet hours. - await db.collection('users').doc(partnerId).collection('notification_queue').add({ - type, title, body, read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - if ((0, quietHours_1.recipientInQuietHours)(partnerData)) { - log_1.logger.log(`[onDateReflectionRevealed] ${partnerId} in quiet hours — push suppressed (in-app kept)`); - return; - } - const senderAvatar = (_g = (await db.collection('users').doc(userId).get()).data()) === null || _g === void 0 ? void 0 : _g.photoUrl; - await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { title, body }, - data: Object.assign({ type, couple_id: coupleId, date_id: dateId }, (typeof senderAvatar === 'string' && senderAvatar.length > 0 - ? { sender_avatar_url: senderAvatar } - : {})), - android: { notification: { channelId: 'partner_activity' } }, - }, partnerData); -}); -//# sourceMappingURL=onDateReflectionRevealed.js.map \ No newline at end of file diff --git a/functions/dist/dates/onDateReflectionRevealed.js.map b/functions/dist/dates/onDateReflectionRevealed.js.map deleted file mode 100644 index 7f9a3fb4..00000000 --- a/functions/dist/dates/onDateReflectionRevealed.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onDateReflectionRevealed.js","sourceRoot":"","sources":["../../src/dates/onDateReflectionRevealed.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/dates/onDateReflectionWritten.js b/functions/dist/dates/onDateReflectionWritten.js deleted file mode 100644 index 7fd339ed..00000000 --- a/functions/dist/dates/onDateReflectionWritten.js +++ /dev/null @@ -1,103 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onDateReflectionWritten = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const quietHours_1 = require("../notifications/quietHours"); -const push_1 = require("../notifications/push"); -const idempotency_1 = require("../notifications/idempotency"); -const log_1 = require("../log"); -/** - * Fires when a partner writes their post-date reflection - * (`couples/{coupleId}/date_reflections/{dateId}/answers/{userId}`). Notifies the OTHER partner — "your - * turn to reflect" if they haven't yet, or "ready to reveal together" if both now have. Without this the - * second partner never learns to reflect and the mutual reveal stalls. Mirrors `onAnswerWritten`: - * generic copy (no decrypted content, no name — the app renders the real name in-app), gated on - * `notifPartnerAnswered` + quiet hours (push suppressed in quiet hours, but the in-app record is kept). - */ -exports.onDateReflectionWritten = (0, firestore_1.onDocumentCreated)('couples/{coupleId}/date_reflections/{dateId}/answers/{userId}', async (event) => { - var _a, _b, _c; - const { coupleId, dateId, userId } = event.params; - const db = admin.firestore(); - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) - return; - const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []); - if (!userIds.includes(userId)) - return; - const partnerId = userIds.find((u) => u !== userId); - if (!partnerId) - return; - const partnerUserDoc = await db.collection('users').doc(partnerId).get(); - const partnerData = partnerUserDoc.data(); - // Partner-activity opt-out (default on). - if ((partnerData === null || partnerData === void 0 ? void 0 : partnerData.notifPartnerAnswered) === false) { - log_1.logger.log(`[onDateReflectionWritten] ${partnerId} has partner notifications off`); - return; - } - // Dedupe redelivery (at-least-once) before writing the in-app record or sending. - if (!(await (0, idempotency_1.claimOnce)((0, idempotency_1.notifMark)(db, coupleId, `date-reflect-${dateId}-${userId}`)))) { - log_1.logger.log(`[onDateReflectionWritten] already notified for ${userId} on ${dateId}; skipping`); - return; - } - // Did this complete the pair? If the recipient already reflected, both are done → reveal-ready. - const partnerReflection = await db - .collection('couples').doc(coupleId) - .collection('date_reflections').doc(dateId) - .collection('answers').doc(partnerId).get(); - const bothReflected = partnerReflection.exists; - const title = bothReflected ? 'Your date reflections are ready ✨' : 'Your partner reflected on your date 💭'; - const body = bothReflected ? 'Open to reveal them together.' : 'Add yours to reveal together.'; - const type = bothReflected ? 'date_reflection_ready' : 'date_reflection_partner'; - // In-app record (→ Together feed) — written regardless of quiet hours. - await db.collection('users').doc(partnerId).collection('notification_queue').add({ - type, title, body, read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - // Quiet hours: keep the in-app record, suppress the disruptive push. - if ((0, quietHours_1.recipientInQuietHours)(partnerData)) { - log_1.logger.log(`[onDateReflectionWritten] ${partnerId} in quiet hours — push suppressed (in-app kept)`); - return; - } - const senderAvatar = (_c = (await db.collection('users').doc(userId).get()).data()) === null || _c === void 0 ? void 0 : _c.photoUrl; - await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { title, body }, - data: Object.assign({ type, couple_id: coupleId, date_id: dateId }, (typeof senderAvatar === 'string' && senderAvatar.length > 0 - ? { sender_avatar_url: senderAvatar } - : {})), - android: { notification: { channelId: 'partner_activity' } }, // E-OBS - }, partnerData); -}); -//# sourceMappingURL=onDateReflectionWritten.js.map \ No newline at end of file diff --git a/functions/dist/dates/onDateReflectionWritten.js.map b/functions/dist/dates/onDateReflectionWritten.js.map deleted file mode 100644 index 4ea837a0..00000000 --- a/functions/dist/dates/onDateReflectionWritten.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onDateReflectionWritten.js","sourceRoot":"","sources":["../../src/dates/onDateReflectionWritten.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAAuC;AACvC,+DAAmE;AACnE,4DAAmE;AACnE,gDAAsD;AACtD,8DAAmE;AACnE,gCAA+B;AAE/B;;;;;;;GAOG;AACU,QAAA,uBAAuB,GAAG,IAAA,6BAAiB,EACtD,+DAA+D,EAC/D,KAAK,EAAE,KAAK,EAAE,EAAE;;IACd,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAA;IACjD,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IACpE,IAAI,CAAC,SAAS,CAAC,MAAM;QAAE,OAAM;IAC7B,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAM;IACrC,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,CAAA;IACnD,IAAI,CAAC,SAAS;QAAE,OAAM;IAEtB,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IACxE,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,EAAE,CAAA;IACzC,yCAAyC;IACzC,IAAI,CAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,oBAAoB,MAAK,KAAK,EAAE,CAAC;QAChD,YAAM,CAAC,GAAG,CAAC,6BAA6B,SAAS,gCAAgC,CAAC,CAAA;QAClF,OAAM;IACR,CAAC;IAED,iFAAiF;IACjF,IAAI,CAAC,CAAC,MAAM,IAAA,uBAAS,EAAC,IAAA,uBAAS,EAAC,EAAE,EAAE,QAAQ,EAAE,gBAAgB,MAAM,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpF,YAAM,CAAC,GAAG,CAAC,kDAAkD,MAAM,OAAO,MAAM,YAAY,CAAC,CAAA;QAC7F,OAAM;IACR,CAAC;IAED,gGAAgG;IAChG,MAAM,iBAAiB,GAAG,MAAM,EAAE;SAC/B,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;SACnC,UAAU,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;SAC1C,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IAC7C,MAAM,aAAa,GAAG,iBAAiB,CAAC,MAAM,CAAA;IAE9C,MAAM,KAAK,GAAG,aAAa,CAAC,CAAC,CAAC,mCAAmC,CAAC,CAAC,CAAC,wCAAwC,CAAA;IAC5G,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,+BAA+B,CAAA;IAC9F,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,yBAAyB,CAAA;IAEhF,uEAAuE;IACvE,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC;QAC/E,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KACxF,CAAC,CAAA;IAEF,qEAAqE;IACrE,IAAI,IAAA,kCAAqB,EAAC,WAAW,CAAC,EAAE,CAAC;QACvC,YAAM,CAAC,GAAG,CAAC,6BAA6B,SAAS,iDAAiD,CAAC,CAAA;QACnG,OAAM;IACR,CAAC;IAED,MAAM,YAAY,GAAG,MAAA,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,0CAAE,QAAQ,CAAA;IAEtF,MAAM,IAAA,qBAAc,EAClB,EAAE,EACF,KAAK,CAAC,SAAS,EAAE,EACjB,SAAS,EACT;QACE,YAAY,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;QAC7B,IAAI,kBACF,IAAI,EACJ,SAAS,EAAE,QAAQ,EACnB,OAAO,EAAE,MAAM,IACZ,CAAC,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC;YAC7D,CAAC,CAAC,EAAE,iBAAiB,EAAE,YAAY,EAAE;YACrC,CAAC,CAAC,EAAE,CAAC,CACR;QACD,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,EAAE,QAAQ;KACvE,EACD,WAAW,CACZ,CAAA;AACH,CAAC,CACF,CAAA"} \ No newline at end of file diff --git a/functions/dist/games/onGameSessionUpdate.js b/functions/dist/games/onGameSessionUpdate.js deleted file mode 100644 index 4d5be6f2..00000000 --- a/functions/dist/games/onGameSessionUpdate.js +++ /dev/null @@ -1,303 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onDesireSyncPartFinished = exports.onHowWellPartFinished = exports.onWheelPartFinished = exports.onThisOrThatPartFinished = exports.onGameSessionUpdate = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const quietHours_1 = require("../notifications/quietHours"); -const push_1 = require("../notifications/push"); -const log_1 = require("../log"); -/** - * Firestore trigger that notifies partners when a game session is created or completed. - * - * Path: couples/{coupleId}/sessions/{sessionId} - * Condition: onWrite (create, update, delete) - */ -exports.onGameSessionUpdate = (0, firestore_1.onDocumentWritten)('couples/{coupleId}/sessions/{sessionId}', async (event) => { - var _a, _b, _c, _d, _e, _f, _g, _h; - const { coupleId, sessionId } = event.params; - // The per-couple active-session lock lives at sessions/_active — it is a pointer, not a - // game session, so it must never produce a partner notification. - if (sessionId === '_active') - return; - const change = event.data; - if (!change) - return; - if (!change.after.exists) - return; // deletion — nothing to notify (skip the four reads below) - const db = admin.firestore(); - const messaging = admin.messaging(); - // Get the session document - const sessionDoc = await db.collection('couples').doc(coupleId).collection('sessions').doc(sessionId).get(); - const session = sessionDoc.data(); - if (!session) { - log_1.logger.log(`[onGameSessionUpdate] session ${sessionId} not found, skipping`); - return; - } - // Get couple info - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) { - log_1.logger.warn(`[onGameSessionUpdate] couple ${coupleId} not found`); - return; - } - const coupleData = (_a = coupleDoc.data()) !== null && _a !== void 0 ? _a : {}; - const userIds = ((_b = coupleData.userIds) !== null && _b !== void 0 ? _b : []); - if (userIds.length !== 2) { - log_1.logger.warn(`[onGameSessionUpdate] invalid couple ${coupleId}: expected 2 users, got ${userIds.length}`); - return; - } - const partnerA = userIds[0]; - const partnerB = userIds[1]; - const userA = await db.collection('users').doc(partnerA).get(); - const userB = await db.collection('users').doc(partnerB).get(); - // displayName is E2EE in users/{uid}, so the OS-rendered push uses a generic label; the app shows - // the real name in-app (resolved locally). Avatar (photoUrl) stays plaintext and is still sent. - const partnerAName = 'Your partner'; - const partnerBName = 'Your partner'; - const avatarA = (_c = userA.data()) === null || _c === void 0 ? void 0 : _c.photoUrl; - const avatarB = (_d = userB.data()) === null || _d === void 0 ? void 0 : _d.photoUrl; - // M-001: per-recipient quiet-hours lookup ("no notifications" promise). Fail-open. - const dataFor = (uid) => (uid === partnerA ? userA.data() : userB.data()); - const currentData = (_e = change.after.data()) !== null && _e !== void 0 ? _e : {}; - const status = currentData.status; - const sessionRef = db.collection('couples').doc(coupleId).collection('sessions').doc(sessionId); - // Detect start/finish from the session's CURRENT state + a one-time flag, NOT from the - // change.before→change.after status diff. The atomic session start (F-RACE-001) writes the - // session doc AND the sessions/_active pointer in a single transaction; transactional writes - // can be delivered to onWrite with change.before === change.after (the "Snapshot has no - // readTime" path), which made the inactive→active edge unreliable and intermittently dropped - // the partner_started_game push. Claiming a flag inside a transaction makes each notification - // fire exactly once no matter how the event is delivered (and prevents double-sends). - // ── New session started ────────────────────────────────────────────── - if (status === 'active' && !currentData.startNotifiedAt) { - const claimed = await db.runTransaction(async (tx) => { - const fresh = await tx.get(sessionRef); - const d = fresh.data(); - if (!fresh.exists || !d || d.status !== 'active' || d.startNotifiedAt) - return false; - tx.update(sessionRef, { startNotifiedAt: admin.firestore.FieldValue.serverTimestamp() }); - return true; - }); - if (claimed) { - const startedBy = currentData.startedByUserId; - const gameType = (_f = currentData.gameType) !== null && _f !== void 0 ? _f : 'wheel'; - const recipientId = startedBy === partnerA ? partnerB : partnerA; - const starterName = startedBy === partnerA ? partnerAName : partnerBName; - const starterAvatar = startedBy === partnerA ? avatarA : avatarB; - if ((0, quietHours_1.recipientInQuietHours)(dataFor(recipientId))) { - log_1.logger.log(`[onGameSessionUpdate] recipient ${recipientId} in quiet hours — suppressing start push`); - } - else { - await notifyPartner(db, messaging, recipientId, starterName, gameType, 'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId, starterAvatar, sessionId); - } - } - return; - } - // ── Partner joined an active session ───────────────────────────────── - // The non-starter opening the session writes their uid into `joinedByUsers` (client, rule-gated). - // Notify the STARTER once, with the joiner's name + avatar. One-time via `joinNotifiedAt` - // (server-only flag, claimed in a transaction — same pattern as start/finishNotifiedAt). - if (status === 'active' && !currentData.joinNotifiedAt && Array.isArray(currentData.joinedByUsers)) { - const startedBy = currentData.startedByUserId; - const joiner = currentData.joinedByUsers.find((u) => u && u !== startedBy); - if (joiner) { - const claimed = await db.runTransaction(async (tx) => { - const fresh = await tx.get(sessionRef); - const d = fresh.data(); - if (!fresh.exists || !d || d.status !== 'active' || d.joinNotifiedAt) - return false; - const j = Array.isArray(d.joinedByUsers) ? d.joinedByUsers : []; - if (!j.some((u) => u && u !== d.startedByUserId)) - return false; - tx.update(sessionRef, { joinNotifiedAt: admin.firestore.FieldValue.serverTimestamp() }); - return true; - }); - if (claimed) { - const gameType = (_g = currentData.gameType) !== null && _g !== void 0 ? _g : 'wheel'; - const joinerName = joiner === partnerA ? partnerAName : partnerBName; - const joinerAvatar = joiner === partnerA ? avatarA : avatarB; - if ((0, quietHours_1.recipientInQuietHours)(dataFor(startedBy))) { - log_1.logger.log(`[onGameSessionUpdate] starter ${startedBy} in quiet hours — suppressing join push`); - } - else { - await notifyPartner(db, messaging, startedBy, joinerName, gameType, 'partner_joined_game', `${joinerName} joined your game — tap to play together.`, coupleId, joinerAvatar, sessionId); - } - } - } - return; - } - // ── Session completed (reveal ready for both) ──────────────────────── - // Only a REAL completion has results: the async flow marks each user complete at reveal, so - // status flips to 'completed' with both uids in completedByUsers. An abandoned/quit session - // also lands on 'completed' (the abandon write) but with an empty list — pushing - // "finished — see your results!" for those was a false notification into an empty reveal. - const completedBy = Array.isArray(currentData.completedByUsers) ? currentData.completedByUsers : []; - if (status === 'completed' && !currentData.finishNotifiedAt && completedBy.length >= 2) { - const claimed = await db.runTransaction(async (tx) => { - const fresh = await tx.get(sessionRef); - const d = fresh.data(); - if (!fresh.exists || !d || d.status !== 'completed' || d.finishNotifiedAt) - return false; - tx.update(sessionRef, { finishNotifiedAt: admin.firestore.FieldValue.serverTimestamp() }); - return true; - }); - if (claimed) { - const gt = (_h = currentData.gameType) !== null && _h !== void 0 ? _h : 'wheel'; - // Notify BOTH partners, each naming the OTHER. M-001: skip a recipient in quiet hours. - if ((0, quietHours_1.recipientInQuietHours)(dataFor(partnerA))) { - log_1.logger.log(`[onGameSessionUpdate] ${partnerA} in quiet hours — suppressing finish push`); - } - else { - await notifyPartner(db, messaging, partnerA, partnerBName, gt, 'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId, avatarB, sessionId); - } - if ((0, quietHours_1.recipientInQuietHours)(dataFor(partnerB))) { - log_1.logger.log(`[onGameSessionUpdate] ${partnerB} in quiet hours — suppressing finish push`); - } - else { - await notifyPartner(db, messaging, partnerB, partnerAName, gt, 'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId, avatarA, sessionId); - } - } - return; - } -}); -/** - * Notify the WAITING partner the moment the FIRST player finishes their part of an async game. - * - * Async games (this_or_that / wheel / how_well / desire_sync) write each player's answers to - * couples/{coupleId}/{gameType}/{sessionId}.answers[uid]; the SESSION doc only flips to - * 'completed' once BOTH have answered (which onGameSessionUpdate turns into partner_finished_game). - * Between first-finish and both-finish the waiting partner got NOTHING — they never learned it was - * their turn (the symptom: "X finished a game but the partner was never notified"). The - * PARTNER_COMPLETED_PART client route already exists; this is the trigger that finally emits it. - * - * Path: couples/{coupleId}/{gameType}/{sessionId} (answer doc; same id as the session doc). - * - * Deployed as four narrow, explicitly-pathed triggers (one per async-game collection) sharing the - * handler below, rather than one broad `{gameType}` wildcard — the wildcard fired a (no-op) - * invocation on every write to ANY couple subcollection (messages, reactions, …); the narrow paths - * only fire for the four game collections. - */ -async function handleGamePartFinished(coupleId, gameType, sessionId, change) { - var _a, _b, _c, _d, _e; - if (!(change === null || change === void 0 ? void 0 : change.after.exists)) - return; - const answers = ((_b = (_a = change.after.data()) === null || _a === void 0 ? void 0 : _a.answers) !== null && _b !== void 0 ? _b : {}); - const answerUids = Object.keys(answers); - // Only the FIRST finisher (exactly one answer present) nudges the partner. Zero = session just - // created; two = both done → the session flips to completed and onGameSessionUpdate sends - // partner_finished_game instead. - if (answerUids.length !== 1) - return; - const finisherUid = answerUids[0]; - const db = admin.firestore(); - const messaging = admin.messaging(); - const sessionRef = db.collection('couples').doc(coupleId).collection('sessions').doc(sessionId); - // Claim a one-time flag on the SESSION doc (consistent with start/finishNotifiedAt; rule-safe; - // writing it re-fires onGameSessionUpdate but that no-ops on an active+already-started session). - const claimed = await db.runTransaction(async (tx) => { - const fresh = await tx.get(sessionRef); - const d = fresh.data(); - if (!fresh.exists || !d) - return false; - if (d.status === 'completed' || d.partFinishNotifiedAt) - return false; - tx.update(sessionRef, { partFinishNotifiedAt: admin.firestore.FieldValue.serverTimestamp() }); - return true; - }); - if (!claimed) - return; - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []); - const recipient = userIds.find((u) => u !== finisherUid); - if (!recipient) - return; - const finisher = await db.collection('users').doc(finisherUid).get(); - const finisherName = 'Your partner'; // displayName is E2EE; the app shows the real name in-app - const finisherAvatar = (_e = finisher.data()) === null || _e === void 0 ? void 0 : _e.photoUrl; - await notifyPartner(db, messaging, recipient, finisherName, gameType, 'partner_completed_part', yourTurnBody(gameType), coupleId, finisherAvatar, sessionId); -} -exports.onThisOrThatPartFinished = (0, firestore_1.onDocumentWritten)('couples/{coupleId}/this_or_that/{sessionId}', (event) => handleGamePartFinished(event.params.coupleId, 'this_or_that', event.params.sessionId, event.data)); -exports.onWheelPartFinished = (0, firestore_1.onDocumentWritten)('couples/{coupleId}/wheel/{sessionId}', (event) => handleGamePartFinished(event.params.coupleId, 'wheel', event.params.sessionId, event.data)); -exports.onHowWellPartFinished = (0, firestore_1.onDocumentWritten)('couples/{coupleId}/how_well/{sessionId}', (event) => handleGamePartFinished(event.params.coupleId, 'how_well', event.params.sessionId, event.data)); -exports.onDesireSyncPartFinished = (0, firestore_1.onDocumentWritten)('couples/{coupleId}/desire_sync/{sessionId}', (event) => handleGamePartFinished(event.params.coupleId, 'desire_sync', event.params.sessionId, event.data)); -/** - * Per-game body for the "your turn" (partner_completed_part) push — mirrors the client banner's - * YOUR_TURN copy in ui/components/GamePromptBanner.kt so foreground and background read the same. - */ -function yourTurnBody(gameType) { - switch (gameType) { - case 'how_well': return 'Your turn — guess their answers'; - case 'desire_sync': return 'Your turn — only mutual yeses ever show'; - case 'this_or_that': return 'Your turn — see where you line up'; - default: return 'Your turn — reveal how you line up'; - } -} -/** - * Send notification to partner via FCM and write to notification_queue. - */ -async function notifyPartner(db, messaging, partnerId, partnerName, gameType, notificationType, body, coupleId, senderAvatarUrl, sessionId) { - const title = notificationType === 'partner_finished_game' - ? `${partnerName} finished the game` - : notificationType === 'partner_completed_part' - ? `${partnerName} finished their part` - : notificationType === 'partner_joined_game' - ? `${partnerName} joined your game` - : `${partnerName} is playing`; - // Write an in-app notification record for the partner - await db - .collection('users') - .doc(partnerId) - .collection('notification_queue') - .add({ - type: notificationType, - title, - body, - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - await (0, push_1.sendPushToUser)(db, messaging, partnerId, { - notification: { title, body }, - // Put backgrounded notifications on the Games channel instead of the FCM fallback channel, - // so importance/sound and the per-category toggle apply. E-OBS. - android: { notification: { channelId: 'game_activity' } }, - data: Object.assign(Object.assign({ type: notificationType, couple_id: coupleId, game_type: gameType, - // The acting partner's display name (public; also in the title) so the in-app foreground - // banner can name them instead of a generic "Your partner". - sender_name: partnerName }, (sessionId ? { game_session_id: sessionId } : {})), (senderAvatarUrl && senderAvatarUrl.length > 0 - ? { sender_avatar_url: senderAvatarUrl } - : {})), - }); -} -//# sourceMappingURL=onGameSessionUpdate.js.map \ No newline at end of file diff --git a/functions/dist/games/onGameSessionUpdate.js.map b/functions/dist/games/onGameSessionUpdate.js.map deleted file mode 100644 index d29ac602..00000000 --- a/functions/dist/games/onGameSessionUpdate.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onGameSessionUpdate.js","sourceRoot":"","sources":["../../src/games/onGameSessionUpdate.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/index.js b/functions/dist/index.js deleted file mode 100644 index 7446c5ee..00000000 --- a/functions/dist/index.js +++ /dev/null @@ -1,125 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.wrapReleaseKeyCallable = exports.onDesireSyncPartFinished = exports.onHowWellPartFinished = exports.onWheelPartFinished = exports.onThisOrThatPartFinished = exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.aggregateOutcomeStats = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleKeyRotated = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerRevealed = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = exports.cleanupExpiredRestoreRequests = exports.onRestoreFulfilled = exports.onRestoreRequested = exports.onDateHistoryCreated = exports.onDateReflectionRevealed = exports.onDateReflectionWritten = exports.notifyOnDateMatch = exports.checkDeviceIntegrity = exports.sendReengagementReminder = exports.sendStreakReminder = exports.sendDailyQuestionProactiveReminder = exports.unlockDueMemoryCapsules = exports.sendChallengeDayReminders = exports.sendThinkingOfYouCallable = exports.sendGentleReminderCallable = exports.onEntitlementChanged = exports.syncEntitlement = exports.revenueCatWebhook = void 0; -// Global v2 options (region pin, instance cap) — MUST be imported before any function module so -// setGlobalOptions runs before the functions below are defined. -require("./options"); -const admin = __importStar(require("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(); -} -// 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 -var revenueCatWebhook_1 = require("./billing/revenueCatWebhook"); -Object.defineProperty(exports, "revenueCatWebhook", { enumerable: true, get: function () { return revenueCatWebhook_1.revenueCatWebhook; } }); -var syncEntitlement_1 = require("./billing/syncEntitlement"); -Object.defineProperty(exports, "syncEntitlement", { enumerable: true, get: function () { return syncEntitlement_1.syncEntitlement; } }); -var onEntitlementChanged_1 = require("./billing/onEntitlementChanged"); -Object.defineProperty(exports, "onEntitlementChanged", { enumerable: true, get: function () { return onEntitlementChanged_1.onEntitlementChanged; } }); -var sendGentleReminderCallable_1 = require("./notifications/sendGentleReminderCallable"); -Object.defineProperty(exports, "sendGentleReminderCallable", { enumerable: true, get: function () { return sendGentleReminderCallable_1.sendGentleReminderCallable; } }); -var sendThinkingOfYouCallable_1 = require("./notifications/sendThinkingOfYouCallable"); -Object.defineProperty(exports, "sendThinkingOfYouCallable", { enumerable: true, get: function () { return sendThinkingOfYouCallable_1.sendThinkingOfYouCallable; } }); -var gameRetention_1 = require("./notifications/gameRetention"); -Object.defineProperty(exports, "sendChallengeDayReminders", { enumerable: true, get: function () { return gameRetention_1.sendChallengeDayReminders; } }); -Object.defineProperty(exports, "unlockDueMemoryCapsules", { enumerable: true, get: function () { return gameRetention_1.unlockDueMemoryCapsules; } }); -var dailyQuestionReminder_1 = require("./notifications/dailyQuestionReminder"); -Object.defineProperty(exports, "sendDailyQuestionProactiveReminder", { enumerable: true, get: function () { return dailyQuestionReminder_1.sendDailyQuestionProactiveReminder; } }); -var streakReminder_1 = require("./notifications/streakReminder"); -Object.defineProperty(exports, "sendStreakReminder", { enumerable: true, get: function () { return streakReminder_1.sendStreakReminder; } }); -var reengagement_1 = require("./notifications/reengagement"); -Object.defineProperty(exports, "sendReengagementReminder", { enumerable: true, get: function () { return reengagement_1.sendReengagementReminder; } }); -var checkDeviceIntegrity_1 = require("./security/checkDeviceIntegrity"); -Object.defineProperty(exports, "checkDeviceIntegrity", { enumerable: true, get: function () { return checkDeviceIntegrity_1.checkDeviceIntegrity; } }); -var createDateMatch_1 = require("./dates/createDateMatch"); -Object.defineProperty(exports, "notifyOnDateMatch", { enumerable: true, get: function () { return createDateMatch_1.notifyOnDateMatch; } }); -var onDateReflectionWritten_1 = require("./dates/onDateReflectionWritten"); -Object.defineProperty(exports, "onDateReflectionWritten", { enumerable: true, get: function () { return onDateReflectionWritten_1.onDateReflectionWritten; } }); -var onDateReflectionRevealed_1 = require("./dates/onDateReflectionRevealed"); -Object.defineProperty(exports, "onDateReflectionRevealed", { enumerable: true, get: function () { return onDateReflectionRevealed_1.onDateReflectionRevealed; } }); -var onDateHistoryCreated_1 = require("./dates/onDateHistoryCreated"); -Object.defineProperty(exports, "onDateHistoryCreated", { enumerable: true, get: function () { return onDateHistoryCreated_1.onDateHistoryCreated; } }); -var onRestoreRequested_1 = require("./backup/onRestoreRequested"); -Object.defineProperty(exports, "onRestoreRequested", { enumerable: true, get: function () { return onRestoreRequested_1.onRestoreRequested; } }); -Object.defineProperty(exports, "onRestoreFulfilled", { enumerable: true, get: function () { return onRestoreRequested_1.onRestoreFulfilled; } }); -var cleanupRestoreRequests_1 = require("./backup/cleanupRestoreRequests"); -Object.defineProperty(exports, "cleanupExpiredRestoreRequests", { enumerable: true, get: function () { return cleanupRestoreRequests_1.cleanupExpiredRestoreRequests; } }); -var assignDailyQuestion_1 = require("./questions/assignDailyQuestion"); -Object.defineProperty(exports, "assignDailyQuestion", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestion; } }); -Object.defineProperty(exports, "assignDailyQuestionCallable", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestionCallable; } }); -var onAnswerWritten_1 = require("./questions/onAnswerWritten"); -Object.defineProperty(exports, "onAnswerWritten", { enumerable: true, get: function () { return onAnswerWritten_1.onAnswerWritten; } }); -var onAnswerRevealed_1 = require("./questions/onAnswerRevealed"); -Object.defineProperty(exports, "onAnswerRevealed", { enumerable: true, get: function () { return onAnswerRevealed_1.onAnswerRevealed; } }); -var onMessageWritten_1 = require("./questions/onMessageWritten"); -Object.defineProperty(exports, "onMessageWritten", { enumerable: true, get: function () { return onMessageWritten_1.onMessageWritten; } }); -var onCoupleLeave_1 = require("./couples/onCoupleLeave"); -Object.defineProperty(exports, "onCoupleLeave", { enumerable: true, get: function () { return onCoupleLeave_1.onCoupleLeave; } }); -var onCoupleKeyRotated_1 = require("./couples/onCoupleKeyRotated"); -Object.defineProperty(exports, "onCoupleKeyRotated", { enumerable: true, get: function () { return onCoupleKeyRotated_1.onCoupleKeyRotated; } }); -var leaveCoupleCallable_1 = require("./couples/leaveCoupleCallable"); -Object.defineProperty(exports, "leaveCoupleCallable", { enumerable: true, get: function () { return leaveCoupleCallable_1.leaveCoupleCallable; } }); -var acceptInviteCallable_1 = require("./couples/acceptInviteCallable"); -Object.defineProperty(exports, "acceptInviteCallable", { enumerable: true, get: function () { return acceptInviteCallable_1.acceptInviteCallable; } }); -var createInviteCallable_1 = require("./couples/createInviteCallable"); -Object.defineProperty(exports, "createInviteCallable", { enumerable: true, get: function () { return createInviteCallable_1.createInviteCallable; } }); -var submitOutcomeCallable_1 = require("./couples/submitOutcomeCallable"); -Object.defineProperty(exports, "submitOutcomeCallable", { enumerable: true, get: function () { return submitOutcomeCallable_1.submitOutcomeCallable; } }); -var aggregateOutcomes_1 = require("./couples/aggregateOutcomes"); -Object.defineProperty(exports, "aggregateOutcomeStats", { enumerable: true, get: function () { return aggregateOutcomes_1.aggregateOutcomeStats; } }); -var scheduledOutcomesReminder_1 = require("./couples/scheduledOutcomesReminder"); -Object.defineProperty(exports, "scheduledOutcomesReminder", { enumerable: true, get: function () { return scheduledOutcomesReminder_1.scheduledOutcomesReminder; } }); -var onUserDelete_1 = require("./users/onUserDelete"); -Object.defineProperty(exports, "onUserDelete", { enumerable: true, get: function () { return onUserDelete_1.onUserDelete; } }); -var onGameSessionUpdate_1 = require("./games/onGameSessionUpdate"); -Object.defineProperty(exports, "onGameSessionUpdate", { enumerable: true, get: function () { return onGameSessionUpdate_1.onGameSessionUpdate; } }); -Object.defineProperty(exports, "onThisOrThatPartFinished", { enumerable: true, get: function () { return onGameSessionUpdate_1.onThisOrThatPartFinished; } }); -Object.defineProperty(exports, "onWheelPartFinished", { enumerable: true, get: function () { return onGameSessionUpdate_1.onWheelPartFinished; } }); -Object.defineProperty(exports, "onHowWellPartFinished", { enumerable: true, get: function () { return onGameSessionUpdate_1.onHowWellPartFinished; } }); -Object.defineProperty(exports, "onDesireSyncPartFinished", { enumerable: true, get: function () { return onGameSessionUpdate_1.onDesireSyncPartFinished; } }); -var wrapReleaseKeyCallable_1 = require("./releaseKey/wrapReleaseKeyCallable"); -Object.defineProperty(exports, "wrapReleaseKeyCallable", { enumerable: true, get: function () { return wrapReleaseKeyCallable_1.wrapReleaseKeyCallable; } }); -// 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. -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/functions/dist/index.js.map b/functions/dist/index.js.map deleted file mode 100644 index 2a5a2bd5..00000000 --- a/functions/dist/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,gGAAgG;AAChG,gEAAgE;AAChE,qBAAkB;AAClB,sDAAuC;AAEvC,qEAAqE;AACrE,8EAA8E;AAC9E,gFAAgF;AAChF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC5B,KAAK,CAAC,aAAa,EAAE,CAAA;AACvB,CAAC;AAED,mGAAmG;AACnG,8FAA8F;AAC9F,iGAAiG;AACjG,8CAA8C;AAC9C,6DAA6D;AAC7D,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yFAAuF;AAA9E,wIAAA,0BAA0B,OAAA;AACnC,uFAAqF;AAA5E,sIAAA,yBAAyB,OAAA;AAClC,+DAGsC;AAFpC,0HAAA,yBAAyB,OAAA;AACzB,wHAAA,uBAAuB,OAAA;AAEzB,+EAA0F;AAAjF,2IAAA,kCAAkC,OAAA;AAC3C,iEAAmE;AAA1D,oHAAA,kBAAkB,OAAA;AAC3B,6DAAuE;AAA9D,wHAAA,wBAAwB,OAAA;AACjC,wEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,2DAA2D;AAAlD,oHAAA,iBAAiB,OAAA;AAC1B,2EAAyE;AAAhE,kIAAA,uBAAuB,OAAA;AAChC,6EAA2E;AAAlE,oIAAA,wBAAwB,OAAA;AACjC,qEAAmE;AAA1D,4HAAA,oBAAoB,OAAA;AAC7B,kEAAoF;AAA3E,wHAAA,kBAAkB,OAAA;AAAE,wHAAA,kBAAkB,OAAA;AAC/C,0EAA+E;AAAtE,uIAAA,6BAA6B,OAAA;AACtC,uEAGwC;AAFtC,0HAAA,mBAAmB,OAAA;AACnB,kIAAA,2BAA2B,OAAA;AAE7B,+DAA6D;AAApD,kHAAA,eAAe,OAAA;AACxB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,yDAAuD;AAA9C,8GAAA,aAAa,OAAA;AACtB,mEAAiE;AAAxD,wHAAA,kBAAkB,OAAA;AAC3B,qEAAmE;AAA1D,0HAAA,mBAAmB,OAAA;AAC5B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yEAAuE;AAA9D,8HAAA,qBAAqB,OAAA;AAC9B,iEAAmE;AAA1D,0HAAA,qBAAqB,OAAA;AAC9B,iFAA+E;AAAtE,sIAAA,yBAAyB,OAAA;AAClC,qDAAmD;AAA1C,4GAAA,YAAY,OAAA;AACrB,mEAMoC;AALlC,0HAAA,mBAAmB,OAAA;AACnB,+HAAA,wBAAwB,OAAA;AACxB,0HAAA,mBAAmB,OAAA;AACnB,4HAAA,qBAAqB,OAAA;AACrB,+HAAA,wBAAwB,OAAA;AAG1B,8EAA4E;AAAnE,gIAAA,sBAAsB,OAAA;AAE/B,oFAAoF;AACpF,uEAAuE;AACvE,iFAAiF;AACjF,0DAA0D"} \ No newline at end of file diff --git a/functions/dist/log.js b/functions/dist/log.js deleted file mode 100644 index 35f93990..00000000 --- a/functions/dist/log.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.logger = void 0; -exports.redactToken = redactToken; -const logger = __importStar(require("firebase-functions/logger")); -exports.logger = logger; -/** - * FCM registration tokens are device secrets — never log them in full. Use this to reduce a token - * to a short, non-identifying suffix for correlation in logs. - */ -function redactToken(token) { - if (!token) - return '(none)'; - return token.length <= 6 ? '***' : `…${token.slice(-6)}`; -} -//# sourceMappingURL=log.js.map \ No newline at end of file diff --git a/functions/dist/log.js.map b/functions/dist/log.js.map deleted file mode 100644 index 5782911e..00000000 --- a/functions/dist/log.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaA,kCAGC;AAhBD,kEAAmD;AAO1C,wBAAM;AAEf;;;GAGG;AACH,SAAgB,WAAW,CAAC,KAAgC;IAC1D,IAAI,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAA;IAC3B,OAAO,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AAC1D,CAAC"} \ No newline at end of file diff --git a/functions/dist/notifications/dailyQuestionReminder.js b/functions/dist/notifications/dailyQuestionReminder.js deleted file mode 100644 index 6d577e83..00000000 --- a/functions/dist/notifications/dailyQuestionReminder.js +++ /dev/null @@ -1,166 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendDailyQuestionProactiveReminder = void 0; -const admin = __importStar(require("firebase-admin")); -const scheduler_1 = require("firebase-functions/v2/scheduler"); -const quietHours_1 = require("./quietHours"); -const push_1 = require("./push"); -const log_1 = require("../log"); -/** - * Proactive daily question reminder. - * - * Schedule: 4:00 PM America/Chicago (2 hours before the question expires at 6 PM). - * - * Logic: - * 1. Query all `daily_question` docs (collection group) with expiresAt in the - * next 3 hours — these are the ones expiring today. - * 2. For each, count the answers subcollection. If 0 answers (neither partner - * has responded), the couple needs a nudge. - * 3. Skip couples where a gentle_reminder or automated_daily_reminder was - * already sent today (avoid over-notifying couples where a partner already - * manually nudged the other). - * 4. Send FCM to all users in the couple + write to notification_queue. - * 5. Record automated_daily_reminder/{docId} so this function is idempotent - * on re-runs. - */ -exports.sendDailyQuestionProactiveReminder = (0, scheduler_1.onSchedule)({ schedule: '0 16 * * *', timeZone: 'America/Chicago', timeoutSeconds: 180 }, async () => { - const db = admin.firestore(); - const messaging = admin.messaging(); - const now = Date.now(); - const threeHoursMs = 3 * 60 * 60 * 1000; - // Docs expiring within the next 3 hours — the active window for today's question. - const expiringSnap = await db - .collectionGroup('daily_question') - .where('expiresAt', '>', admin.firestore.Timestamp.fromMillis(now)) - .where('expiresAt', '<', admin.firestore.Timestamp.fromMillis(now + threeHoursMs)) - .get(); - let notified = 0; - let skipped = 0; - // Per-couple failures are isolated so one bad couple can't abort the whole run. - await Promise.allSettled(expiringSnap.docs.map(async (questionDoc) => { - var _a, _b; - const coupleRef = questionDoc.ref.parent.parent; - if (!coupleRef) - return; - const reminderId = `auto_${questionDoc.id}`; - const reminderRef = coupleRef.collection('daily_reminders').doc(reminderId); - // Idempotency: skip if we already sent this reminder. - const alreadySent = await reminderRef.get(); - if (alreadySent.exists) { - skipped++; - return; - } - // Skip if a manual gentle_reminder was sent today (same date key). - const dateKey = questionDoc.id; // daily_question docs are keyed by date (YYYY-MM-DD) - const gentleRef = coupleRef.collection('gentle_reminders').doc(dateKey); - const gentleSent = await gentleRef.get(); - if (gentleSent.exists) { - skipped++; - return; - } - // Check answer count — only remind if nobody has answered yet. - const answersSnap = await questionDoc.ref.collection('answers').limit(1).get(); - if (!answersSnap.empty) { - skipped++; - return; - } - // Fetch couple members. - const coupleDoc = await coupleRef.get(); - const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []); - if (userIds.length === 0) { - skipped++; - return; - } - // Claim the reminder slot atomically. - try { - await db.runTransaction(async (tx) => { - const fresh = await tx.get(reminderRef); - if (fresh.exists) - throw new Error('already_sent'); - tx.set(reminderRef, { - sentAt: admin.firestore.FieldValue.serverTimestamp(), - questionDate: dateKey, - }); - }); - } - catch (_c) { - skipped++; - return; - } - // Send FCM + notification_queue entry to every user in the couple. - await Promise.allSettled(userIds.map((userId) => sendReminder(db, messaging, userId, coupleRef.id, dateKey))); - notified += userIds.length; - })); - log_1.logger.log(`[sendDailyQuestionProactiveReminder] scanned ${expiringSnap.size} docs; notified ${notified} users; skipped ${skipped}`); -}); -async function sendReminder(db, messaging, userId, coupleId, questionDate) { - const userDoc = await db.collection('users').doc(userId).get(); - const userData = userDoc.data(); - // Respect the user's Daily Reminder toggle (default on) and quiet hours. - if ((userData === null || userData === void 0 ? void 0 : userData.notifDailyReminder) === false) { - log_1.logger.log(`[sendDailyQuestionProactiveReminder] skip ${userId} — daily reminder off`); - return; - } - if ((0, quietHours_1.recipientInQuietHours)(userData)) { - log_1.logger.log(`[sendDailyQuestionProactiveReminder] skip ${userId} — quiet hours`); - return; - } - // In-app notification record. - await db - .collection('users') - .doc(userId) - .collection('notification_queue') - .add({ - type: 'daily_question_reminder', - title: "Tonight's question is waiting.", - body: 'Answer together before it expires.', - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - await (0, push_1.sendPushToUser)(db, messaging, userId, { - notification: { - title: "Tonight's question is waiting.", - body: 'Answer together before it expires.', - }, - android: { notification: { channelId: 'reminders' } }, // E-OBS - data: { - type: 'daily_question_reminder', - couple_id: coupleId, - question_date: questionDate, - }, - }, userData); -} -//# sourceMappingURL=dailyQuestionReminder.js.map \ No newline at end of file diff --git a/functions/dist/notifications/dailyQuestionReminder.js.map b/functions/dist/notifications/dailyQuestionReminder.js.map deleted file mode 100644 index de685007..00000000 --- a/functions/dist/notifications/dailyQuestionReminder.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"dailyQuestionReminder.js","sourceRoot":"","sources":["../../src/notifications/dailyQuestionReminder.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/gameRetention.js b/functions/dist/notifications/gameRetention.js deleted file mode 100644 index e9d7d763..00000000 --- a/functions/dist/notifications/gameRetention.js +++ /dev/null @@ -1,213 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendChallengeDayReminders = exports.unlockDueMemoryCapsules = void 0; -const admin = __importStar(require("firebase-admin")); -const scheduler_1 = require("firebase-functions/v2/scheduler"); -const quietHours_1 = require("./quietHours"); -const push_1 = require("./push"); -const log_1 = require("../log"); -const DAY_MS = 24 * 60 * 60 * 1000; -const CHALLENGE_TITLES = { - gratitude_week: { title: 'Gratitude Week', durationDays: 7 }, - appreciation_notes: { title: 'Appreciation Notes', durationDays: 7 }, - quality_time: { title: 'Quality Time', durationDays: 7 }, - deep_conversations: { title: 'Deep Conversations', durationDays: 7 }, -}; -exports.unlockDueMemoryCapsules = (0, scheduler_1.onSchedule)('every 1 hours', async () => { - const db = admin.firestore(); - const messaging = admin.messaging(); - const now = Date.now(); - const snapshot = await db - .collectionGroup('capsules') - .where('status', '==', 'sealed') - .where('unlockAt', '<=', now) - .limit(100) - .get(); - const notifications = []; - for (const capsuleDoc of snapshot.docs) { - const capsuleRef = capsuleDoc.ref; - const coupleRef = capsuleRef.parent.parent; - if (!coupleRef) - continue; - const capsuleNotifications = await db.runTransaction(async (tx) => { - var _a, _b, _c, _d; - const freshCapsule = await tx.get(capsuleRef); - const capsule = (_a = freshCapsule.data()) !== null && _a !== void 0 ? _a : {}; - if (capsule.status !== 'sealed' || Number((_b = capsule.unlockAt) !== null && _b !== void 0 ? _b : 0) > now) { - return []; - } - const coupleDoc = await tx.get(coupleRef); - const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []); - if (userIds.length === 0) - return []; - tx.update(capsuleRef, { - status: 'unlocked', - unlockedAt: now, - unlockNotifiedAt: admin.firestore.FieldValue.serverTimestamp(), - }); - const capsuleId = capsuleRef.id; - const coupleId = coupleRef.id; - const title = typeof capsule.title === 'string' && capsule.title.trim().length > 0 - ? capsule.title.trim() - : 'A memory capsule'; - return userIds.map((userId) => ({ - userId, - type: 'memory_capsule_unlocked', - title: 'Your memory capsule opened', - body: `${title} is ready to read together.`, - data: { couple_id: coupleId, capsule_id: capsuleId }, - })); - }); - notifications.push(...capsuleNotifications); - } - // Per-recipient failures are isolated so one bad send can't abort the batch. - await Promise.allSettled(notifications.map((notification) => sendNotification(db, messaging, notification))); - log_1.logger.log(`[unlockDueMemoryCapsules] unlocked ${snapshot.size}; notified ${notifications.length}`); -}); -exports.sendChallengeDayReminders = (0, scheduler_1.onSchedule)('every 24 hours', async () => { - const db = admin.firestore(); - const messaging = admin.messaging(); - const now = Date.now(); - const snapshot = await db - .collectionGroup('challenges') - .where('status', '==', 'active') - .limit(100) - .get(); - const notifications = []; - for (const challengeDoc of snapshot.docs) { - const challengeRef = challengeDoc.ref; - const coupleRef = challengeRef.parent.parent; - if (!coupleRef) - continue; - const challengeNotifications = await db.runTransaction(async (tx) => { - var _a, _b, _c, _d, _e, _f, _g; - const freshChallenge = await tx.get(challengeRef); - const challenge = (_a = freshChallenge.data()) !== null && _a !== void 0 ? _a : {}; - if (challenge.status !== 'active') - return []; - const startedAt = Number((_b = challenge.startedAt) !== null && _b !== void 0 ? _b : 0); - if (startedAt <= 0 || startedAt > now) - return []; - const challengeId = typeof challenge.challengeId === 'string' - ? challenge.challengeId - : challengeRef.id; - const catalogEntry = (_c = CHALLENGE_TITLES[challengeId]) !== null && _c !== void 0 ? _c : { - title: 'Connection Challenge', - durationDays: 7, - }; - const day = Math.floor((now - startedAt) / DAY_MS) + 1; - if (day < 1 || day > catalogEntry.durationDays) - return []; - const coupleDoc = await tx.get(coupleRef); - const userIds = ((_e = (_d = coupleDoc.data()) === null || _d === void 0 ? void 0 : _d.userIds) !== null && _e !== void 0 ? _e : []); - if (userIds.length === 0) - return []; - const completions = ((_f = challenge.completions) !== null && _f !== void 0 ? _f : {}); - const reminderSent = ((_g = challenge.challengeReminderSent) !== null && _g !== void 0 ? _g : {}); - const dueUserIds = userIds.filter((userId) => { - var _a; - const completedDays = (_a = completions[userId]) !== null && _a !== void 0 ? _a : []; - const alreadyCompleted = completedDays.map(Number).includes(day); - const alreadySent = reminderSent[reminderKey(userId, day)] === true; - return !alreadyCompleted && !alreadySent; - }); - if (dueUserIds.length === 0) - return []; - const updates = { - lastChallengeReminderAt: admin.firestore.FieldValue.serverTimestamp(), - }; - dueUserIds.forEach((userId) => { - updates[`challengeReminderSent.${reminderKey(userId, day)}`] = true; - }); - tx.update(challengeRef, updates); - const coupleId = coupleRef.id; - return dueUserIds.map((userId) => ({ - userId, - type: 'challenge_day_ready', - title: `Day ${day} is ready`, - body: `${catalogEntry.title}: today's connection prompt is waiting.`, - data: { couple_id: coupleId, challenge_id: challengeId, day: String(day) }, - })); - }); - notifications.push(...challengeNotifications); - } - // Per-recipient failures are isolated so one bad send can't abort the batch. - await Promise.allSettled(notifications.map((notification) => sendNotification(db, messaging, notification))); - log_1.logger.log(`[sendChallengeDayReminders] scanned ${snapshot.size}; notified ${notifications.length}`); -}); -function reminderKey(userId, day) { - return `${userId.replace(/[^\w-]/g, '_')}_${day}`; -} -async function sendNotification(db, messaging, notification) { - const userDoc = await db.collection('users').doc(notification.userId).get(); - const userData = userDoc.data(); - // Challenge-day reminders are retention nudges → respect the promotional opt-out (default on). - // (Memory-capsule unlocks are a genuine couple event, so they are not promotional-gated.) - if (notification.type === 'challenge_day_ready' && (userData === null || userData === void 0 ? void 0 : userData.notifPromotional) === false) { - log_1.logger.log(`[sendNotification] skip ${notification.userId} — promotional off`); - return; - } - // Honor the recipient's quiet hours for every scheduled push. - if ((0, quietHours_1.recipientInQuietHours)(userData)) { - log_1.logger.log(`[sendNotification] skip ${notification.userId} — quiet hours`); - return; - } - await db - .collection('users') - .doc(notification.userId) - .collection('notification_queue') - .add({ - type: notification.type, - title: notification.title, - body: notification.body, - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - await (0, push_1.sendPushToUser)(db, messaging, notification.userId, { - notification: { - title: notification.title, - body: notification.body, - }, - // E-OBS: challenge reminders → Reminders channel; capsule-unlocked → partner-activity channel. - android: { - notification: { - channelId: notification.type === 'challenge_day_ready' ? 'reminders' : 'partner_activity', - }, - }, - data: Object.assign({ type: notification.type }, notification.data), - }, userData); -} -//# sourceMappingURL=gameRetention.js.map \ No newline at end of file diff --git a/functions/dist/notifications/gameRetention.js.map b/functions/dist/notifications/gameRetention.js.map deleted file mode 100644 index 23a1b358..00000000 --- a/functions/dist/notifications/gameRetention.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"gameRetention.js","sourceRoot":"","sources":["../../src/notifications/gameRetention.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/idempotency.js b/functions/dist/notifications/idempotency.js deleted file mode 100644 index b73264eb..00000000 --- a/functions/dist/notifications/idempotency.js +++ /dev/null @@ -1,75 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.claimOnce = claimOnce; -exports.notifMark = notifMark; -const admin = __importStar(require("firebase-admin")); -const log_1 = require("../log"); -/** gRPC status for a create() that hit an existing document. */ -const ALREADY_EXISTS = 6; -/** - * Best-effort one-time claim for a notification event, keyed by a deterministic marker doc. - * - * Background triggers are delivered AT LEAST ONCE, so a redelivery of the same event would - * otherwise double-send a push. `create()` is an atomic create-if-absent: the first delivery - * writes the marker and returns true; a redelivery finds it present and returns false, so the - * caller skips. Claim right before sending — a suppressed (quiet-hours / opted-out) notification - * should not burn a claim. - * - * Fail-OPEN: if create() fails for any reason OTHER than "already claimed", return true and let - * the notification proceed. A rare duplicate is better UX than a silently dropped ping, and it - * keeps an infra blip on the marker write from swallowing every notification. The trade this makes - * explicit: at-least-once → at-most-once (a send failure AFTER the claim drops that one push). - */ -async function claimOnce(markRef) { - try { - await markRef.create({ claimedAt: admin.firestore.FieldValue.serverTimestamp() }); - return true; - } - catch (e) { - if (e.code === ALREADY_EXISTS) - return false; - log_1.logger.warn('[claimOnce] marker create failed; proceeding without dedupe', { - path: markRef.path, - error: String(e), - }); - return true; - } -} -/** Standard per-couple marker ref for a notification event (`couples/{id}/notif_marks/{markId}`). */ -function notifMark(db, coupleId, markId) { - return db.collection('couples').doc(coupleId).collection('notif_marks').doc(markId); -} -//# sourceMappingURL=idempotency.js.map \ No newline at end of file diff --git a/functions/dist/notifications/idempotency.js.map b/functions/dist/notifications/idempotency.js.map deleted file mode 100644 index 4216cd11..00000000 --- a/functions/dist/notifications/idempotency.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../src/notifications/idempotency.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,8BAYC;AAGD,8BAMC;AAzCD,sDAAuC;AACvC,gCAA+B;AAE/B,gEAAgE;AAChE,MAAM,cAAc,GAAG,CAAC,CAAA;AAExB;;;;;;;;;;;;;GAaG;AACI,KAAK,UAAU,SAAS,CAAC,OAA0C;IACxE,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;QACjF,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAK,CAAuB,CAAC,IAAI,KAAK,cAAc;YAAE,OAAO,KAAK,CAAA;QAClE,YAAM,CAAC,IAAI,CAAC,6DAA6D,EAAE;YACzE,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;SACjB,CAAC,CAAA;QACF,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,qGAAqG;AACrG,SAAgB,SAAS,CACvB,EAA6B,EAC7B,QAAgB,EAChB,MAAc;IAEd,OAAO,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;AACrF,CAAC"} \ No newline at end of file diff --git a/functions/dist/notifications/idempotency.test.js b/functions/dist/notifications/idempotency.test.js deleted file mode 100644 index 833b29c2..00000000 --- a/functions/dist/notifications/idempotency.test.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const idempotency_1 = require("./idempotency"); -function markRef(create) { - return { path: 'couples/c1/notif_marks/m1', create }; -} -describe('claimOnce', () => { - it('claims (true) when the marker did not exist', async () => { - expect(await (0, idempotency_1.claimOnce)(markRef(async () => undefined))).toBe(true); - }); - it('does not claim (false) when the marker already exists (ALREADY_EXISTS = 6)', async () => { - expect(await (0, idempotency_1.claimOnce)(markRef(async () => Promise.reject({ code: 6 })))).toBe(false); - }); - it('fails open (true) on any other create error so a ping is not silently dropped', async () => { - expect(await (0, idempotency_1.claimOnce)(markRef(async () => Promise.reject({ code: 14 })))).toBe(true); - expect(await (0, idempotency_1.claimOnce)(markRef(async () => Promise.reject(new Error('boom'))))).toBe(true); - }); -}); -//# sourceMappingURL=idempotency.test.js.map \ No newline at end of file diff --git a/functions/dist/notifications/idempotency.test.js.map b/functions/dist/notifications/idempotency.test.js.map deleted file mode 100644 index 6b26c299..00000000 --- a/functions/dist/notifications/idempotency.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"idempotency.test.js","sourceRoot":"","sources":["../../src/notifications/idempotency.test.ts"],"names":[],"mappings":";;AAAA,+CAAyC;AAEzC,SAAS,OAAO,CAAC,MAA8B;IAC7C,OAAO,EAAE,IAAI,EAAE,2BAA2B,EAAE,MAAM,EAAS,CAAA;AAC7D,CAAC;AAED,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,CAAC,MAAM,IAAA,uBAAS,EAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;QAC1F,MAAM,CAAC,MAAM,IAAA,uBAAS,EAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACvF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,MAAM,CAAC,MAAM,IAAA,uBAAS,EAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACrF,MAAM,CAAC,MAAM,IAAA,uBAAS,EAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5F,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/functions/dist/notifications/pruneTokens.js b/functions/dist/notifications/pruneTokens.js deleted file mode 100644 index 711c4994..00000000 --- a/functions/dist/notifications/pruneTokens.js +++ /dev/null @@ -1,125 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.isDeadTokenError = isDeadTokenError; -exports.selectDeadTokens = selectDeadTokens; -exports.pruneDeadTokens = pruneDeadTokens; -const admin = __importStar(require("firebase-admin")); -const log_1 = require("../log"); -/** - * FCM error codes that mean a token is *permanently* dead and safe to delete. - * - * Deliberately narrow. We only prune on errors that unambiguously blame the TOKEN: - * - `registration-token-not-registered` — the app was uninstalled / data-cleared / token rotated. - * - `invalid-registration-token` — the token string itself is malformed. - * We intentionally do NOT include `messaging/invalid-argument` or any transient/server error - * (`unavailable`, `internal`, `quota-exceeded`, auth issues): those can be caused by a bad *payload* - * or a temporary outage, and treating them as dead would let one bug wipe every user's tokens. - */ -const DEAD_TOKEN_CODES = new Set([ - 'messaging/registration-token-not-registered', - 'messaging/invalid-registration-token', -]); -/** Extract the `messaging/*` code from a firebase-admin messaging rejection, wherever it lives. */ -function messagingErrorCode(reason) { - var _a, _b; - const r = reason; - const code = (_b = (_a = r === null || r === void 0 ? void 0 : r.errorInfo) === null || _a === void 0 ? void 0 : _a.code) !== null && _b !== void 0 ? _b : r === null || r === void 0 ? void 0 : r.code; - return typeof code === 'string' ? code : undefined; -} -/** True only when an FCM send rejection means the token is permanently invalid (safe to delete). */ -function isDeadTokenError(reason) { - const code = messagingErrorCode(reason); - return code !== undefined && DEAD_TOKEN_CODES.has(code); -} -/** - * Pure: given the same `tokens` array and `Promise.allSettled` results a caller already has, return the - * distinct token strings that FCM reported as permanently dead. Index `i` in results maps to `tokens[i]`. - */ -function selectDeadTokens(tokens, results) { - const dead = new Set(); - results.forEach((r, i) => { - const token = tokens[i]; - if (r.status === 'rejected' && token && isDeadTokenError(r.reason)) - dead.add(token); - }); - return [...dead]; -} -/** - * Best-effort removal of tokens FCM reported as permanently dead. Pass the `tokens` array and the - * `Promise.allSettled` results from the send you just performed; any dead token is deleted from the - * user's `fcmTokens` subcollection and cleared from the legacy `fcmToken` field if it matches. - * - * Never throws — pruning is housekeeping and must never fail the notification path it runs after. - * Returns the number of token records removed. - */ -async function pruneDeadTokens(db, uid, tokens, results) { - var _a; - const dead = new Set(selectDeadTokens(tokens, results)); - if (dead.size === 0) - return 0; - try { - const userRef = db.collection('users').doc(uid); - const [tokenSnap, userDoc] = await Promise.all([ - userRef.collection('fcmTokens').get(), - userRef.get(), - ]); - const batch = db.batch(); - let removed = 0; - tokenSnap.docs.forEach((d) => { - var _a; - const t = (_a = d.data()) === null || _a === void 0 ? void 0 : _a.token; - if (typeof t === 'string' && dead.has(t)) { - batch.delete(d.ref); - removed++; - } - }); - const legacy = (_a = userDoc.data()) === null || _a === void 0 ? void 0 : _a.fcmToken; - if (typeof legacy === 'string' && dead.has(legacy)) { - batch.update(userRef, { fcmToken: admin.firestore.FieldValue.delete() }); - removed++; - } - if (removed > 0) { - await batch.commit(); - log_1.logger.log(`[pruneDeadTokens] uid=${uid} removed ${removed} dead token(s)`); - } - return removed; - } - catch (e) { - log_1.logger.warn(`[pruneDeadTokens] uid=${uid} prune failed (ignored)`, { error: String(e) }); - return 0; - } -} -//# sourceMappingURL=pruneTokens.js.map \ No newline at end of file diff --git a/functions/dist/notifications/pruneTokens.js.map b/functions/dist/notifications/pruneTokens.js.map deleted file mode 100644 index 73632703..00000000 --- a/functions/dist/notifications/pruneTokens.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"pruneTokens.js","sourceRoot":"","sources":["../../src/notifications/pruneTokens.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/pruneTokens.test.js b/functions/dist/notifications/pruneTokens.test.js deleted file mode 100644 index 3d610deb..00000000 --- a/functions/dist/notifications/pruneTokens.test.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const pruneTokens_1 = require("./pruneTokens"); -// Shapes mirror what firebase-admin's FirebaseMessagingError actually throws (see errorInfo.code), -// plus the `code` getter form. Both must be recognised; transient/server errors must NOT be. -const deadNotRegistered = { errorInfo: { code: 'messaging/registration-token-not-registered' }, codePrefix: 'messaging' }; -const deadInvalidToken = { code: 'messaging/invalid-registration-token' }; -const transientUnavailable = { errorInfo: { code: 'messaging/server-unavailable' } }; -const transientInternal = { code: 'messaging/internal-error' }; -const badPayload = { errorInfo: { code: 'messaging/invalid-argument' } }; // payload bug — must NOT prune -const rej = (reason) => ({ status: 'rejected', reason }); -const ful = () => ({ status: 'fulfilled', value: 'id' }); -describe('isDeadTokenError', () => { - it('is true for permanently-dead token codes (both errorInfo.code and code shapes)', () => { - expect((0, pruneTokens_1.isDeadTokenError)(deadNotRegistered)).toBe(true); - expect((0, pruneTokens_1.isDeadTokenError)(deadInvalidToken)).toBe(true); - }); - it('is false for transient/server errors — never prune on these', () => { - expect((0, pruneTokens_1.isDeadTokenError)(transientUnavailable)).toBe(false); - expect((0, pruneTokens_1.isDeadTokenError)(transientInternal)).toBe(false); - }); - it('is false for invalid-argument — that blames the payload, not the token', () => { - expect((0, pruneTokens_1.isDeadTokenError)(badPayload)).toBe(false); - }); - it('is false for missing/garbage reasons (fail safe)', () => { - expect((0, pruneTokens_1.isDeadTokenError)(undefined)).toBe(false); - expect((0, pruneTokens_1.isDeadTokenError)(null)).toBe(false); - expect((0, pruneTokens_1.isDeadTokenError)({})).toBe(false); - expect((0, pruneTokens_1.isDeadTokenError)('boom')).toBe(false); - expect((0, pruneTokens_1.isDeadTokenError)({ code: 42 })).toBe(false); - }); -}); -describe('selectDeadTokens', () => { - it('maps rejected-dead results to their token by index', () => { - const tokens = ['A', 'B', 'C']; - const results = [ful(), rej(deadNotRegistered), rej(transientUnavailable)]; - expect((0, pruneTokens_1.selectDeadTokens)(tokens, results)).toEqual(['B']); - }); - it('ignores fulfilled sends and transient failures', () => { - const tokens = ['A', 'B']; - expect((0, pruneTokens_1.selectDeadTokens)(tokens, [ful(), rej(transientInternal)])).toEqual([]); - }); - it('dedupes when the same dead token appears twice', () => { - const tokens = ['A', 'A']; - expect((0, pruneTokens_1.selectDeadTokens)(tokens, [rej(deadNotRegistered), rej(deadInvalidToken)])).toEqual(['A']); - }); - it('returns empty when there are no failures', () => { - expect((0, pruneTokens_1.selectDeadTokens)(['A', 'B'], [ful(), ful()])).toEqual([]); - }); - it('never selects a token whose index is missing', () => { - // more results than tokens (defensive) — no crash, no phantom token - expect((0, pruneTokens_1.selectDeadTokens)(['A'], [rej(deadNotRegistered), rej(deadNotRegistered)])).toEqual(['A']); - }); -}); -//# sourceMappingURL=pruneTokens.test.js.map \ No newline at end of file diff --git a/functions/dist/notifications/pruneTokens.test.js.map b/functions/dist/notifications/pruneTokens.test.js.map deleted file mode 100644 index 132672b8..00000000 --- a/functions/dist/notifications/pruneTokens.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"pruneTokens.test.js","sourceRoot":"","sources":["../../src/notifications/pruneTokens.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/push.js b/functions/dist/notifications/push.js deleted file mode 100644 index 189af975..00000000 --- a/functions/dist/notifications/push.js +++ /dev/null @@ -1,82 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getUserTokens = getUserTokens; -exports.batchResponseToResults = batchResponseToResults; -exports.sendPushToUser = sendPushToUser; -const pruneTokens_1 = require("./pruneTokens"); -const log_1 = require("../log"); -/** - * One canonical FCM-token reader and one canonical sender for the whole codebase. - * - * Before this module, ~10 files re-implemented "read the user's tokens" and ~19 re-implemented - * "send to each token, log failures, prune the dead ones". This collapses both into a single, - * unit-tested unit. Callers keep what is genuinely theirs — gating (quiet hours / per-notif - * toggles) and any in-app `notification_queue` write; this module only touches FCM. - */ -/** - * Merge a user's FCM tokens from the legacy `fcmToken` field and the `fcmTokens` subcollection, - * de-duplicated, order-stable (legacy first). Pass `userData` if the user doc is already loaded to - * avoid a redundant read. - */ -async function getUserTokens(db, uid, userData) { - const tokens = []; - const data = userData !== null && userData !== void 0 ? userData : (await db.collection('users').doc(uid).get()).data(); - const legacy = data === null || data === void 0 ? void 0 : data.fcmToken; - if (typeof legacy === 'string' && legacy.length > 0) - tokens.push(legacy); - const snap = await db.collection('users').doc(uid).collection('fcmTokens').get(); - snap.docs.forEach((d) => { - var _a; - const t = (_a = d.data()) === null || _a === void 0 ? void 0 : _a.token; - if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) - tokens.push(t); - }); - return tokens; -} -/** - * Adapt a multicast `BatchResponse` to the `PromiseSettledResult[]` shape `pruneDeadTokens` - * consumes (index `i` still maps to `tokens[i]`). Exported for unit testing the dead-token path. - */ -function batchResponseToResults(batch) { - return batch.responses.map((r) => r.success - ? { status: 'fulfilled', value: r.messageId } - : { status: 'rejected', reason: r.error }); -} -/** - * Send one push to every FCM token a user has, in a single batched `sendEachForMulticast` - * round-trip, then prune any tokens FCM reports as permanently dead. Best-effort: never throws; - * returns a per-user summary. A whole-batch failure (auth/quota, not a per-token death) is logged - * and does not prune anything. - * - * `excludeTokens` skips specific devices — e.g. the restore self-alert must not go to the very - * device that requested the restore ("was this you?" to the asker is noise; their partner's and - * their other devices' copies are the signal). Excluding everything yields a clean zero no-op. - */ -async function sendPushToUser(db, messaging, uid, content, userData, excludeTokens) { - let tokens = await getUserTokens(db, uid, userData); - if (excludeTokens && excludeTokens.length > 0) { - tokens = tokens.filter((t) => !excludeTokens.includes(t)); - } - if (tokens.length === 0) - return { sent: 0, failed: 0, pruned: 0 }; - const message = Object.assign(Object.assign({ tokens, notification: content.notification }, (content.data ? { data: content.data } : {})), (content.android ? { android: content.android } : {})); - let batch; - try { - batch = await messaging.sendEachForMulticast(message); - } - catch (e) { - log_1.logger.warn(`[push] sendEachForMulticast failed for uid=${uid}`, { error: String(e) }); - return { sent: 0, failed: tokens.length, pruned: 0 }; - } - batch.responses.forEach((r, i) => { - var _a; - if (!r.success) { - log_1.logger.warn(`[push] FCM send failed uid=${uid} token=${(0, log_1.redactToken)(tokens[i])}`, { - code: (_a = r.error) === null || _a === void 0 ? void 0 : _a.code, - }); - } - }); - const pruned = await (0, pruneTokens_1.pruneDeadTokens)(db, uid, tokens, batchResponseToResults(batch)); - return { sent: batch.successCount, failed: batch.failureCount, pruned }; -} -//# sourceMappingURL=push.js.map \ No newline at end of file diff --git a/functions/dist/notifications/push.js.map b/functions/dist/notifications/push.js.map deleted file mode 100644 index b398823c..00000000 --- a/functions/dist/notifications/push.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/notifications/push.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/push.test.js b/functions/dist/notifications/push.test.js deleted file mode 100644 index d7437d0e..00000000 --- a/functions/dist/notifications/push.test.js +++ /dev/null @@ -1,166 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const push_1 = require("./push"); -const pruneTokens_1 = require("./pruneTokens"); -// ── Lightweight Firestore/Messaging fakes ──────────────────────────────────── -// Only the surface push.ts + pruneTokens.ts actually touch. -const deadError = { code: 'messaging/registration-token-not-registered' }; -const transientError = { code: 'messaging/server-unavailable' }; -function makeFakeDb(userData, subTokens) { - const deleted = []; - const commits = { count: 0 }; - const fcmDocs = subTokens.map((t, i) => ({ - ref: { __token: t, __id: `tok${i}` }, - data: () => ({ token: t }), - })); - const userRef = { - get: async () => ({ exists: true, data: () => userData }), - collection: (name) => { - if (name !== 'fcmTokens') - throw new Error(`unexpected subcollection ${name}`); - return { get: async () => ({ docs: fcmDocs }) }; - }, - }; - const db = { - collection: (name) => { - if (name !== 'users') - throw new Error(`unexpected collection ${name}`); - return { doc: () => userRef }; - }, - batch: () => ({ - delete: (ref) => deleted.push(ref.__token), - update: () => deleted.push('__legacy__'), - commit: async () => { - commits.count++; - }, - }), - }; - return { db, deleted, commits }; -} -function batch(responses) { - return { - responses, - successCount: responses.filter((r) => r.success).length, - failureCount: responses.filter((r) => !r.success).length, - }; -} -function makeFakeMessaging(response) { - return { sendEachForMulticast: async () => response }; -} -describe('getUserTokens', () => { - it('merges the legacy field with the subcollection, deduped and order-stable', async () => { - const { db } = makeFakeDb({ fcmToken: 'A' }, ['A', 'B']); - expect(await (0, push_1.getUserTokens)(db, 'u1')).toEqual(['A', 'B']); - }); - it('returns only subcollection tokens when there is no legacy field', async () => { - const { db } = makeFakeDb({}, ['B', 'C']); - expect(await (0, push_1.getUserTokens)(db, 'u1')).toEqual(['B', 'C']); - }); - it('returns empty when the user has no tokens', async () => { - const { db } = makeFakeDb({}, []); - expect(await (0, push_1.getUserTokens)(db, 'u1')).toEqual([]); - }); - it('uses provided userData without re-reading the user doc', async () => { - const { db } = makeFakeDb({}, ['B']); - // Make the user-doc read blow up; passing userData must avoid it entirely. - db.collection = (name) => { - if (name !== 'users') - throw new Error('unexpected'); - return { - doc: () => ({ - get: async () => { - throw new Error('should not read user doc'); - }, - collection: () => ({ get: async () => ({ docs: [{ data: () => ({ token: 'B' }) }] }) }), - }), - }; - }; - expect(await (0, push_1.getUserTokens)(db, 'u1', { fcmToken: 'A' })).toEqual(['A', 'B']); - }); -}); -describe('batchResponseToResults', () => { - it('maps a multicast BatchResponse to settled results that pick out dead tokens by index', () => { - const results = (0, push_1.batchResponseToResults)(batch([ - { success: true, messageId: 'm1' }, - { success: false, error: deadError }, - { success: false, error: transientError }, - ])); - expect(results.map((r) => r.status)).toEqual(['fulfilled', 'rejected', 'rejected']); - // End-to-end: only the permanently-dead token (index 1) is selected for pruning. - expect((0, pruneTokens_1.selectDeadTokens)(['A', 'B', 'C'], results)).toEqual(['B']); - }); -}); -describe('sendPushToUser', () => { - it('skips excluded tokens — the requesting device must not get its own "was this you?"', async () => { - const { db, deleted } = makeFakeDb({}, ['A', 'B', 'C']); - const sent = []; - const messaging = { - sendEachForMulticast: async (msg) => { - sent.push(msg.tokens); - return batch(msg.tokens.map((_) => ({ success: true, messageId: 'm' }))); - }, - }; - const result = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }, undefined, ['B']); - expect(sent).toEqual([['A', 'C']]); - expect(result).toEqual({ sent: 2, failed: 0, pruned: 0 }); - expect(deleted).toEqual([]); - }); - it('excluding every token is a clean zero no-op, never a throw', async () => { - const { db } = makeFakeDb({}, ['A']); - const messaging = { - sendEachForMulticast: async () => { - throw new Error('must not be called with zero tokens'); - }, - }; - const result = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }, undefined, ['A']); - expect(result).toEqual({ sent: 0, failed: 0, pruned: 0 }); - }); - it('an absent exclude list changes nothing', async () => { - const { db } = makeFakeDb({}, ['A', 'B']); - const sent = []; - const messaging = { - sendEachForMulticast: async (msg) => { - sent.push(msg.tokens); - return batch(msg.tokens.map(() => ({ success: true, messageId: 'm' }))); - }, - }; - await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }); - expect(sent).toEqual([['A', 'B']]); - }); - it('sends to every token and prunes exactly the permanently-dead ones', async () => { - const { db, deleted, commits } = makeFakeDb({ fcmToken: 'LEG' }, ['LEG', 'DEAD', 'GOOD']); - const messaging = makeFakeMessaging(batch([ - { success: true, messageId: 'm1' }, // LEG - { success: false, error: deadError }, // DEAD - { success: true, messageId: 'm2' }, // GOOD - ])); - const res = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }); - expect(res).toEqual({ sent: 2, failed: 1, pruned: 1 }); - expect(deleted).toEqual(['DEAD']); - expect(commits.count).toBe(1); - }); - it('does not prune on transient failures', async () => { - const { db, deleted } = makeFakeDb({}, ['T1']); - const messaging = makeFakeMessaging(batch([{ success: false, error: transientError }])); - const res = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }); - expect(res).toEqual({ sent: 0, failed: 1, pruned: 0 }); - expect(deleted).toEqual([]); - }); - it('no-ops cleanly when the user has no tokens', async () => { - const { db } = makeFakeDb({}, []); - const messaging = makeFakeMessaging(batch([])); - expect(await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } })).toEqual({ - sent: 0, - failed: 0, - pruned: 0, - }); - }); - it('treats a whole-batch send failure as non-fatal (no prune)', async () => { - const { db, deleted } = makeFakeDb({}, ['X']); - const messaging = { sendEachForMulticast: async () => Promise.reject(new Error('quota')) }; - const res = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }); - expect(res).toEqual({ sent: 0, failed: 1, pruned: 0 }); - expect(deleted).toEqual([]); - }); -}); -//# sourceMappingURL=push.test.js.map \ No newline at end of file diff --git a/functions/dist/notifications/push.test.js.map b/functions/dist/notifications/push.test.js.map deleted file mode 100644 index e6c1b688..00000000 --- a/functions/dist/notifications/push.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"push.test.js","sourceRoot":"","sources":["../../src/notifications/push.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/queueAndPush.js b/functions/dist/notifications/queueAndPush.js deleted file mode 100644 index 1f6ad263..00000000 --- a/functions/dist/notifications/queueAndPush.js +++ /dev/null @@ -1,65 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.queueAndPush = queueAndPush; -const admin = __importStar(require("firebase-admin")); -const quietHours_1 = require("./quietHours"); -const push_1 = require("./push"); -/** - * Write the durable in-app queue entry (always) and — unless quiet hours suppress it — push to every - * device. `bypassQuietHours` is for security signals (the "was this you?" self-alert class) that must - * not be silenced. - * - * Extracted from `backup/onRestoreRequested.ts` when the restore-request cleanup sweep needed the same - * behavior — one implementation, so queue-entry/quiet-hours/push semantics can't drift between callers. - */ -async function queueAndPush(db, uid, opts) { - const { type, title, body, coupleId, bypassQuietHours, channelId = 'partner_activity', excludeTokens } = opts; - await db.collection('users').doc(uid).collection('notification_queue').add({ - type, title, body, read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - const userDoc = await db.collection('users').doc(uid).get(); - if (!userDoc.exists) - return; - const userData = userDoc.data(); - if (!bypassQuietHours && (0, quietHours_1.recipientInQuietHours)(userData)) - return; - await (0, push_1.sendPushToUser)(db, admin.messaging(), uid, { - notification: { title, body }, - data: { type, couple_id: coupleId }, - android: { notification: { channelId } }, - }, userData, excludeTokens); -} -//# sourceMappingURL=queueAndPush.js.map \ No newline at end of file diff --git a/functions/dist/notifications/queueAndPush.js.map b/functions/dist/notifications/queueAndPush.js.map deleted file mode 100644 index 5a87dda8..00000000 --- a/functions/dist/notifications/queueAndPush.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"queueAndPush.js","sourceRoot":"","sources":["../../src/notifications/queueAndPush.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,oCAoCC;AAhDD,sDAAuC;AACvC,6CAAoD;AACpD,iCAAuC;AAEvC;;;;;;;GAOG;AACI,KAAK,UAAU,YAAY,CAChC,EAA6B,EAC7B,GAAW,EACX,IASC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,gBAAgB,EAAE,SAAS,GAAG,kBAAkB,EAAE,aAAa,EAAE,GAAG,IAAI,CAAA;IAC7G,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC;QACzE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KACxF,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAA;IAC3D,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAM;IAC3B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;IAC/B,IAAI,CAAC,gBAAgB,IAAI,IAAA,kCAAqB,EAAC,QAAQ,CAAC;QAAE,OAAM;IAEhE,MAAM,IAAA,qBAAc,EAClB,EAAE,EACF,KAAK,CAAC,SAAS,EAAE,EACjB,GAAG,EACH;QACE,YAAY,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;QAC7B,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE;QACnC,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,EAAE;KACzC,EACD,QAAQ,EACR,aAAa,CACd,CAAA;AACH,CAAC"} \ No newline at end of file diff --git a/functions/dist/notifications/quietHours.js b/functions/dist/notifications/quietHours.js deleted file mode 100644 index 17f479a5..00000000 --- a/functions/dist/notifications/quietHours.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.recipientInQuietHours = recipientInQuietHours; -/** - * Quiet-hours suppression for partner-action pushes. - * - * The Settings UI promises "10 PM – 8 AM, no notifications". The client stores the window in local - * DataStore AND mirrors it to the recipient's `users/{uid}` doc (quietHoursEnabled / *StartMinutes / - * *EndMinutes / timezone). Because a partner push carries a `notification` block, the OS shows it - * directly when the recipient app is backgrounded/killed — so the only place the promise can be kept - * is server-side, here, before the push is sent (M-001). - * - * FAIL-OPEN by design: if quiet hours is not explicitly enabled, or any field (window/timezone) is - * missing or malformed, we return `false` (do NOT suppress). A bug here can therefore only ever fall - * back to today's behavior (notification delivered) — it can never wrongly drop a notification, and - * existing installs keep delivering exactly as before until the client backfills the fields. - */ -function recipientInQuietHours(userData, now = new Date()) { - var _a, _b; - if (!userData || userData.quietHoursEnabled !== true) - return false; - const start = userData.quietHoursStartMinutes; - const end = userData.quietHoursEndMinutes; - const tz = userData.timezone; - if (typeof start !== 'number' || typeof end !== 'number' || typeof tz !== 'string' || !tz) { - return false; - } - let nowMinutes; - try { - const parts = new Intl.DateTimeFormat('en-US', { - timeZone: tz, - hour: '2-digit', - minute: '2-digit', - hour12: false, - }).formatToParts(now); - const hour = Number((_a = parts.find((p) => p.type === 'hour')) === null || _a === void 0 ? void 0 : _a.value) % 24; - const minute = Number((_b = parts.find((p) => p.type === 'minute')) === null || _b === void 0 ? void 0 : _b.value); - if (Number.isNaN(hour) || Number.isNaN(minute)) - return false; - nowMinutes = hour * 60 + minute; - } - catch (_c) { - // Unknown/invalid timezone id → fail open. - return false; - } - // Start == end means "no window" (nothing suppressed) — kept in lockstep with the client's - // QuietHoursManager.isInQuietHours so both decide identically. - if (start === end) - return false; - // Window may cross midnight (e.g. 22:00 → 08:00). - return start < end - ? nowMinutes >= start && nowMinutes <= end - : nowMinutes >= start || nowMinutes <= end; -} -//# sourceMappingURL=quietHours.js.map \ No newline at end of file diff --git a/functions/dist/notifications/quietHours.js.map b/functions/dist/notifications/quietHours.js.map deleted file mode 100644 index 576acaf2..00000000 --- a/functions/dist/notifications/quietHours.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"quietHours.js","sourceRoot":"","sources":["../../src/notifications/quietHours.ts"],"names":[],"mappings":";;AAcA,sDAsCC;AApDD;;;;;;;;;;;;;GAaG;AACH,SAAgB,qBAAqB,CACnC,QAAoD,EACpD,MAAY,IAAI,IAAI,EAAE;;IAEtB,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,iBAAiB,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAElE,MAAM,KAAK,GAAG,QAAQ,CAAC,sBAAsB,CAAA;IAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,oBAAoB,CAAA;IACzC,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAA;IAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,EAAE,EAAE,CAAC;QAC1F,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,UAAkB,CAAA;IACtB,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;YAC7C,QAAQ,EAAE,EAAE;YACZ,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,KAAK;SACd,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;QACrB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAA,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,0CAAE,KAAK,CAAC,GAAG,EAAE,CAAA;QACrE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAA,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,0CAAE,KAAK,CAAC,CAAA;QACpE,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,OAAO,KAAK,CAAA;QAC5D,UAAU,GAAG,IAAI,GAAG,EAAE,GAAG,MAAM,CAAA;IACjC,CAAC;IAAC,WAAM,CAAC;QACP,2CAA2C;QAC3C,OAAO,KAAK,CAAA;IACd,CAAC;IAED,2FAA2F;IAC3F,+DAA+D;IAC/D,IAAI,KAAK,KAAK,GAAG;QAAE,OAAO,KAAK,CAAA;IAE/B,kDAAkD;IAClD,OAAO,KAAK,GAAG,GAAG;QAChB,CAAC,CAAC,UAAU,IAAI,KAAK,IAAI,UAAU,IAAI,GAAG;QAC1C,CAAC,CAAC,UAAU,IAAI,KAAK,IAAI,UAAU,IAAI,GAAG,CAAA;AAC9C,CAAC"} \ No newline at end of file diff --git a/functions/dist/notifications/quietHours.test.js b/functions/dist/notifications/quietHours.test.js deleted file mode 100644 index b25edd2d..00000000 --- a/functions/dist/notifications/quietHours.test.js +++ /dev/null @@ -1,60 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const quietHours_1 = require("./quietHours"); -// recipientInQuietHours is a pure function (Intl + plain userData), so no firebase-admin mock is needed. -// All times below are expressed in UTC and the userData timezone is 'UTC' for determinism. Minutes: -// 22:00 = 1320, 08:00 = 480, 10:00 = 600, 12:00 = 720. -const at = (iso) => new Date(`2026-01-15T${iso}:00Z`); -describe('recipientInQuietHours', () => { - describe('fail-open guards', () => { - it('returns false when quiet hours is not explicitly enabled', () => { - expect((0, quietHours_1.recipientInQuietHours)(undefined, at('23:30'))).toBe(false); - expect((0, quietHours_1.recipientInQuietHours)({}, at('23:30'))).toBe(false); - expect((0, quietHours_1.recipientInQuietHours)({ quietHoursStartMinutes: 1320, quietHoursEndMinutes: 480, timezone: 'UTC' }, at('23:30'))).toBe(false); - }); - it('returns false when window or timezone fields are missing/malformed', () => { - const base = { quietHoursEnabled: true, timezone: 'UTC' }; - expect((0, quietHours_1.recipientInQuietHours)(Object.assign(Object.assign({}, base), { quietHoursEndMinutes: 480 }), at('23:30'))).toBe(false); - expect((0, quietHours_1.recipientInQuietHours)(Object.assign(Object.assign({}, base), { quietHoursStartMinutes: 1320 }), at('23:30'))).toBe(false); - expect((0, quietHours_1.recipientInQuietHours)({ quietHoursEnabled: true, quietHoursStartMinutes: 1320, quietHoursEndMinutes: 480 }, at('23:30'))).toBe(false); - }); - it('returns false for an unknown timezone id', () => { - expect((0, quietHours_1.recipientInQuietHours)({ quietHoursEnabled: true, quietHoursStartMinutes: 1320, quietHoursEndMinutes: 480, timezone: 'Not/AZone' }, at('23:30'))).toBe(false); - }); - }); - describe('overnight window (22:00 → 08:00)', () => { - const qh = { quietHoursEnabled: true, quietHoursStartMinutes: 1320, quietHoursEndMinutes: 480, timezone: 'UTC' }; - it('suppresses inside the window, including both boundaries', () => { - expect((0, quietHours_1.recipientInQuietHours)(qh, at('23:30'))).toBe(true); - expect((0, quietHours_1.recipientInQuietHours)(qh, at('03:00'))).toBe(true); - expect((0, quietHours_1.recipientInQuietHours)(qh, at('22:00'))).toBe(true); // start inclusive - expect((0, quietHours_1.recipientInQuietHours)(qh, at('08:00'))).toBe(true); // end inclusive - }); - it('does not suppress outside the window', () => { - expect((0, quietHours_1.recipientInQuietHours)(qh, at('08:01'))).toBe(false); - expect((0, quietHours_1.recipientInQuietHours)(qh, at('12:00'))).toBe(false); - expect((0, quietHours_1.recipientInQuietHours)(qh, at('21:59'))).toBe(false); - }); - }); - describe('same-day window (10:00 → 12:00)', () => { - const qh = { quietHoursEnabled: true, quietHoursStartMinutes: 600, quietHoursEndMinutes: 720, timezone: 'UTC' }; - it('suppresses inside the window, including both boundaries', () => { - expect((0, quietHours_1.recipientInQuietHours)(qh, at('10:30'))).toBe(true); - expect((0, quietHours_1.recipientInQuietHours)(qh, at('10:00'))).toBe(true); - expect((0, quietHours_1.recipientInQuietHours)(qh, at('12:00'))).toBe(true); - }); - it('does not suppress outside the window', () => { - expect((0, quietHours_1.recipientInQuietHours)(qh, at('09:59'))).toBe(false); - expect((0, quietHours_1.recipientInQuietHours)(qh, at('12:01'))).toBe(false); - }); - }); - describe('start == end is "no window"', () => { - it('never suppresses (matches the client QuietHoursManager)', () => { - const qh = { quietHoursEnabled: true, quietHoursStartMinutes: 1320, quietHoursEndMinutes: 1320, timezone: 'UTC' }; - expect((0, quietHours_1.recipientInQuietHours)(qh, at('22:00'))).toBe(false); - expect((0, quietHours_1.recipientInQuietHours)(qh, at('03:00'))).toBe(false); - expect((0, quietHours_1.recipientInQuietHours)(qh, at('12:00'))).toBe(false); - }); - }); -}); -//# sourceMappingURL=quietHours.test.js.map \ No newline at end of file diff --git a/functions/dist/notifications/quietHours.test.js.map b/functions/dist/notifications/quietHours.test.js.map deleted file mode 100644 index 7aeab062..00000000 --- a/functions/dist/notifications/quietHours.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"quietHours.test.js","sourceRoot":"","sources":["../../src/notifications/quietHours.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/reengagement.js b/functions/dist/notifications/reengagement.js deleted file mode 100644 index 3b3673a1..00000000 --- a/functions/dist/notifications/reengagement.js +++ /dev/null @@ -1,138 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendReengagementReminder = void 0; -const admin = __importStar(require("firebase-admin")); -const scheduler_1 = require("firebase-functions/v2/scheduler"); -const quietHours_1 = require("./quietHours"); -const push_1 = require("./push"); -const log_1 = require("../log"); -const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000; -const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000; -const REENGAGEMENT_COOLDOWN_MS = 3 * 24 * 60 * 60 * 1000; -/** - * Re-engagement nudge for couples who went quiet. - * - * Schedule: 12:00 PM America/Chicago daily. - * - * Targets couples whose lastAnsweredAt is between 3 and 10 days ago — - * recently lapsed but not completely inactive. Respects a 3-day cooldown - * via reengagementSentAt to avoid spamming. - * - * Requires a Firestore composite index on couples: lastAnsweredAt ASC. - */ -exports.sendReengagementReminder = (0, scheduler_1.onSchedule)({ schedule: '0 12 * * *', timeZone: 'America/Chicago', timeoutSeconds: 180 }, async () => { - const db = admin.firestore(); - const messaging = admin.messaging(); - const now = Date.now(); - const threeDaysAgo = admin.firestore.Timestamp.fromMillis(now - THREE_DAYS_MS); - const tenDaysAgo = admin.firestore.Timestamp.fromMillis(now - TEN_DAYS_MS); - const snap = await db - .collection('couples') - .where('lastAnsweredAt', '>', tenDaysAgo) - .where('lastAnsweredAt', '<', threeDaysAgo) - .limit(200) - .get(); - let notified = 0; - let skipped = 0; - // Per-couple failures are isolated so one bad couple can't abort the whole run. - await Promise.allSettled(snap.docs.map(async (coupleDoc) => { - var _a; - const data = coupleDoc.data(); - const coupleId = coupleDoc.id; - const userIds = ((_a = data.userIds) !== null && _a !== void 0 ? _a : []); - if (userIds.length === 0) { - skipped++; - return; - } - // Respect cooldown — don't re-send within 3 days. - const sentAt = data.reengagementSentAt; - if (sentAt && now - sentAt.toMillis() < REENGAGEMENT_COOLDOWN_MS) { - skipped++; - return; - } - // Claim atomically so parallel runs don't double-send. - const claimed = await db.runTransaction(async (tx) => { - var _a; - const fresh = await tx.get(coupleDoc.ref); - const freshSentAt = (_a = fresh.data()) === null || _a === void 0 ? void 0 : _a.reengagementSentAt; - if (freshSentAt && now - freshSentAt.toMillis() < REENGAGEMENT_COOLDOWN_MS) - return false; - tx.update(coupleDoc.ref, { - reengagementSentAt: admin.firestore.FieldValue.serverTimestamp(), - }); - return true; - }); - if (!claimed) { - skipped++; - return; - } - await Promise.allSettled(userIds.map((uid) => sendNudge(db, messaging, uid, coupleId))); - notified += userIds.length; - })); - log_1.logger.log(`[sendReengagementReminder] scanned ${snap.size}; notified ${notified}; skipped ${skipped}`); -}); -async function sendNudge(db, messaging, userId, coupleId) { - const userDoc = await db.collection('users').doc(userId).get(); - const userData = userDoc.data(); - // Re-engagement is a promotional nudge — respect the opt-out (default on) and quiet hours. - if ((userData === null || userData === void 0 ? void 0 : userData.notifPromotional) === false) { - log_1.logger.log(`[sendReengagementReminder] skip ${userId} — promotional off`); - return; - } - if ((0, quietHours_1.recipientInQuietHours)(userData)) { - log_1.logger.log(`[sendReengagementReminder] skip ${userId} — quiet hours`); - return; - } - await db.collection('users').doc(userId).collection('notification_queue').add({ - type: 'reengagement', - title: "It's been a while.", - body: "Tonight's question is a good reason to reconnect.", - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - await (0, push_1.sendPushToUser)(db, messaging, userId, { - notification: { - title: "It's been a while.", - body: "Tonight's question is a good reason to reconnect.", - }, - android: { notification: { channelId: 'reminders' } }, // E-OBS - data: { - type: 'reengagement', - couple_id: coupleId, - }, - }, userData); -} -//# sourceMappingURL=reengagement.js.map \ No newline at end of file diff --git a/functions/dist/notifications/reengagement.js.map b/functions/dist/notifications/reengagement.js.map deleted file mode 100644 index b07312f1..00000000 --- a/functions/dist/notifications/reengagement.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"reengagement.js","sourceRoot":"","sources":["../../src/notifications/reengagement.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/sendGentleReminderCallable.js b/functions/dist/notifications/sendGentleReminderCallable.js deleted file mode 100644 index 0a80694d..00000000 --- a/functions/dist/notifications/sendGentleReminderCallable.js +++ /dev/null @@ -1,173 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendGentleReminderCallable = void 0; -const admin = __importStar(require("firebase-admin")); -const https_1 = require("firebase-functions/v2/https"); -const push_1 = require("./push"); -const log_1 = require("../log"); -const GENTLE_REMINDER_MAX_PER_HOUR = 5; -const GENTLE_REMINDER_WINDOW_MS = 60 * 60 * 1000; // 1 hour -/** - * Sends a gentle nudge from one partner to the other when the caller has - * already answered today's question but the partner hasn't. - * - * Rate limits: - * - Per-user: max 5 gentle reminders per rolling hour. Guarded by a server-side - * transaction on `rate_limits/{uid}_gentle_reminder` so malicious clients - * cannot bypass it by calling the callable in a loop. The Android-side - * NotificationRateLimiter remains for UX but is not the authoritative guard. - * - Per-couple: one reminder per couple per calendar day (UTC). The lock is - * stored in couples/{coupleId}/gentle_reminders/{date} so it survives - * function restarts and is visible to both partners. - * - * The notification is both an FCM push (for the system tray) and an entry in - * the partner's notification_queue (for in-app display). - */ -exports.sendGentleReminderCallable = (0, https_1.onCall)(async (request) => { - var _a, _b, _c, _d; - const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); - } - if (!request.app) { - throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); - } - const db = admin.firestore(); - // ── 1. Resolve couple + partner ────────────────────────────────────────── - const userDoc = await db.collection('users').doc(callerId).get(); - const coupleId = (_b = userDoc.data()) === null || _b === void 0 ? void 0 : _b.coupleId; - if (!coupleId) { - throw new https_1.HttpsError('failed-precondition', 'Not in a couple.'); - } - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) { - throw new https_1.HttpsError('not-found', 'Couple not found.'); - } - const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []); - const partnerId = userIds.find((id) => id !== callerId); - if (!partnerId) { - throw new https_1.HttpsError('failed-precondition', 'No partner found.'); - } - try { - // ── 2. Server-side per-user throttle: 5 per hour (rolling window) ──────── - const now = admin.firestore.Timestamp.now(); - const rateLimitRef = db.collection('rate_limits').doc(`${callerId}_gentle_reminder`); - const throttleResult = await db.runTransaction(async (tx) => { - const snap = await tx.get(rateLimitRef); - const data = snap.data(); - let windowStart; - let count; - if (!snap.exists || !data) { - windowStart = now; - count = 0; - } - else { - windowStart = data.windowStart; - count = (typeof data.count === 'number' ? data.count : 0); - const elapsedMs = now.toMillis() - windowStart.toMillis(); - if (elapsedMs >= GENTLE_REMINDER_WINDOW_MS) { - // Rolling window has expired; start a fresh one. - windowStart = now; - count = 0; - } - } - if (count >= GENTLE_REMINDER_MAX_PER_HOUR) { - const retryAfterMs = GENTLE_REMINDER_WINDOW_MS - (now.toMillis() - windowStart.toMillis()); - const retryAfterMinutes = Math.max(1, Math.ceil(retryAfterMs / 60000)); - return { allowed: false, retryAfterMinutes }; - } - tx.set(rateLimitRef, { - count: count + 1, - windowStart, - updatedAt: now, - }, { merge: true }); - return { allowed: true }; - }); - if (!throttleResult.allowed) { - throw new https_1.HttpsError('resource-exhausted', `Too many gentle reminders. Try again in ${throttleResult.retryAfterMinutes} minutes.`); - } - // ── 3. Rate limit: one per couple per day ──────────────────────────────── - const today = new Date().toISOString().slice(0, 10); // e.g. "2026-06-19" - const lockRef = db - .collection('couples') - .doc(coupleId) - .collection('gentle_reminders') - .doc(today); - const existingLock = await lockRef.get(); - if (existingLock.exists) { - return { sent: false, reason: 'already_sent_today' }; - } - // ── 4. Write in-app notification record ────────────────────────────────── - await db - .collection('users') - .doc(partnerId) - .collection('notification_queue') - .add({ - type: 'gentle_reminder', - title: 'Your partner is thinking about you.', - body: "They left tonight's question open. Answer when you're ready.", - read: false, - sent: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - // ── 5. Claim the daily rate-limit lock ─────────────────────────────────── - await lockRef.set({ - sentBy: callerId, - sentAt: admin.firestore.FieldValue.serverTimestamp(), - }); - // ── 6. Send FCM push ───────────────────────────────────────────────────── - await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { - title: 'Your partner is thinking about you.', - body: "They left tonight's question open. Answer when you're ready.", - }, - android: { notification: { channelId: 'partner_activity' } }, // E-OBS - data: { - type: 'gentle_reminder', - couple_id: coupleId, - }, - }); - log_1.logger.log(`[sendGentleReminderCallable] reminder sent from ${callerId} to ${partnerId} in couple ${coupleId}`); - return { sent: true }; - } - catch (e) { - if (e instanceof https_1.HttpsError) - throw e; - log_1.logger.error('[sendGentleReminderCallable] failed', { error: String(e) }); - throw new https_1.HttpsError('internal', 'Failed to send reminder. Please try again.'); - } -}); -//# sourceMappingURL=sendGentleReminderCallable.js.map \ No newline at end of file diff --git a/functions/dist/notifications/sendGentleReminderCallable.js.map b/functions/dist/notifications/sendGentleReminderCallable.js.map deleted file mode 100644 index 3ea13369..00000000 --- a/functions/dist/notifications/sendGentleReminderCallable.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"sendGentleReminderCallable.js","sourceRoot":"","sources":["../../src/notifications/sendGentleReminderCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/sendThinkingOfYouCallable.js b/functions/dist/notifications/sendThinkingOfYouCallable.js deleted file mode 100644 index 03152748..00000000 --- a/functions/dist/notifications/sendThinkingOfYouCallable.js +++ /dev/null @@ -1,137 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendThinkingOfYouCallable = void 0; -const admin = __importStar(require("firebase-admin")); -const https_1 = require("firebase-functions/v2/https"); -const quietHours_1 = require("./quietHours"); -const push_1 = require("./push"); -const log_1 = require("../log"); -const THINKING_OF_YOU_MAX_PER_DAY = 10; -const THINKING_OF_YOU_WINDOW_MS = 24 * 60 * 60 * 1000; // rolling 24h -/** - * "Thinking of you 💜" — a one-tap affectionate nudge from the partner sheet. - * - * Partner-initiated (like the gentle reminder), so it is guarded by a rate limit + quiet hours rather - * than a per-type opt-out: - * - Per-user rolling 24h cap (transaction on `rate_limits/{uid}_thinking_of_you`) so it can't be looped. - * - Quiet hours: during the recipient's window the FCM push is suppressed, but the in-app record is - * still written so the love is waiting when they wake. (Improvement over the gentle reminder.) - * - * Writes a partner-safe `notification_queue` entry (in-app + the "Together" feed) and an FCM push. - */ -exports.sendThinkingOfYouCallable = (0, https_1.onCall)(async (request) => { - var _a, _b, _c, _d; - const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); - } - if (!request.app) { - throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); - } - const db = admin.firestore(); - // ── Resolve couple + partner ───────────────────────────────────────────── - const userDoc = await db.collection('users').doc(callerId).get(); - const coupleId = (_b = userDoc.data()) === null || _b === void 0 ? void 0 : _b.coupleId; - if (!coupleId) { - throw new https_1.HttpsError('failed-precondition', 'Not in a couple.'); - } - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) { - throw new https_1.HttpsError('not-found', 'Couple not found.'); - } - const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []); - const partnerId = userIds.find((id) => id !== callerId); - if (!partnerId) { - throw new https_1.HttpsError('failed-precondition', 'No partner found.'); - } - try { - // ── Per-user rolling rate limit (transactional) ────────────────────────── - const now = admin.firestore.Timestamp.now(); - const rateLimitRef = db.collection('rate_limits').doc(`${callerId}_thinking_of_you`); - const throttle = await db.runTransaction(async (tx) => { - const snap = await tx.get(rateLimitRef); - const data = snap.data(); - let windowStart = now; - let count = 0; - if (snap.exists && data) { - windowStart = data.windowStart; - count = typeof data.count === 'number' ? data.count : 0; - if (now.toMillis() - windowStart.toMillis() >= THINKING_OF_YOU_WINDOW_MS) { - windowStart = now; - count = 0; - } - } - if (count >= THINKING_OF_YOU_MAX_PER_DAY) { - return { allowed: false }; - } - tx.set(rateLimitRef, { count: count + 1, windowStart, updatedAt: now }, { merge: true }); - return { allowed: true }; - }); - if (!throttle.allowed) { - throw new https_1.HttpsError('resource-exhausted', "You've sent a few already — give it a moment."); - } - // ── In-app record (always — shows in-app + the Together feed) ──────────── - await db.collection('users').doc(partnerId).collection('notification_queue').add({ - type: 'thinking_of_you', - title: 'Your partner is thinking of you 💜', - body: 'Tap to send one back.', - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - // ── Quiet hours: keep the in-app record, suppress the disruptive push ───── - const partnerDoc = await db.collection('users').doc(partnerId).get(); - const partnerData = partnerDoc.data(); - if ((0, quietHours_1.recipientInQuietHours)(partnerData)) { - log_1.logger.log(`[sendThinkingOfYouCallable] ${partnerId} in quiet hours — push suppressed (in-app kept)`); - return { sent: true }; - } - // ── FCM push ───────────────────────────────────────────────────────────── - await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { title: 'Your partner is thinking of you 💜', body: 'Tap to send one back.' }, - android: { notification: { channelId: 'partner_activity' } }, // E-OBS - data: { type: 'thinking_of_you', couple_id: coupleId }, - }, partnerData); - log_1.logger.log(`[sendThinkingOfYouCallable] sent from ${callerId} to ${partnerId} in couple ${coupleId}`); - return { sent: true }; - } - catch (e) { - if (e instanceof https_1.HttpsError) - throw e; - log_1.logger.error('[sendThinkingOfYouCallable] failed', { error: String(e) }); - throw new https_1.HttpsError('internal', 'Failed to send. Please try again.'); - } -}); -//# sourceMappingURL=sendThinkingOfYouCallable.js.map \ No newline at end of file diff --git a/functions/dist/notifications/sendThinkingOfYouCallable.js.map b/functions/dist/notifications/sendThinkingOfYouCallable.js.map deleted file mode 100644 index 07c273f8..00000000 --- a/functions/dist/notifications/sendThinkingOfYouCallable.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"sendThinkingOfYouCallable.js","sourceRoot":"","sources":["../../src/notifications/sendThinkingOfYouCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/streakReminder.js b/functions/dist/notifications/streakReminder.js deleted file mode 100644 index 5c2486dc..00000000 --- a/functions/dist/notifications/streakReminder.js +++ /dev/null @@ -1,136 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendStreakReminder = void 0; -const admin = __importStar(require("firebase-admin")); -const scheduler_1 = require("firebase-functions/v2/scheduler"); -const quietHours_1 = require("./quietHours"); -const push_1 = require("./push"); -const time_1 = require("./time"); -const log_1 = require("../log"); -/** - * Streak reminder — an evening "don't lose your streak" nudge. - * - * Schedule: 7 PM America/Chicago. For couples with an active streak (`streakCount > 0`) who have NOT - * recorded a shared action today, nudge each partner to do something together before the day ends. - * - * Gating: per-user `notifStreakReminder` toggle (default on) + quiet hours. Deduped per local day via a - * transactional `couples/{id}/streak_reminders/{dateKey}` marker (scheduled jobs can fire more than once). - * - * Known limitation: the "today" boundary uses America/Chicago (the cron's timezone), not each couple's - * local day — true per-timezone firing is a future refinement; quiet-hours suppression keeps a mistimed - * fire from landing at a bad local hour. (Streak day-boundary note in the plan.) - */ -exports.sendStreakReminder = (0, scheduler_1.onSchedule)({ schedule: '0 19 * * *', timeZone: 'America/Chicago', timeoutSeconds: 180 }, async () => { - const db = admin.firestore(); - const messaging = admin.messaging(); - const todayKey = (0, time_1.chicagoDateKey)(new Date()); - const coupleSnap = await db.collection('couples').where('streakCount', '>', 0).get(); - let notified = 0; - let skipped = 0; - const results = await Promise.allSettled(coupleSnap.docs.map(async (coupleDoc) => { - var _a, _b; - const couple = coupleDoc.data(); - const streak = ((_a = couple.streakCount) !== null && _a !== void 0 ? _a : 0); - if (streak <= 0) { - skipped++; - return; - } - // Already did a shared action today → the streak is safe, no nudge needed. - const lastMs = (0, time_1.toMillis)(couple.lastAnsweredAt); - if (lastMs > 0 && (0, time_1.chicagoDateKey)(new Date(lastMs)) === todayKey) { - skipped++; - return; - } - const userIds = ((_b = couple.userIds) !== null && _b !== void 0 ? _b : []); - if (userIds.length === 0) { - skipped++; - return; - } - // Per-day dedupe (transactional create-if-absent) — idempotent across re-runs. - const markerRef = coupleDoc.ref.collection('streak_reminders').doc(todayKey); - try { - await db.runTransaction(async (tx) => { - const fresh = await tx.get(markerRef); - if (fresh.exists) - throw new Error('already_sent'); - tx.set(markerRef, { sentAt: admin.firestore.FieldValue.serverTimestamp(), streak }); - }); - } - catch (_c) { - skipped++; - return; - } - await Promise.allSettled(userIds.map((userId) => sendStreakNudge(db, messaging, userId, coupleDoc.id, streak, todayKey))); - notified += userIds.length; - })); - results.forEach((r) => { - if (r.status === 'rejected') - log_1.logger.warn('[sendStreakReminder] couple failed', { error: String(r.reason) }); - }); - log_1.logger.log(`[sendStreakReminder] scanned ${coupleSnap.size} streak couples; notified ${notified}; skipped ${skipped}`); -}); -async function sendStreakNudge(db, messaging, userId, coupleId, streak, dateKey) { - const userDoc = await db.collection('users').doc(userId).get(); - const userData = userDoc.data(); - // Respect the user's Streak Reminder toggle (default on) and quiet hours. - if ((userData === null || userData === void 0 ? void 0 : userData.notifStreakReminder) === false) { - log_1.logger.log(`[sendStreakReminder] skip ${userId} — streak reminder off`); - return; - } - if ((0, quietHours_1.recipientInQuietHours)(userData)) { - log_1.logger.log(`[sendStreakReminder] skip ${userId} — quiet hours`); - return; - } - const title = `🔥 Keep your ${streak}-day streak`; - const body = "Answer tonight's question together before the day ends."; - await db - .collection('users') - .doc(userId) - .collection('notification_queue') - .add({ - type: 'streak', - title, - body, - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - await (0, push_1.sendPushToUser)(db, messaging, userId, { - notification: { title, body }, - android: { notification: { channelId: 'reminders' } }, - data: { type: 'streak', couple_id: coupleId, reminder_date: dateKey }, - }, userData); -} -//# sourceMappingURL=streakReminder.js.map \ No newline at end of file diff --git a/functions/dist/notifications/streakReminder.js.map b/functions/dist/notifications/streakReminder.js.map deleted file mode 100644 index 78716837..00000000 --- a/functions/dist/notifications/streakReminder.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"streakReminder.js","sourceRoot":"","sources":["../../src/notifications/streakReminder.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/notifications/time.js b/functions/dist/notifications/time.js deleted file mode 100644 index 5f259640..00000000 --- a/functions/dist/notifications/time.js +++ /dev/null @@ -1,65 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.chicagoDateKey = chicagoDateKey; -exports.toMillis = toMillis; -const admin = __importStar(require("firebase-admin")); -/** - * Shared date/time helpers for the scheduled jobs, consolidated from per-file copies - * (streakReminder's chicagoDateKey/toMillis, scheduledOutcomesReminder's millisFromFirestoreValue). - * The CST assignment helpers in questions/assignDailyQuestion.ts are a separate, unit-tested set and - * stay there. - */ -/** YYYY-MM-DD for the given instant in America/Chicago (DST-safe; en-CA gives ISO date order). */ -function chicagoDateKey(d) { - return new Intl.DateTimeFormat('en-CA', { - timeZone: 'America/Chicago', - year: 'numeric', - month: '2-digit', - day: '2-digit', - }).format(d); -} -/** Best-effort milliseconds from a Firestore Timestamp, a raw number, or any `{ toMillis() }` value. */ -function toMillis(value) { - if (typeof value === 'number') - return value; - if (value instanceof admin.firestore.Timestamp) - return value.toMillis(); - if (value != null && typeof value.toMillis === 'function') { - return value.toMillis(); - } - return 0; -} -//# sourceMappingURL=time.js.map \ No newline at end of file diff --git a/functions/dist/notifications/time.js.map b/functions/dist/notifications/time.js.map deleted file mode 100644 index e94c3e87..00000000 --- a/functions/dist/notifications/time.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"time.js","sourceRoot":"","sources":["../../src/notifications/time.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA,wCAOC;AAGD,4BAOC;AA3BD,sDAAuC;AAEvC;;;;;GAKG;AAEH,kGAAkG;AAClG,SAAgB,cAAc,CAAC,CAAO;IACpC,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;QACtC,QAAQ,EAAE,iBAAiB;QAC3B,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,SAAS;QAChB,GAAG,EAAE,SAAS;KACf,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;AACd,CAAC;AAED,wGAAwG;AACxG,SAAgB,QAAQ,CAAC,KAAc;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,KAAK,YAAY,KAAK,CAAC,SAAS,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAA;IACvE,IAAI,KAAK,IAAI,IAAI,IAAI,OAAQ,KAAgC,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;QACtF,OAAQ,KAAoC,CAAC,QAAQ,EAAE,CAAA;IACzD,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC"} \ No newline at end of file diff --git a/functions/dist/options.js b/functions/dist/options.js deleted file mode 100644 index 398cdf6a..00000000 --- a/functions/dist/options.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.REGION = void 0; -const v2_1 = require("firebase-functions/v2"); -/** - * Global 2nd-gen options for every v2 function in this codebase. - * - * region: pinned to us-central1 — the Android client calls FirebaseFunctions.getInstance() with - * no region override (di/SecurityModule.kt, data/remote/FirestoreAnswerDataSource.kt), so the - * callables MUST live in us-central1 or every install breaks. Do not change without also - * pinning the client. - * maxInstances: a runaway-bill guardrail AND a Cloud Run quota constraint. Each 2nd-gen function - * is a Cloud Run service, and the project's regional "total CPU allocation" quota is charged as - * the SUM of (maxInstances x vCPU) across every function. With ~34 v2 functions and a new - * project's default quota (~560 vCPU in us-central1), this must stay low or deploys fail with - * "Quota exceeded for total allowable CPU per project per region". 5 x 34 = 170, well under. - * For production: request a Cloud Run CPU quota increase (console → IAM & Admin → Quotas) and - * raise this. ~80 concurrent requests per instance means 5 instances still serves ~400 in flight. - * - * cpu 'gcf_gen1' + concurrency 1: v2 defaults every instance to a FULL vCPU (required for the - * default concurrency of 80). On this project's default Cloud Run CPU quota that made deploys - * fail their container healthchecks ("Quota exceeded for total allowable CPU"). gcf_gen1 uses - * the gen1 fractional tiers (256MiB → 1/6 vCPU, 512MiB → 1/3) — a 6x smaller CPU footprint, - * which is exactly how these workloads ran on gen1 until now. Concurrency must be 1 when - * cpu < 1; at dev scale that costs nothing. AT LAUNCH: request the Cloud Run CPU quota - * increase, then delete these two lines to restore 1 vCPU + concurrency 80. - * - * This module is imported FIRST in index.ts so these options are set before any v2 function is - * defined. It has no effect on the one v1 holdout (onUserDelete), which keeps its own defaults. - */ -(0, v2_1.setGlobalOptions)({ region: 'us-central1', maxInstances: 5, cpu: 'gcf_gen1', concurrency: 1 }); -/** Region constant for any per-function override that ever needs to pin it explicitly. */ -exports.REGION = 'us-central1'; -//# sourceMappingURL=options.js.map \ No newline at end of file diff --git a/functions/dist/options.js.map b/functions/dist/options.js.map deleted file mode 100644 index 8af2ffa9..00000000 --- a/functions/dist/options.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":";;;AAAA,8CAAwD;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,IAAA,qBAAgB,EAAC,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAAA;AAE7F,0FAA0F;AAC7E,QAAA,MAAM,GAAG,aAAa,CAAA"} \ No newline at end of file diff --git a/functions/dist/questions/assignDailyQuestion.js b/functions/dist/questions/assignDailyQuestion.js deleted file mode 100644 index f663c356..00000000 --- a/functions/dist/questions/assignDailyQuestion.js +++ /dev/null @@ -1,310 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.assignDailyQuestionCallable = exports.assignDailyQuestion = void 0; -exports.dayOfYearUtc = dayOfYearUtc; -exports.isWildcardDay = isWildcardDay; -exports.formatCstDate = formatCstDate; -exports.parseCstDate = parseCstDate; -exports.timestampAt6PmCst = timestampAt6PmCst; -const admin = __importStar(require("firebase-admin")); -const scheduler_1 = require("firebase-functions/v2/scheduler"); -const https_1 = require("firebase-functions/v2/https"); -const log_1 = require("../log"); -/** - * Scheduled function that assigns one daily question per couple every day. - * - * Schedule: 6:00 PM CST (America/Chicago) == 23:00 UTC. - * Document path: couples/{coupleId}/daily_question/{date} - * - questionId: string - * - date: string (YYYY-MM-DD) - * - assignedAt: Timestamp - * - expiresAt: Timestamp (next day at 6 PM CST) - * - * Admin SDK bypasses Firestore rules; the `daily_question` doc is server-write - * only (`allow write: if false` in rules). - */ -exports.assignDailyQuestion = (0, scheduler_1.onSchedule)({ schedule: '0 23 * * *', timeZone: 'America/Chicago', memory: '512MiB', timeoutSeconds: 300 }, async () => { - const db = admin.firestore(); - const today = cstDateString(); - const nextDay = nextCstDateString(); - const questionId = await pickDailyQuestionId(today); - if (!questionId) { - log_1.logger.error('[assignDailyQuestion] questions pool not seeded — skipping assignment'); - return; - } - const assignedAt = admin.firestore.FieldValue.serverTimestamp(); - const expiresAt = timestampAt6PmCst(nextDay); - // Paginate the couple scan so we never load every couple into memory at once, and each write - // burst is bounded to a page (instead of an unbounded parallel fan-out). Ordering by __name__ - // needs no custom index. create() is idempotent — ALREADY_EXISTS (code 6) is the no-op path. - const PAGE_SIZE = 300; - let scanned = 0; - let lastDoc; - for (;;) { - let query = db.collection('couples').orderBy('__name__').limit(PAGE_SIZE); - if (lastDoc) - query = query.startAfter(lastDoc); - const page = await query.get(); - if (page.empty) - break; - scanned += page.size; - await Promise.allSettled(page.docs.map(async (coupleDoc) => { - var _a; - const docRef = db - .collection('couples') - .doc(coupleDoc.id) - .collection('daily_question') - .doc(today); - try { - await docRef.create({ questionId, date: today, assignedAt, expiresAt }); - } - catch (err) { - if ((err === null || err === void 0 ? void 0 : err.code) === 6 || ((_a = err === null || err === void 0 ? void 0 : err.message) === null || _a === void 0 ? void 0 : _a.includes('ALREADY_EXISTS'))) - return; // idempotent - log_1.logger.error(`[assignDailyQuestion] failed for ${coupleDoc.id}`, { error: String(err) }); - } - })); - lastDoc = page.docs[page.docs.length - 1]; - if (page.size < PAGE_SIZE) - break; - } - log_1.logger.log(`[assignDailyQuestion] assigned ${questionId} to ${scanned} couples for ${today}`); -}); -/** - * Callable function for immediate daily question assignment. - * - * Useful when a couple is newly created and should not wait for the next - * scheduled run, or for manual admin/testing triggers. - * - * Body: { coupleId: string, date?: string } - */ -exports.assignDailyQuestionCallable = (0, https_1.onCall)(async (request) => { - var _a, _b, _c, _d; - const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new https_1.HttpsError('unauthenticated', 'Caller must be authenticated.'); - } - if (!request.app) { - throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); - } - const data = request.data; - const coupleId = data === null || data === void 0 ? void 0 : data.coupleId; - if (!coupleId || typeof coupleId !== 'string') { - throw new https_1.HttpsError('invalid-argument', 'coupleId is required.'); - } - const db = admin.firestore(); - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) { - throw new https_1.HttpsError('not-found', 'Couple not found.'); - } - // Caller must be a member of the couple. - const userIds = ((_c = (_b = coupleDoc.data()) === null || _b === void 0 ? void 0 : _b.userIds) !== null && _c !== void 0 ? _c : []); - if (!userIds.includes(callerId)) { - throw new https_1.HttpsError('permission-denied', 'Caller is not a couple member.'); - } - // Security review Batch 2: constrain the client-supplied date. Only today's CST date - // may be assigned on demand — this blocks creating arbitrary past/future daily_question - // docs and, combined with create()'s ALREADY_EXISTS guard, caps it to one per day - // (effective rate limit; repeat calls return already-exists). - const today = cstDateString(); - const date = (data === null || data === void 0 ? void 0 : data.date) && typeof data.date === 'string' ? data.date : today; - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { - throw new https_1.HttpsError('invalid-argument', 'date must be YYYY-MM-DD.'); - } - if (date !== today) { - throw new https_1.HttpsError('invalid-argument', 'Daily question can only be assigned for today.'); - } - const questionId = await pickDailyQuestionId(today); - if (!questionId) { - throw new https_1.HttpsError('internal', 'No active questions available.'); - } - const nextDay = nextCstDateString(date); - const docRef = db - .collection('couples') - .doc(coupleId) - .collection('daily_question') - .doc(date); - try { - await docRef.create({ - questionId, - date, - assignedAt: admin.firestore.FieldValue.serverTimestamp(), - expiresAt: timestampAt6PmCst(nextDay), - }); - } - catch (err) { - if (err instanceof https_1.HttpsError) - throw err; - if ((err === null || err === void 0 ? void 0 : err.code) === 6 || ((_d = err === null || err === void 0 ? void 0 : err.message) === null || _d === void 0 ? void 0 : _d.includes('ALREADY_EXISTS'))) { - throw new https_1.HttpsError('already-exists', `Daily question already assigned for ${date}.`); - } - throw new https_1.HttpsError('internal', 'Failed to assign daily question.'); - } - return { success: true, coupleId, date, questionId }; -}); -/** - * Picks a random active free question from the Firestore `questions` pool. - * - * The pool is expected to be a top-level collection with documents: - * - id: string - * - active: boolean - * - isPremium: boolean - * - * If no Firestore pool exists yet, falls back to a deterministic placeholder - * so the app still functions during rollout. In production, keep the local - * Room database and Firestore pool in sync through the existing seed flow. - */ -// Weekday → daily mode tag. FROZEN to mirror the client's DailyModeResolver.DOW_DEFAULTS -// (app/.../domain/DailyModeResolver.kt); JS getUTCDay(): 0=Sun … 6=Sat. -const WEEKDAY_MODE_TAGS = { - 0: 'mode_tiny_date_night', // Sunday — Slow Burn Sunday - 1: 'mode_soft_monday', // Monday — Mood Check Monday - 2: 'mode_snack_mission', // Tuesday — Tiny Win Tuesday - 3: 'mode_no_phone_moment', // Wednesday — Real One Wednesday - 4: 'mode_laugh_reset', // Thursday — Laugh It Off Thursday - 5: 'mode_flirty_friday', // Friday — Flirty Friday - 6: 'mode_weekend_side_quest', // Saturday — Side Quest Saturday -}; -/** Days since epoch for a YYYY-MM-DD Chicago date — mirrors LocalDate.toEpochDay() the client - * uses for its deterministic daily offset, so server and client land on the same question. */ -function epochDayOf(dateStr) { - const [y, m, d] = dateStr.split('-').map(Number); - return Math.floor(Date.UTC(y, m - 1, d) / 86400000); -} -// The client promotes ~10% of days to a "Wildcard" surprise theme instead of the weekday mode -// (DailyModeResolver.resolve(): dayOfYear % 10 === 3). FROZEN to mirror that tag + cadence. -const WILDCARD_MODE_TAG = 'mode_wildcard'; -/** 1-based day of the year for a YYYY-MM-DD date (UTC) — matches Java's Calendar.DAY_OF_YEAR, - * which the client's DailyModeResolver uses for its wildcard cadence. */ -function dayOfYearUtc(dateStr) { - const [y, m, d] = dateStr.split('-').map(Number); - return Math.floor((Date.UTC(y, m - 1, d) - Date.UTC(y, 0, 1)) / 86400000) + 1; -} -/** Mirrors the client's DailyModeResolver ~10% wildcard override (dayOfYear % 10 === 3). */ -function isWildcardDay(dateStr) { - return dayOfYearUtc(dateStr) % 10 === 3; -} -/** - * Deterministically pick the SAME daily question the free client would compute for [dateStr]: - * today's mode (weekday, or wildcard on ~10% of days), ordered by id, indexed by epochDay % - * poolSize. Making this SERVER-side and authoritative also fixes partners in different time zones - * getting different questions (the client falls back to device-local weekday only when there is no - * server assignment). Returns null if the questions pool is unseeded so the caller can no-op rather - * than write an unresolvable id. - */ -async function pickDailyQuestionId(dateStr) { - const db = admin.firestore(); - // The whole free daily pool is tiny (~75 rows); fetch once and filter in memory to avoid a - // composite index on (active, isPremium, modeTag). - const snapshot = await db - .collection('questions') - .where('active', '==', true) - .where('isPremium', '==', false) - .get(); - if (snapshot.empty) - return null; - const byId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0); - const inTag = (tag) => snapshot.docs.filter((d) => d.get('modeTag') === tag).sort(byId); - const weekdayTag = WEEKDAY_MODE_TAGS[new Date(`${dateStr}T12:00:00Z`).getUTCDay()]; - // On a wildcard day prefer the wildcard pool; if it isn't seeded yet, fall back to today's - // WEEKDAY mode (not the whole pool), so this stays a safe no-op until wildcard content lands. - let pool = isWildcardDay(dateStr) ? inTag(WILDCARD_MODE_TAG) : []; - if (pool.length === 0) - pool = inTag(weekdayTag); - if (pool.length === 0) - pool = snapshot.docs.slice().sort(byId); // last-resort: mode unseeded - const offset = ((epochDayOf(dateStr) % pool.length) + pool.length) % pool.length; - return pool[offset].id; -} -/** - * Returns today's date as YYYY-MM-DD in America/Chicago time. - */ -function cstDateString() { - return formatCstDate(new Date()); -} -/** - * Returns tomorrow's date as YYYY-MM-DD relative to America/Chicago time. - */ -function nextCstDateString(from) { - if (from) { - const d = parseCstDate(from); - d.setUTCDate(d.getUTCDate() + 1); - return formatCstDate(d); - } - // Tomorrow in Chicago: parse today's Chicago date back to a concrete instant and add 24h. - const todayChicago = parseCstDate(formatCstDate(new Date())); - todayChicago.setUTCDate(todayChicago.getUTCDate() + 1); - return formatCstDate(todayChicago); -} -/** - * UTC-vs-Chicago offset in hours at the given instant (5 during CDT, 6 during CST). - * DST-safe replacement for the old hardcoded -6 (which mislabeled dates and shifted - * reveals by an hour for the ~8 months of the year Chicago observes daylight time). - * Pattern matches streakReminder.ts's Intl-based chicagoDateKey. - */ -function chicagoOffsetHours(atUtcMs) { - const parts = new Intl.DateTimeFormat('en-US', { - timeZone: 'America/Chicago', - hour12: false, - year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', - }).formatToParts(new Date(atUtcMs)); - const get = (t) => { var _a; return Number((_a = parts.find((p) => p.type === t)) === null || _a === void 0 ? void 0 : _a.value); }; - const localAsUtcMs = Date.UTC(get('year'), get('month') - 1, get('day'), get('hour') % 24, get('minute')); - return Math.round((atUtcMs - localAsUtcMs) / 3600000); -} -/** YYYY-MM-DD for the given instant in America/Chicago (en-CA gives ISO date order). */ -function formatCstDate(d) { - return new Intl.DateTimeFormat('en-CA', { - timeZone: 'America/Chicago', - year: 'numeric', month: '2-digit', day: '2-digit', - }).format(d); -} -/** The UTC instant of midnight America/Chicago on the given calendar date. */ -function parseCstDate(dateStr) { - const [y, m, day] = dateStr.split('-').map(Number); - // Guess with the standard offset, then correct with the real offset at that instant. - const guessMs = Date.UTC(y, m - 1, day, 6); - const offset = chicagoOffsetHours(guessMs); - return new Date(Date.UTC(y, m - 1, day, offset)); -} -/** The UTC instant of 6:00 PM America/Chicago on the given calendar date. */ -function timestampAt6PmCst(dateStr) { - const [y, m, day] = dateStr.split('-').map(Number); - const guessMs = Date.UTC(y, m - 1, day, 18 + 6); - const offset = chicagoOffsetHours(guessMs); - return admin.firestore.Timestamp.fromMillis(Date.UTC(y, m - 1, day, 18 + offset)); -} -//# sourceMappingURL=assignDailyQuestion.js.map \ No newline at end of file diff --git a/functions/dist/questions/assignDailyQuestion.js.map b/functions/dist/questions/assignDailyQuestion.js.map deleted file mode 100644 index 37b5808b..00000000 --- a/functions/dist/questions/assignDailyQuestion.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"assignDailyQuestion.js","sourceRoot":"","sources":["../../src/questions/assignDailyQuestion.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/questions/assignDailyQuestion.test.js b/functions/dist/questions/assignDailyQuestion.test.js deleted file mode 100644 index 785758d0..00000000 --- a/functions/dist/questions/assignDailyQuestion.test.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const assignDailyQuestion_1 = require("./assignDailyQuestion"); -// DST regression tests for the Chicago date helpers (the old hardcoded -6 offset -// mislabeled dates and shifted reveal times during the ~8 CDT months). -describe('Chicago date helpers (DST-safe)', () => { - it('labels a summer (CDT, UTC-5) evening with the correct local date', () => { - // 2026-07-08T03:30Z == 2026-07-07 22:30 CDT — the old -6 math said "07-07" too, - // but 2026-07-08T04:30Z == 2026-07-07 23:30 CDT is where they diverge: - expect((0, assignDailyQuestion_1.formatCstDate)(new Date('2026-07-08T03:30:00Z'))).toBe('2026-07-07'); - // 23:30 CDT is still Jul 7 locally; hardcoded -6 called it Jul 7 22:30 → same date, - // but 2026-07-08T05:30Z == Jul 8 00:30 CDT; old math: Jul 7 23:30 → WRONG date. - expect((0, assignDailyQuestion_1.formatCstDate)(new Date('2026-07-08T05:30:00Z'))).toBe('2026-07-08'); - }); - it('labels a winter (CST, UTC-6) instant correctly', () => { - // 2026-01-08T05:30Z == 2026-01-07 23:30 CST - expect((0, assignDailyQuestion_1.formatCstDate)(new Date('2026-01-08T05:30:00Z'))).toBe('2026-01-07'); - expect((0, assignDailyQuestion_1.formatCstDate)(new Date('2026-01-08T06:30:00Z'))).toBe('2026-01-08'); - }); - it('parses midnight Chicago with the seasonal offset', () => { - // Summer: midnight CDT == 05:00Z - expect((0, assignDailyQuestion_1.parseCstDate)('2026-07-07').toISOString()).toBe('2026-07-07T05:00:00.000Z'); - // Winter: midnight CST == 06:00Z - expect((0, assignDailyQuestion_1.parseCstDate)('2026-01-07').toISOString()).toBe('2026-01-07T06:00:00.000Z'); - }); - it('puts the 6 PM reveal at 6 PM local in both seasons', () => { - // Summer: 18:00 CDT == 23:00Z - expect((0, assignDailyQuestion_1.timestampAt6PmCst)('2026-07-07').toDate().toISOString()).toBe('2026-07-07T23:00:00.000Z'); - // Winter: 18:00 CST == 00:00Z next day - expect((0, assignDailyQuestion_1.timestampAt6PmCst)('2026-01-07').toDate().toISOString()).toBe('2026-01-08T00:00:00.000Z'); - }); - it('round-trips format(parse(d)) across the spring-forward boundary', () => { - for (const day of ['2026-03-07', '2026-03-08', '2026-03-09', '2026-11-01', '2026-11-02']) { - expect((0, assignDailyQuestion_1.formatCstDate)((0, assignDailyQuestion_1.parseCstDate)(day))).toBe(day); - } - }); -}); -// Wildcard cadence must mirror the client's DailyModeResolver (dayOfYear % 10 === 3), or the -// server would assign a weekday question on days the app themes "Wildcard" (and vice versa). -describe('wildcard-day cadence (mirrors client DailyModeResolver)', () => { - it('computes 1-based day-of-year like Calendar.DAY_OF_YEAR', () => { - expect((0, assignDailyQuestion_1.dayOfYearUtc)('2026-01-01')).toBe(1); - expect((0, assignDailyQuestion_1.dayOfYearUtc)('2026-01-03')).toBe(3); - expect((0, assignDailyQuestion_1.dayOfYearUtc)('2026-02-01')).toBe(32); - expect((0, assignDailyQuestion_1.dayOfYearUtc)('2026-12-31')).toBe(365); // 2026 is not a leap year - expect((0, assignDailyQuestion_1.dayOfYearUtc)('2028-12-31')).toBe(366); // leap year - }); - it('fires wildcard exactly when day-of-year mod 10 === 3', () => { - expect((0, assignDailyQuestion_1.isWildcardDay)('2026-01-03')).toBe(true); // doy 3 - expect((0, assignDailyQuestion_1.isWildcardDay)('2026-01-13')).toBe(true); // doy 13 - expect((0, assignDailyQuestion_1.isWildcardDay)('2026-02-01')).toBe(false); // doy 32 - expect((0, assignDailyQuestion_1.isWildcardDay)('2026-01-01')).toBe(false); // doy 1 - expect((0, assignDailyQuestion_1.isWildcardDay)('2026-01-04')).toBe(false); // doy 4 - }); - it('flags ~10% of the year as wildcard days', () => { - let wild = 0; - for (let doy = 1; doy <= 365; doy++) { - const d = new Date(Date.UTC(2026, 0, doy)); - const dateStr = d.toISOString().slice(0, 10); - if ((0, assignDailyQuestion_1.isWildcardDay)(dateStr)) - wild++; - } - expect(wild).toBe(37); // doy ∈ {3,13,…,363} → 37 days (~10.1%) - }); -}); -//# sourceMappingURL=assignDailyQuestion.test.js.map \ No newline at end of file diff --git a/functions/dist/questions/assignDailyQuestion.test.js.map b/functions/dist/questions/assignDailyQuestion.test.js.map deleted file mode 100644 index 0bc2d338..00000000 --- a/functions/dist/questions/assignDailyQuestion.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"assignDailyQuestion.test.js","sourceRoot":"","sources":["../../src/questions/assignDailyQuestion.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/questions/onAnswerRevealed.js b/functions/dist/questions/onAnswerRevealed.js deleted file mode 100644 index 1326f2ec..00000000 --- a/functions/dist/questions/onAnswerRevealed.js +++ /dev/null @@ -1,113 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onAnswerRevealed = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const quietHours_1 = require("../notifications/quietHours"); -const push_1 = require("../notifications/push"); -const idempotency_1 = require("../notifications/idempotency"); -const log_1 = require("../log"); -/** - * Firestore trigger: when one partner OPENS (reveals) the shared answers — i.e. their own - * daily answer doc flips isRevealed false → true — notify the other partner that they've - * looked, so the pair stays in sync ("[Name] just opened your answers"). - * - * Path: couples/{coupleId}/daily_question/{date}/answers/{userId} - */ -exports.onAnswerRevealed = (0, firestore_1.onDocumentUpdated)('couples/{coupleId}/daily_question/{date}/answers/{userId}', async (event) => { - var _a, _b, _c, _d, _e, _f, _g; - const { coupleId, date, userId } = event.params; - const before = ((_b = (_a = event.data) === null || _a === void 0 ? void 0 : _a.before.data()) !== null && _b !== void 0 ? _b : {}); - const after = ((_d = (_c = event.data) === null || _c === void 0 ? void 0 : _c.after.data()) !== null && _d !== void 0 ? _d : {}); - // Only fire on the false → true reveal transition. - if (before.isRevealed === true || after.isRevealed !== true) - return; - const db = admin.firestore(); - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) { - log_1.logger.warn(`[onAnswerRevealed] couple ${coupleId} not found`); - return; - } - const userIds = ((_f = (_e = coupleDoc.data()) === null || _e === void 0 ? void 0 : _e.userIds) !== null && _f !== void 0 ? _f : []); - if (!userIds.includes(userId)) { - log_1.logger.warn(`[onAnswerRevealed] revealer ${userId} not a member of ${coupleId}`); - return; - } - const partnerId = userIds.find((uid) => uid !== userId); - if (!partnerId) { - log_1.logger.warn(`[onAnswerRevealed] no partner for couple ${coupleId}`); - return; - } - const partnerUserDoc = await db.collection('users').doc(partnerId).get(); - const partnerData = partnerUserDoc.data(); - // Respect the same partner-activity opt-out as the answered ping. - if ((partnerData === null || partnerData === void 0 ? void 0 : partnerData.notifPartnerAnswered) === false) { - log_1.logger.log(`[onAnswerRevealed] partner ${partnerId} has partner-activity notifications off`); - return; - } - // M-001: honor the recipient's quiet-hours window ("no notifications" promise). Fail-open. - if ((0, quietHours_1.recipientInQuietHours)(partnerData)) { - log_1.logger.log(`[onAnswerRevealed] partner ${partnerId} is in quiet hours — suppressing`); - return; - } - // Dedupe redelivery of this reveal (at-least-once). The false→true guard above already stops - // the marker write from re-triggering this handler; the claim stops a redelivered event. - if (!(await (0, idempotency_1.claimOnce)((0, idempotency_1.notifMark)(db, coupleId, `reveal-${date}-${userId}`)))) { - log_1.logger.log(`[onAnswerRevealed] already notified for ${userId}'s reveal on ${date}; skipping`); - return; - } - const questionId = typeof after.questionId === 'string' ? after.questionId : ''; - // displayName is E2EE in users/{uid} → generic title; the app shows the real name in-app. Avatar - // (photoUrl) stays plaintext, so it's still sent. - const revealerDoc = await db.collection('users').doc(userId).get(); - const revealerAvatar = (_g = revealerDoc.data()) === null || _g === void 0 ? void 0 : _g.photoUrl; - const res = await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { - title: 'Your partner opened your answers', - body: 'Open to see what you each said.', - }, - data: Object.assign({ type: 'partner_opened_answer', couple_id: coupleId, question_id: questionId, date }, (typeof revealerAvatar === 'string' && revealerAvatar.length > 0 - ? { sender_avatar_url: revealerAvatar } - : {})), - android: { notification: { channelId: 'partner_activity' } }, - }, partnerData); - if (res.sent === 0 && res.failed === 0) { - log_1.logger.log(`[onAnswerRevealed] no FCM tokens for partner ${partnerId}`); - return; - } - log_1.logger.log(`[onAnswerRevealed] notified ${partnerId} that ${userId} opened couple ${coupleId}`); -}); -//# sourceMappingURL=onAnswerRevealed.js.map \ No newline at end of file diff --git a/functions/dist/questions/onAnswerRevealed.js.map b/functions/dist/questions/onAnswerRevealed.js.map deleted file mode 100644 index 1a6b0979..00000000 --- a/functions/dist/questions/onAnswerRevealed.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onAnswerRevealed.js","sourceRoot":"","sources":["../../src/questions/onAnswerRevealed.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAAuC;AACvC,+DAAmE;AACnE,4DAAmE;AACnE,gDAAsD;AACtD,8DAAmE;AACnE,gCAA+B;AAE/B;;;;;;GAMG;AACU,QAAA,gBAAgB,GAAG,IAAA,6BAAiB,EAC/C,2DAA2D,EAC3D,KAAK,EAAE,KAAK,EAAE,EAAE;;IACd,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAA;IAE/C,MAAM,MAAM,GAAG,CAAC,MAAA,MAAA,KAAK,CAAC,IAAI,0CAAE,MAAM,CAAC,IAAI,EAAE,mCAAI,EAAE,CAAqC,CAAA;IACpF,MAAM,KAAK,GAAG,CAAC,MAAA,MAAA,KAAK,CAAC,IAAI,0CAAE,KAAK,CAAC,IAAI,EAAE,mCAAI,EAAE,CAAqC,CAAA;IAElF,mDAAmD;IACnD,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI;QAAE,OAAM;IAEnE,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IACpE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACtB,YAAM,CAAC,IAAI,CAAC,6BAA6B,QAAQ,YAAY,CAAC,CAAA;QAC9D,OAAM;IACR,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,YAAM,CAAC,IAAI,CAAC,+BAA+B,MAAM,oBAAoB,QAAQ,EAAE,CAAC,CAAA;QAChF,OAAM;IACR,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,CAAA;IACvD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,YAAM,CAAC,IAAI,CAAC,4CAA4C,QAAQ,EAAE,CAAC,CAAA;QACnE,OAAM;IACR,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IACxE,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,EAAE,CAAA;IAEzC,kEAAkE;IAClE,IAAI,CAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,oBAAoB,MAAK,KAAK,EAAE,CAAC;QAChD,YAAM,CAAC,GAAG,CAAC,8BAA8B,SAAS,yCAAyC,CAAC,CAAA;QAC5F,OAAM;IACR,CAAC;IAED,2FAA2F;IAC3F,IAAI,IAAA,kCAAqB,EAAC,WAAW,CAAC,EAAE,CAAC;QACvC,YAAM,CAAC,GAAG,CAAC,8BAA8B,SAAS,kCAAkC,CAAC,CAAA;QACrF,OAAM;IACR,CAAC;IAED,6FAA6F;IAC7F,yFAAyF;IACzF,IAAI,CAAC,CAAC,MAAM,IAAA,uBAAS,EAAC,IAAA,uBAAS,EAAC,EAAE,EAAE,QAAQ,EAAE,UAAU,IAAI,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,YAAM,CAAC,GAAG,CAAC,2CAA2C,MAAM,gBAAgB,IAAI,YAAY,CAAC,CAAA;QAC7F,OAAM;IACR,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAA;IAC/E,iGAAiG;IACjG,kDAAkD;IAClD,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAA;IAClE,MAAM,cAAc,GAAG,MAAA,WAAW,CAAC,IAAI,EAAE,0CAAE,QAAQ,CAAA;IAEnD,MAAM,GAAG,GAAG,MAAM,IAAA,qBAAc,EAC9B,EAAE,EACF,KAAK,CAAC,SAAS,EAAE,EACjB,SAAS,EACT;QACE,YAAY,EAAE;YACZ,KAAK,EAAE,kCAAkC;YACzC,IAAI,EAAE,iCAAiC;SACxC;QACD,IAAI,kBACF,IAAI,EAAE,uBAAuB,EAC7B,SAAS,EAAE,QAAQ,EACnB,WAAW,EAAE,UAAU,EACvB,IAAI,IACD,CAAC,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC;YACjE,CAAC,CAAC,EAAE,iBAAiB,EAAE,cAAc,EAAE;YACvC,CAAC,CAAC,EAAE,CAAC,CACR;QACD,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE;KAC7D,EACD,WAAW,CACZ,CAAA;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvC,YAAM,CAAC,GAAG,CAAC,gDAAgD,SAAS,EAAE,CAAC,CAAA;QACvE,OAAM;IACR,CAAC;IAED,YAAM,CAAC,GAAG,CAAC,+BAA+B,SAAS,SAAS,MAAM,kBAAkB,QAAQ,EAAE,CAAC,CAAA;AACjG,CAAC,CACF,CAAA"} \ No newline at end of file diff --git a/functions/dist/questions/onAnswerWritten.js b/functions/dist/questions/onAnswerWritten.js deleted file mode 100644 index 2bef5fc5..00000000 --- a/functions/dist/questions/onAnswerWritten.js +++ /dev/null @@ -1,132 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onAnswerWritten = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const quietHours_1 = require("../notifications/quietHours"); -const push_1 = require("../notifications/push"); -const idempotency_1 = require("../notifications/idempotency"); -const log_1 = require("../log"); -/** - * Firestore trigger that sends an FCM notification to the other partner when - * one partner writes an answer under - * couples/{coupleId}/daily_question/{date}/answers/{userId}. - * - * The notification payload is a data message so the client can route directly to - * the answer reveal screen. It also contains a `notification` block for - * system-tray display when the app is in the background. - */ -exports.onAnswerWritten = (0, firestore_1.onDocumentCreated)('couples/{coupleId}/daily_question/{date}/answers/{userId}', async (event) => { - var _a, _b, _c; - const { coupleId, date, userId } = event.params; - const snap = event.data; - if (!snap) - return; - const db = admin.firestore(); - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) { - log_1.logger.warn(`[onAnswerWritten] couple ${coupleId} not found`); - return; - } - const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []); - // Security review Batch 2: re-verify the writer actually belongs to this couple - // before sending a cross-user notification. Firestore rules already enforce this, - // but defense-in-depth ensures a stray/forged answer doc can't trigger a partner ping. - if (!userIds.includes(userId)) { - log_1.logger.warn(`[onAnswerWritten] writer ${userId} is not a member of couple ${coupleId}`); - return; - } - const partnerId = userIds.find((uid) => uid !== userId); - if (!partnerId) { - log_1.logger.warn(`[onAnswerWritten] no partner found for couple ${coupleId}`); - return; - } - const partnerUserDoc = await db.collection('users').doc(partnerId).get(); - const partnerData = partnerUserDoc.data(); - // Respect the partner's notification preference (opt-out; default is enabled). - if ((partnerData === null || partnerData === void 0 ? void 0 : partnerData.notifPartnerAnswered) === false) { - log_1.logger.log(`[onAnswerWritten] partner ${partnerId} has partner-answered notifications off`); - return; - } - // M-001: honor the recipient's quiet-hours window ("no notifications" promise). Fail-open. - if ((0, quietHours_1.recipientInQuietHours)(partnerData)) { - log_1.logger.log(`[onAnswerWritten] partner ${partnerId} is in quiet hours — suppressing`); - return; - } - // Dedupe: at-least-once delivery can redeliver this create; claim once so a redelivery - // doesn't double-ping the partner (at-most-once: a post-claim send failure drops this one). - if (!(await (0, idempotency_1.claimOnce)((0, idempotency_1.notifMark)(db, coupleId, `answer-${date}-${userId}`)))) { - log_1.logger.log(`[onAnswerWritten] already notified for ${userId}'s answer on ${date}; skipping`); - return; - } - const answerData = snap.data(); - const questionId = typeof answerData.questionId === 'string' ? answerData.questionId : ''; - // Sender (the partner who just answered) avatar — used as the notification large icon. - const senderDoc = await db.collection('users').doc(userId).get(); - const senderAvatar = (_c = senderDoc.data()) === null || _c === void 0 ? void 0 : _c.photoUrl; - // Did this answer COMPLETE the pair? If the recipient (partner) has already answered, the - // reveal just unlocked for both — tell them it's ready to open, instead of "go answer". - const partnerAnswerSnap = await db - .collection('couples').doc(coupleId) - .collection('daily_question').doc(date) - .collection('answers').doc(partnerId).get(); - const bothAnswered = partnerAnswerSnap.exists; - const res = await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: bothAnswered - ? { - title: 'Your answers are unlocked ✨', - body: "You've both answered — open to see what you each said.", - } - : { - title: 'Your partner just answered!', - body: "See what they shared for tonight's prompt.", - }, - data: Object.assign({ type: 'partner_answered', couple_id: coupleId, question_id: questionId, date }, (typeof senderAvatar === 'string' && senderAvatar.length > 0 - ? { sender_avatar_url: senderAvatar } - : {})), - android: { notification: { channelId: 'partner_activity' } }, // E-OBS - }, partnerData); - if (res.sent === 0 && res.failed === 0) { - log_1.logger.log(`[onAnswerWritten] no FCM tokens for partner ${partnerId}`); - return; - } - // Track last activity time on the couple doc for re-engagement targeting. - await db.collection('couples').doc(coupleId).update({ - lastAnsweredAt: admin.firestore.FieldValue.serverTimestamp(), - }).catch((e) => log_1.logger.warn('[onAnswerWritten] lastAnsweredAt update failed', { error: String(e) })); - log_1.logger.log(`[onAnswerWritten] notified partner ${partnerId} for couple ${coupleId} question ${questionId}`); -}); -//# sourceMappingURL=onAnswerWritten.js.map \ No newline at end of file diff --git a/functions/dist/questions/onAnswerWritten.js.map b/functions/dist/questions/onAnswerWritten.js.map deleted file mode 100644 index bc27a4e3..00000000 --- a/functions/dist/questions/onAnswerWritten.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onAnswerWritten.js","sourceRoot":"","sources":["../../src/questions/onAnswerWritten.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/questions/onMessageWritten.js b/functions/dist/questions/onMessageWritten.js deleted file mode 100644 index 004aab7f..00000000 --- a/functions/dist/questions/onMessageWritten.js +++ /dev/null @@ -1,111 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onMessageWritten = void 0; -const admin = __importStar(require("firebase-admin")); -const firestore_1 = require("firebase-functions/v2/firestore"); -const quietHours_1 = require("../notifications/quietHours"); -const push_1 = require("../notifications/push"); -const idempotency_1 = require("../notifications/idempotency"); -const log_1 = require("../log"); -/** - * Firestore trigger that notifies the other partner when a chat message is - * sent in a conversation (the couple chat or a per-question discussion). - * - * Path: couples/{coupleId}/conversations/{conversationId}/messages/{messageId} - * - * Respects the recipient's `notifChatMessage` preference (default: enabled). - */ -exports.onMessageWritten = (0, firestore_1.onDocumentCreated)('couples/{coupleId}/conversations/{conversationId}/messages/{messageId}', async (event) => { - var _a, _b, _c, _d; - const { coupleId, conversationId, messageId } = event.params; - const snap = event.data; - if (!snap) - return; - const db = admin.firestore(); - const messageData = snap.data(); - const authorId = typeof messageData.authorUserId === 'string' ? messageData.authorUserId : null; - if (!authorId) { - log_1.logger.warn(`[onMessageWritten] no authorUserId on message ${messageId}`); - return; - } - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) { - log_1.logger.warn(`[onMessageWritten] couple ${coupleId} not found`); - return; - } - const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []); - const partnerId = userIds.find((uid) => uid !== authorId); - if (!partnerId) { - log_1.logger.warn(`[onMessageWritten] no partner found for couple ${coupleId}`); - return; - } - const partnerUserDoc = await db.collection('users').doc(partnerId).get(); - const partnerData = partnerUserDoc.data(); - // Respect the partner's notification preference (opt-out; default is enabled). - if ((partnerData === null || partnerData === void 0 ? void 0 : partnerData.notifChatMessage) === false) { - log_1.logger.log(`[onMessageWritten] partner ${partnerId} has chat notifications off`); - return; - } - // M-001: honor the recipient's quiet-hours window ("no notifications" promise). Fail-open. - if ((0, quietHours_1.recipientInQuietHours)(partnerData)) { - log_1.logger.log(`[onMessageWritten] partner ${partnerId} is in quiet hours — suppressing`); - return; - } - // Dedupe redelivery of this message create (at-least-once). messageId is unique per message. - if (!(await (0, idempotency_1.claimOnce)((0, idempotency_1.notifMark)(db, coupleId, `msg-${messageId}`)))) { - log_1.logger.log(`[onMessageWritten] already notified for message ${messageId}; skipping`); - return; - } - // displayName is E2EE in users/{uid}, so the server can't read it for the OS-rendered push (the - // app shows the real name in-app). photoUrl stays plaintext, so the avatar is still sent. - const authorDoc = await db.collection('users').doc(authorId).get(); - const authorPhotoUrl = (_d = (_c = authorDoc.data()) === null || _c === void 0 ? void 0 : _c.photoUrl) !== null && _d !== void 0 ? _d : ''; - const res = await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { - title: 'Your partner sent a message', - body: 'Tap to read and reply.', - }, - data: Object.assign({ type: 'chat_message', couple_id: coupleId, conversation_id: conversationId }, (authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {})), - // E-OBS: backgrounded delivery on the Chat/partner channel, not the FCM fallback channel. - android: { notification: { channelId: 'partner_activity' } }, - }, partnerData); - if (res.sent === 0 && res.failed === 0) { - log_1.logger.log(`[onMessageWritten] no FCM tokens for partner ${partnerId}`); - return; - } - log_1.logger.log(`[onMessageWritten] notified partner ${partnerId} for conversation ${conversationId} in couple ${coupleId}`); -}); -//# sourceMappingURL=onMessageWritten.js.map \ No newline at end of file diff --git a/functions/dist/questions/onMessageWritten.js.map b/functions/dist/questions/onMessageWritten.js.map deleted file mode 100644 index 57b01063..00000000 --- a/functions/dist/questions/onMessageWritten.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/releaseKey/wrapReleaseKeyCallable.js b/functions/dist/releaseKey/wrapReleaseKeyCallable.js deleted file mode 100644 index f4cbf1c2..00000000 --- a/functions/dist/releaseKey/wrapReleaseKeyCallable.js +++ /dev/null @@ -1,162 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.wrapReleaseKeyCallable = void 0; -const admin = __importStar(require("firebase-admin")); -const https_1 = require("firebase-functions/v2/https"); -const log_1 = require("../log"); -const DEFAULT_AAD = 'closer_release_key'; -exports.wrapReleaseKeyCallable = (0, https_1.onCall)({ memory: '512MiB' }, async (request) => { - var _a, _b, _c, _d, _e; - const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); - } - if (!request.app) { - throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); - } - const data = request.data; - const recipientUserId = data === null || data === void 0 ? void 0 : data.recipientUserId; - const oneTimeKeyB64 = data === null || data === void 0 ? void 0 : data.oneTimeKey; - if (!recipientUserId || typeof recipientUserId !== 'string') { - throw new https_1.HttpsError('invalid-argument', 'recipientUserId is required.'); - } - if (!oneTimeKeyB64 || typeof oneTimeKeyB64 !== 'string') { - throw new https_1.HttpsError('invalid-argument', 'oneTimeKey is required.'); - } - // Validate the oneTimeKey is well-formed base64 of a 32-byte AES-256 key. - let oneTimeKey; - try { - oneTimeKey = Buffer.from(oneTimeKeyB64, 'base64'); - } - catch (_f) { - throw new https_1.HttpsError('invalid-argument', 'oneTimeKey is not valid base64.'); - } - if (oneTimeKey.length !== 32) { - throw new https_1.HttpsError('invalid-argument', `oneTimeKey must be 32 bytes; got ${oneTimeKey.length}.`); - } - const db = admin.firestore(); - // Caller must be in a couple and the recipient must be their partner. - const callerDoc = await db.collection('users').doc(callerId).get(); - const coupleId = (_b = callerDoc.data()) === null || _b === void 0 ? void 0 : _b.coupleId; - if (!coupleId) { - throw new https_1.HttpsError('failed-precondition', 'Caller is not paired.'); - } - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - if (!coupleDoc.exists) { - throw new https_1.HttpsError('not-found', 'Couple not found.'); - } - const coupleUserIds = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds; - if (!coupleUserIds || !coupleUserIds.includes(callerId)) { - throw new https_1.HttpsError('permission-denied', 'Caller is not a member of this couple.'); - } - if (!coupleUserIds.includes(recipientUserId)) { - throw new https_1.HttpsError('permission-denied', 'recipientUserId is not the caller\'s partner.'); - } - if (recipientUserId === callerId) { - throw new https_1.HttpsError('permission-denied', 'Cannot wrap a release key for yourself.'); - } - // Read recipient's public key. Android stores it at users/{uid}/devices/primary. - const deviceDoc = await db.collection('users').doc(recipientUserId).collection('devices').doc('primary').get(); - if (!deviceDoc.exists) { - throw new https_1.HttpsError('failed-precondition', 'Recipient has not registered a release-key device. Ask them to open the app first.'); - } - const publicKeyB64 = (_d = deviceDoc.data()) === null || _d === void 0 ? void 0 : _d.publicKey; - if (!publicKeyB64 || typeof publicKeyB64 !== 'string') { - throw new https_1.HttpsError('failed-precondition', 'Recipient device is missing a public key.'); - } - // The actual Tink wrap requires the Tink runtime, which is available on the server via - // the Node.js tink-crypto package. We import it lazily so missing Tink is a clear runtime - // error rather than a cold-start crash. - let tinkAead; - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const tink = require('@tink-crypto/tink-crypto'); - tinkAead = tink.aead; - } - catch (_g) { - throw new https_1.HttpsError('internal', 'Tink crypto library is not available on the server.'); - } - // Decode the recipient's public keyset and create a HybridEncrypt primitive. - let hybridEncrypt; - try { - const publicKeyset = Buffer.from(publicKeyB64.replace(/^pub:v1:/, ''), 'base64url'); - const publicHandle = tinkAead.cleartextKeysetHandle.read(tinkAead.jsonKeysetReader.withBytes(publicKeyset)); - hybridEncrypt = publicHandle.getPrimitive(tinkAead.hybrid.HybridEncrypt); - } - catch (err) { - log_1.logger.warn(`[wrapReleaseKeyCallable] public key parse failed for ${recipientUserId}`, { error: String(err) }); - throw new https_1.HttpsError('failed-precondition', 'Recipient public key could not be parsed.'); - } - // Wrap the one-time key. - const aad = (_e = data === null || data === void 0 ? void 0 : data.aad) !== null && _e !== void 0 ? _e : DEFAULT_AAD; - let ciphertext; - try { - ciphertext = Buffer.from(hybridEncrypt.encrypt(oneTimeKey, Buffer.from(aad, 'utf-8'))); - } - catch (err) { - log_1.logger.warn(`[wrapReleaseKeyCallable] wrap failed for ${recipientUserId}`, { error: String(err) }); - throw new https_1.HttpsError('internal', 'Failed to wrap release key.'); - } - // Build the keybox:v1: envelope. The response also exposes the raw components so iOS - // can log them for debugging without learning the Tink-internal layout. - const keyboxB64 = ciphertext.toString('base64url'); - const auditFields = { - action: 'wrap_release_key', - actor: callerId, - recipient: recipientUserId, - coupleId, - aad, - timestamp: admin.firestore.FieldValue.serverTimestamp(), - }; - // Best-effort audit log. Do not fail the wrap if logging fails. - try { - await db.collection('users').doc(callerId).collection('notification_queue').add(Object.assign(Object.assign({}, auditFields), { type: 'wrap_release_key', read: true })); - } - catch (err) { - log_1.logger.warn(`[wrapReleaseKeyCallable] audit log failed for ${callerId}`, { error: String(err) }); - } - // Tink's ECIES ciphertext is a single opaque blob; the ephemeral public key and MAC are - // embedded inside it. We expose placeholder fields for iOS diagnostics. - const response = { - keybox: `keybox:v1:${keyboxB64}`, - ephemeralPublicKey: '', - ciphertext: keyboxB64, - mac: '', - }; - log_1.logger.log(`[wrapReleaseKeyCallable] ${callerId} wrapped release key for ${recipientUserId} in couple ${coupleId}`); - return response; -}); -//# sourceMappingURL=wrapReleaseKeyCallable.js.map \ No newline at end of file diff --git a/functions/dist/releaseKey/wrapReleaseKeyCallable.js.map b/functions/dist/releaseKey/wrapReleaseKeyCallable.js.map deleted file mode 100644 index 7147cca8..00000000 --- a/functions/dist/releaseKey/wrapReleaseKeyCallable.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"wrapReleaseKeyCallable.js","sourceRoot":"","sources":["../../src/releaseKey/wrapReleaseKeyCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/security/checkDeviceIntegrity.js b/functions/dist/security/checkDeviceIntegrity.js deleted file mode 100644 index d483c8a4..00000000 --- a/functions/dist/security/checkDeviceIntegrity.js +++ /dev/null @@ -1,72 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.checkDeviceIntegrity = void 0; -const https_1 = require("firebase-functions/v2/https"); -const google_auth_library_1 = require("google-auth-library"); -const log_1 = require("../log"); -const PACKAGE_NAME = 'app.closer'; -const PLAY_INTEGRITY_URL = `https://playintegrity.googleapis.com/v1/${PACKAGE_NAME}:decodeIntegrityToken`; -// Cap the upstream call so a hung Play Integrity API can't pin the instance for the whole -// function timeout; the fail-closed catch below turns a timeout into passed:false. -const PLAY_INTEGRITY_TIMEOUT_MS = 10000; -/** - * Verifies a Play Integrity API token server-side and returns whether the - * device meets basic integrity requirements. - * - * Called by [PlayIntegrityChecker] on app startup. Requires the caller to be - * authenticated — the Firebase Auth token is verified automatically by the - * Functions runtime. - * - * Setup required before this function works in production: - * 1. Enable the Play Integrity API in your Google Cloud project. - * 2. Link the Cloud project to your Play Console app (Play Console → - * Setup → API access → Link to Cloud project). - * 3. Grant the Cloud Functions service account the "Play Integrity API User" - * role in IAM, or use a dedicated service account key via - * GOOGLE_APPLICATION_CREDENTIALS. - * - * If the Play Integrity API is not configured the function fails-closed - * (returns passed: false). Configure the API and grant the service account - * the "Play Integrity API User" IAM role before deploying to production. - */ -exports.checkDeviceIntegrity = (0, https_1.onCall)({ memory: '512MiB' }, async (request) => { - var _a; - if (!request.auth) { - throw new https_1.HttpsError('unauthenticated', 'Caller must be authenticated.'); - } - if (!request.app) { - throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); - } - const token = (_a = request.data) === null || _a === void 0 ? void 0 : _a.token; - if (!token || typeof token !== 'string') { - throw new https_1.HttpsError('invalid-argument', 'token is required.'); - } - try { - const verdicts = await decodeIntegrityToken(token); - const passed = verdicts.includes('MEETS_DEVICE_INTEGRITY') || - verdicts.includes('MEETS_STRONG_INTEGRITY'); - return { passed, verdicts }; - } - catch (err) { - log_1.logger.error('[checkDeviceIntegrity] verification failed', { error: String(err) }); - // Fail-closed: an unverifiable request is treated as failed, not passed. - // Ensure the Play Integrity API is enabled and the service account has - // "Play Integrity API User" role before deploying to production. - return { passed: false, verdicts: [], error: 'verification_unavailable' }; - } -}); -async function decodeIntegrityToken(token) { - var _a, _b, _c; - const auth = new google_auth_library_1.GoogleAuth({ - scopes: ['https://www.googleapis.com/auth/playintegrity'], - }); - const client = await auth.getClient(); - const response = await client.request({ - url: PLAY_INTEGRITY_URL, - method: 'POST', - data: { integrity_token: token }, - timeout: PLAY_INTEGRITY_TIMEOUT_MS, - }); - return ((_c = (_b = (_a = response.data.tokenPayloadExternal) === null || _a === void 0 ? void 0 : _a.deviceIntegrity) === null || _b === void 0 ? void 0 : _b.deviceRecognitionVerdict) !== null && _c !== void 0 ? _c : []); -} -//# sourceMappingURL=checkDeviceIntegrity.js.map \ No newline at end of file diff --git a/functions/dist/security/checkDeviceIntegrity.js.map b/functions/dist/security/checkDeviceIntegrity.js.map deleted file mode 100644 index 658163bb..00000000 --- a/functions/dist/security/checkDeviceIntegrity.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"checkDeviceIntegrity.js","sourceRoot":"","sources":["../../src/security/checkDeviceIntegrity.ts"],"names":[],"mappings":";;;AAAA,uDAAiF;AACjF,6DAAgD;AAChD,gCAA+B;AAE/B,MAAM,YAAY,GAAG,YAAY,CAAA;AACjC,MAAM,kBAAkB,GACtB,2CAA2C,YAAY,uBAAuB,CAAA;AAChF,0FAA0F;AAC1F,mFAAmF;AACnF,MAAM,yBAAyB,GAAG,KAAM,CAAA;AAExC;;;;;;;;;;;;;;;;;;;GAmBG;AACU,QAAA,oBAAoB,GAAG,IAAA,cAAM,EACxC,EAAE,MAAM,EAAE,QAAQ,EAAE,EACpB,KAAK,EAAE,OAA4C,EAAE,EAAE;;IACrD,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAClB,MAAM,IAAI,kBAAU,CAAC,iBAAiB,EAAE,+BAA+B,CAAC,CAAA;IAC1E,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACjB,MAAM,IAAI,kBAAU,CAAC,qBAAqB,EAAE,kCAAkC,CAAC,CAAA;IACjF,CAAC;IAED,MAAM,KAAK,GAAG,MAAA,OAAO,CAAC,IAAI,0CAAE,KAAK,CAAA;IACjC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxC,MAAM,IAAI,kBAAU,CAAC,kBAAkB,EAAE,oBAAoB,CAAC,CAAA;IAChE,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,KAAK,CAAC,CAAA;QAClD,MAAM,MAAM,GACV,QAAQ,CAAC,QAAQ,CAAC,wBAAwB,CAAC;YAC3C,QAAQ,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAA;QAC7C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,YAAM,CAAC,KAAK,CAAC,4CAA4C,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAClF,yEAAyE;QACzE,uEAAuE;QACvE,iEAAiE;QACjE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAA;IAC3E,CAAC;AACH,CAAC,CACF,CAAA;AAUD,KAAK,UAAU,oBAAoB,CAAC,KAAa;;IAC/C,MAAM,IAAI,GAAG,IAAI,gCAAU,CAAC;QAC1B,MAAM,EAAE,CAAC,+CAA+C,CAAC;KAC1D,CAAC,CAAA;IACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;IACrC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAoB;QACvD,GAAG,EAAE,kBAAkB;QACvB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE;QAChC,OAAO,EAAE,yBAAyB;KACnC,CAAC,CAAA;IACF,OAAO,CACL,MAAA,MAAA,MAAA,QAAQ,CAAC,IAAI,CAAC,oBAAoB,0CAAE,eAAe,0CAC/C,wBAAwB,mCAAI,EAAE,CACnC,CAAA;AACH,CAAC"} \ No newline at end of file diff --git a/functions/dist/users/onUserDelete.js b/functions/dist/users/onUserDelete.js deleted file mode 100644 index ad98fa95..00000000 --- a/functions/dist/users/onUserDelete.js +++ /dev/null @@ -1,138 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.onUserDelete = void 0; -const admin = __importStar(require("firebase-admin")); -const v1_1 = require("firebase-functions/v1"); -const push_1 = require("../notifications/push"); -const log_1 = require("../log"); -/** - * Auth deletion trigger — cascades account deletion server-side. - * - * This is the ONE function kept on the v1 (1st-gen) API: 2nd gen has no - * `auth.user().onDelete` equivalent, so it is imported explicitly from - * firebase-functions/v1 and coexists with the v2 functions in this codebase. - * Global v2 options (setGlobalOptions) do not apply to it, so the timeout/memory - * for its dual recursiveDelete + Storage sweep are set here via runWith(). - * - * Fires when a Firebase Auth user is deleted (via client deleteAccount() or - * Admin SDK). The Admin SDK bypasses Firestore rules, so this function can - * reach everything the client cannot. - * - * Steps (all best-effort after the auth account is already gone): - * 1. If the user was in a couple: - * a. Clear the partner's coupleId. - * b. Send the partner an FCM push notification. - * c. Recursively delete the couple doc + all subcollections. - * 2. Delete all Storage objects under users/{uid}/ (profile photo etc.). - * 3. Recursively delete the user doc + all subcollections - * (entitlements, fcmTokens, notification_queue). - */ -exports.onUserDelete = (0, v1_1.runWith)({ timeoutSeconds: 300, memory: '512MB' }) - .auth.user() - .onDelete(async (user) => { - var _a, _b, _c, _d; - const uid = user.uid; - const db = admin.firestore(); - // ── 1. Couple unpair ────────────────────────────────────────────────────── - const userDocRef = db.collection('users').doc(uid); - const userDoc = await userDocRef.get(); - const coupleId = userDoc.exists - ? (_a = userDoc.data()) === null || _a === void 0 ? void 0 : _a.coupleId - : undefined; - if (coupleId) { - const coupleRef = db.collection('couples').doc(coupleId); - const coupleDoc = await coupleRef.get(); - if (coupleDoc.exists) { - const userIds = ((_c = (_b = coupleDoc.data()) === null || _b === void 0 ? void 0 : _b.userIds) !== null && _c !== void 0 ? _c : []); - const partnerId = userIds.find((id) => id !== uid); - if (partnerId) { - const partnerRef = db.collection('users').doc(partnerId); - const partnerDoc = await partnerRef.get(); - // Only act if the partner is still recorded in this couple. - if (partnerDoc.exists && ((_d = partnerDoc.data()) === null || _d === void 0 ? void 0 : _d.coupleId) === coupleId) { - // Clear partner's coupleId first so the onCoupleLeave trigger does - // not fire a second time (it would find no partner left to notify). - await partnerRef.update({ coupleId: null }); - // Send push notification to the partner. - await notifyPartner(partnerId, partnerDoc, db).catch((err) => { - log_1.logger.warn(`[onUserDelete] FCM notification to partner ${partnerId} failed`, { error: String(err) }); - }); - } - } - // Recursively delete the couple doc + all subcollections. - await db.recursiveDelete(coupleRef); - } - } - // ── 2. Storage cleanup ──────────────────────────────────────────────────── - try { - await admin.storage().bucket().deleteFiles({ prefix: `users/${uid}/` }); - } - catch (err) { - log_1.logger.warn(`[onUserDelete] Storage cleanup failed for ${uid}`, { error: String(err) }); - } - // ── 3. User doc + subcollections ────────────────────────────────────────── - await db.recursiveDelete(userDocRef); - log_1.logger.log(`[onUserDelete] completed cleanup for user ${uid}`); -}); -async function notifyPartner(partnerId, partnerDoc, db) { - const partnerData = partnerDoc.data(); - // Preserve the original behavior: only write the in-app record + push if the partner has a - // live device token (nothing to deliver otherwise). - const tokens = await (0, push_1.getUserTokens)(db, partnerId, partnerData); - if (tokens.length === 0) - return; - // Write an in-app notification record. - await db - .collection('users') - .doc(partnerId) - .collection('notification_queue') - .add({ - type: 'partner_deleted_account', - title: 'Your partner deleted their account', - body: 'You are no longer paired. Tap to create a new invite.', - read: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }); - await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { - notification: { - title: 'Your partner deleted their account', - body: 'You are no longer paired. Tap to create a new invite.', - }, - android: { notification: { channelId: 'partner_activity' } }, // E-OBS - data: { type: 'partner_deleted_account' }, - }, partnerData); -} -//# sourceMappingURL=onUserDelete.js.map \ No newline at end of file diff --git a/functions/dist/users/onUserDelete.js.map b/functions/dist/users/onUserDelete.js.map deleted file mode 100644 index d3fe2e18..00000000 --- a/functions/dist/users/onUserDelete.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"onUserDelete.js","sourceRoot":"","sources":["../../src/users/onUserDelete.ts"],"names":[],"mappings":"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"} \ No newline at end of file