From e0c2d673737301ae349b27543dec1d6541fb3733 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 7 Jul 2026 23:58:32 -0500 Subject: [PATCH] refactor(functions): B3 migrate 12 callables to v2 + harden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate all callables off functions.https.onCall to firebase-functions/v2/https onCall: createInvite, acceptInvite, leaveCouple, submitOutcome, sendGentleReminder, sendThinkingOfYou, checkDeviceIntegrity, syncEntitlement, assignDailyQuestionCallable, wrapReleaseKey. context.auth/app → request.auth/app, data arg → request.data. The 8 client-hardcoded callable names are preserved verbatim (verified via emulator discovery). The manual `if (!request.app)` App Check check is a 1:1 port (no enforceAppCheck switch). Hardening folded in: - acceptInviteCallable: await the previously fire-and-forget partner_joined push (gen 2 freezes the instance after the response) — still swallows push errors so a failed push never fails the accept. - checkDeviceIntegrity: 10s timeout on the Play Integrity client.request so a hung upstream can't pin the instance (fail-closed catch already handles the throw); memory 512MiB. - wrapReleaseKey: memory 512MiB (tink); HttpsError swapped to v2; lazy tink require + graceful failure preserved. - Error mapping with `if (e instanceof HttpsError) throw e` re-throw guard around the risky DB sections in acceptInvite, leaveCouple, submitOutcome, sendGentleReminder, sendThinkingOfYou — raw errors map to a clean 'internal' without masking intentional codes (resource-exhausted rate limits, permission-denied, etc.). leaveCouple's best-effort recursiveDelete sweep now swallows errors (the transactional leave already succeeded). - Adopt shared sendPushToUser()/logger; remove copied token readers + plaintext token logging. Delete dead placeholder callables notifications/reminders.ts (sendDailyQuestionReminder, sendPartnerAnsweredNotification) — no client caller; wrote sent:false rows nothing consumed. Build clean; 70 tests green; discovery loads all callables as v2 in us-central1. dist rebuilt. Co-Authored-By: Claude Opus 4.8 --- functions/dist/billing/syncEntitlement.js | 48 +-- functions/dist/billing/syncEntitlement.js.map | 2 +- .../dist/couples/acceptInviteCallable.js | 81 +++-- .../dist/couples/acceptInviteCallable.js.map | 2 +- .../dist/couples/createInviteCallable.js | 30 +- .../dist/couples/createInviteCallable.js.map | 2 +- functions/dist/couples/leaveCoupleCallable.js | 82 +++-- .../dist/couples/leaveCoupleCallable.js.map | 2 +- .../dist/couples/submitOutcomeCallable.js | 127 ++++---- .../dist/couples/submitOutcomeCallable.js.map | 2 +- functions/dist/index.js | 5 +- functions/dist/index.js.map | 2 +- functions/dist/notifications/reminders.js | 102 ------ functions/dist/notifications/reminders.js.map | 1 - .../sendGentleReminderCallable.js | 189 +++++------- .../sendGentleReminderCallable.js.map | 2 +- .../sendThinkingOfYouCallable.js | 132 ++++---- .../sendThinkingOfYouCallable.js.map | 2 +- .../dist/questions/assignDailyQuestion.js | 31 +- .../dist/questions/assignDailyQuestion.js.map | 2 +- .../dist/releaseKey/wrapReleaseKeyCallable.js | 50 +-- .../releaseKey/wrapReleaseKeyCallable.js.map | 2 +- .../dist/security/checkDeviceIntegrity.js | 57 +--- .../dist/security/checkDeviceIntegrity.js.map | 2 +- functions/src/billing/syncEntitlement.ts | 21 +- functions/src/couples/acceptInviteCallable.ts | 98 +++--- functions/src/couples/createInviteCallable.ts | 30 +- functions/src/couples/leaveCoupleCallable.ts | 83 ++--- .../src/couples/submitOutcomeCallable.ts | 147 ++++----- functions/src/index.ts | 4 - functions/src/notifications/reminders.ts | 94 ------ .../sendGentleReminderCallable.ts | 240 ++++++-------- .../sendThinkingOfYouCallable.ts | 153 +++++---- .../src/questions/assignDailyQuestion.ts | 30 +- .../src/releaseKey/wrapReleaseKeyCallable.ts | 292 +++++++++--------- .../src/security/checkDeviceIntegrity.ts | 35 +-- 36 files changed, 932 insertions(+), 1252 deletions(-) delete mode 100644 functions/dist/notifications/reminders.js delete mode 100644 functions/dist/notifications/reminders.js.map delete mode 100644 functions/src/notifications/reminders.ts diff --git a/functions/dist/billing/syncEntitlement.js b/functions/dist/billing/syncEntitlement.js index 25dd6ed9..faa3a4f7 100644 --- a/functions/dist/billing/syncEntitlement.js +++ b/functions/dist/billing/syncEntitlement.js @@ -1,41 +1,9 @@ "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.syncEntitlement = void 0; -const functions = __importStar(require("firebase-functions")); +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. @@ -49,19 +17,19 @@ const entitlementLogic_1 = require("./entitlementLogic"); * state from the existing Firestore document and rewrites it, ensuring * consistent field shapes. */ -exports.syncEntitlement = functions.https.onCall(async (data, context) => { +exports.syncEntitlement = (0, https_1.onCall)(async (request) => { var _a; - if (!((_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid)) { - throw new functions.https.HttpsError('unauthenticated', 'Caller must be authenticated.'); + if (!((_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid)) { + throw new https_1.HttpsError('unauthenticated', 'Caller must be authenticated.'); } - const userId = context.auth.uid; + const userId = request.auth.uid; try { const state = await (0, entitlementLogic_1.applyEntitlementSync)(userId); return state; } catch (err) { - console.error('[syncEntitlement] failed:', err); - throw new functions.https.HttpsError('internal', 'Entitlement sync failed.'); + 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 index 5c09906c..06ac9781 100644 --- a/functions/dist/billing/syncEntitlement.js.map +++ b/functions/dist/billing/syncEntitlement.js.map @@ -1 +1 @@ -{"version":3,"file":"syncEntitlement.js","sourceRoot":"","sources":["../../src/billing/syncEntitlement.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,yDAA2E;AAE3E;;;;;;;;;;;;GAYG;AACU,QAAA,eAAe,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;;IAC5E,IAAI,CAAC,CAAA,MAAA,OAAO,CAAC,IAAI,0CAAE,GAAG,CAAA,EAAE,CAAC;QACvB,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,iBAAiB,EACjB,+BAA+B,CAChC,CAAA;IACH,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,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,CAAA;QAC/C,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,UAAU,EACV,0BAA0B,CAC3B,CAAA;IACH,CAAC;AACH,CAAC,CAAC,CAAA"} \ No newline at end of file +{"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 index 224f6d60..1f30e6db 100644 --- a/functions/dist/couples/acceptInviteCallable.js +++ b/functions/dist/couples/acceptInviteCallable.js @@ -34,9 +34,10 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.acceptInviteCallable = void 0; -const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); -const pruneTokens_1 = require("../notifications/pruneTokens"); +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. * @@ -68,18 +69,18 @@ const pruneTokens_1 = require("../notifications/pruneTokens"); 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 = functions.https.onCall(async (data, context) => { - var _a, _b, _c; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; +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 functions.https.HttpsError('unauthenticated', 'Must be signed in.'); + throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.'); + if (!request.app) { + throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); } - const code = data === null || data === void 0 ? void 0 : data.code; + const code = (_b = request.data) === null || _b === void 0 ? void 0 : _b.code; if (!code || typeof code !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'code is required.'); + 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. @@ -91,7 +92,7 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => { .count() .get(); if (recentAttempts.data().count >= ACCEPT_RATE_LIMIT_MAX) { - throw new functions.https.HttpsError('resource-exhausted', 'Too many code attempts. Try again later.'); + 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. @@ -103,15 +104,15 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => { }); // 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 functions.https.HttpsError('failed-precondition', 'Caller is already paired.'); + 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 functions.https.HttpsError('not-found', 'Invite not found.'); + throw new https_1.HttpsError('not-found', 'Invite not found.'); } - const invite = (_c = inviteDoc.data()) !== null && _c !== void 0 ? _c : {}; + const invite = (_d = inviteDoc.data()) !== null && _d !== void 0 ? _d : {}; const inviterUserId = invite.inviterUserId; const status = invite.status; const expiresAt = invite.expiresAt; @@ -120,17 +121,17 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => { const kdfParams = invite.kdfParams; const encryptedRecoveryPhrase = invite.encryptedRecoveryPhrase; if (status !== 'pending') { - throw new functions.https.HttpsError('failed-precondition', 'Invite has already been used.'); + 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 functions.https.HttpsError('failed-precondition', 'Invite has expired.'); + throw new https_1.HttpsError('failed-precondition', 'Invite has expired.'); } if (!inviterUserId) { - throw new functions.https.HttpsError('failed-precondition', 'Invite is missing inviterUserId.'); + throw new https_1.HttpsError('failed-precondition', 'Invite is missing inviterUserId.'); } if (inviterUserId === callerId) { - throw new functions.https.HttpsError('permission-denied', 'Cannot accept your own invite.'); + 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); @@ -138,7 +139,7 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => { // 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 functions.https.HttpsError('failed-precondition', 'Invite is missing encryption material. Ask your partner to create a new invite.'); + 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(); @@ -167,10 +168,19 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => { // from here on). It was only needed for the pre-accept "X invited you" preview. inviterDisplayName: admin.firestore.FieldValue.delete(), }); - await batch.commit(); - console.log(`[acceptInviteCallable] ${callerId} accepted an invite; created couple ${coupleId}`); - // Notify the inviter that their partner joined — fire-and-forget. - notifyPartnerJoined(db, inviterUserId, coupleId).catch((e) => console.warn('[acceptInviteCallable] partner_joined FCM failed:', e)); + 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, @@ -188,11 +198,7 @@ async function notifyPartnerJoined(db, inviterUserId, coupleId) { read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(), }); - const tokens = await getUserTokens(db, inviterUserId); - if (tokens.length === 0) - return; - const results = await Promise.allSettled(tokens.map((token) => admin.messaging().send({ - token, + 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.", @@ -202,23 +208,6 @@ async function notifyPartnerJoined(db, inviterUserId, coupleId) { type: 'partner_joined', couple_id: coupleId, }, - }))); - await (0, pruneTokens_1.pruneDeadTokens)(db, inviterUserId, tokens, results); -} -async function getUserTokens(db, userId) { - var _a; - const tokens = []; - const userDoc = await db.collection('users').doc(userId).get(); - const legacy = (_a = userDoc.data()) === null || _a === void 0 ? void 0 : _a.fcmToken; - if (typeof legacy === 'string' && legacy.length > 0) - tokens.push(legacy); - const snap = await db.collection('users').doc(userId).collection('fcmTokens').get(); - snap.docs.forEach((doc) => { - var _a; - const t = (_a = doc.data()) === null || _a === void 0 ? void 0 : _a.token; - if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) - tokens.push(t); }); - return tokens; } //# 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 index d017cac4..9f5c3660 100644 --- a/functions/dist/couples/acceptInviteCallable.js.map +++ b/functions/dist/couples/acceptInviteCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"acceptInviteCallable.js","sourceRoot":"","sources":["../../src/couples/acceptInviteCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"acceptInviteCallable.js","sourceRoot":"","sources":["../../src/couples/acceptInviteCallable.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 index 55ccf1d2..4f5dd0b9 100644 --- a/functions/dist/couples/createInviteCallable.js +++ b/functions/dist/couples/createInviteCallable.js @@ -34,8 +34,9 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.createInviteCallable = void 0; -const functions = __importStar(require("firebase-functions")); 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. * @@ -79,20 +80,21 @@ function generateCode() { } return code; } -exports.createInviteCallable = functions.https.onCall(async (data, context) => { +exports.createInviteCallable = (0, https_1.onCall)(async (request) => { var _a, _b, _c; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; + const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.'); + throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.'); + 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 functions.https.HttpsError('failed-precondition', 'Caller is already paired.'); + 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. @@ -106,7 +108,7 @@ exports.createInviteCallable = functions.https.onCall(async (data, context) => { .limit(RATE_LIMIT_MAX + 1); const recentInvites = await recentInvitesQuery.get(); if (recentInvites.size >= RATE_LIMIT_MAX) { - throw new functions.https.HttpsError('resource-exhausted', 'Too many invites created. Try again later.'); + 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; @@ -116,16 +118,16 @@ exports.createInviteCallable = functions.https.onCall(async (data, context) => { // 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 functions.https.HttpsError('invalid-argument', 'code is required.'); + 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 functions.https.HttpsError('invalid-argument', 'code must be 6 valid characters.'); + throw new https_1.HttpsError('invalid-argument', 'code must be 6 valid characters.'); } if (wrappedCoupleKey == null || kdfSalt == null || kdfParams == null || encryptedRecoveryPhrase == null) { - throw new functions.https.HttpsError('invalid-argument', 'E2EE fields (wrappedCoupleKey, kdfSalt, kdfParams, encryptedRecoveryPhrase) are required.'); + 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 @@ -166,7 +168,7 @@ exports.createInviteCallable = functions.https.onCall(async (data, context) => { } if (!code || !inviteRef) { // Client-supplied code collided; the Android client will retry with a new code. - throw new functions.https.HttpsError('already-exists', 'Invite code is already taken. Please try again.'); + 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. @@ -180,9 +182,9 @@ exports.createInviteCallable = functions.https.onCall(async (data, context) => { } catch (err) { // Audit write is best-effort; do not fail the invite if it errors. - console.warn(`[createInviteCallable] audit log failed for ${callerId}:`, err); + log_1.logger.warn(`[createInviteCallable] audit log failed for ${callerId}`, { error: String(err) }); } - console.log(`[createInviteCallable] ${callerId} created an invite; expires ${expiresAt.toDate().toISOString()}`); + 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 index 12d72cd8..4d324595 100644 --- a/functions/dist/couples/createInviteCallable.js.map +++ b/functions/dist/couples/createInviteCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"createInviteCallable.js","sourceRoot":"","sources":["../../src/couples/createInviteCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"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 index f9d679c4..43a6a4b2 100644 --- a/functions/dist/couples/leaveCoupleCallable.js +++ b/functions/dist/couples/leaveCoupleCallable.js @@ -34,8 +34,9 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.leaveCoupleCallable = void 0; -const functions = __importStar(require("firebase-functions")); 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. * @@ -50,14 +51,14 @@ const admin = __importStar(require("firebase-admin")); * The existing onCoupleLeave Firestore trigger fires after step 2 and handles * partner notification, so we don't duplicate that here. */ -exports.leaveCoupleCallable = functions.https.onCall(async (_data, context) => { +exports.leaveCoupleCallable = (0, https_1.onCall)(async (request) => { var _a, _b; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; + const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.'); + throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.'); + 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(); @@ -71,39 +72,54 @@ exports.leaveCoupleCallable = functions.https.onCall(async (_data, context) => { // 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. - const 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 }); + 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 }; - } - 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 functions.https.HttpsError('permission-denied', 'Not a member of this couple.'); + 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. - // Idempotent if a concurrent caller already removed them. - await db.recursiveDelete(coupleRef); - console.log(`[leaveCoupleCallable] user ${callerId} left couple ${coupleId}`); + // 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 index edba7587..e874a0f2 100644 --- a/functions/dist/couples/leaveCoupleCallable.js.map +++ b/functions/dist/couples/leaveCoupleCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"leaveCoupleCallable.js","sourceRoot":"","sources":["../../src/couples/leaveCoupleCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"leaveCoupleCallable.js","sourceRoot":"","sources":["../../src/couples/leaveCoupleCallable.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAAuC;AACvC,uDAAgE;AAChE,gCAA+B;AAE/B;;;;;;;;;;;;;GAaG;AACU,QAAA,mBAAmB,GAAG,IAAA,cAAM,EAAC,KAAK,EAAE,OAAO,EAAE,EAAE;;IAC1D,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,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IAChE,MAAM,QAAQ,GAAG,MAAA,OAAO,CAAC,IAAI,EAAE,0CAAE,QAA8B,CAAA;IAC/D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,yCAAyC;QACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IAC1B,CAAC;IAED,MAAM,SAAS,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAExD,oFAAoF;IACpF,sFAAsF;IACtF,mFAAmF;IACnF,2EAA2E;IAC3E,IAAI,MAA+B,CAAA;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;;YAC5C,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAC1C,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;gBACvB,MAAM,SAAS,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;gBACtD,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;gBAC1C,IAAI,CAAA,MAAA,UAAU,CAAC,IAAI,EAAE,0CAAE,QAAQ,MAAK,QAAQ,EAAE,CAAC;oBAC7C,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;gBAC1C,CAAC;gBACD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;YAC7B,CAAC;YAED,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,UAAU,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;YAC9D,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAChC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAA;YAC9B,CAAC;YAED,2EAA2E;YAC3E,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,GAAG,CACnC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAC9D,CAAA;YACD,WAAW,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;;gBAC9B,IAAI,CAAA,MAAA,IAAI,CAAC,IAAI,EAAE,0CAAE,QAAQ,MAAK,QAAQ,EAAE,CAAC;oBACvC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;gBACvE,CAAC;YACH,CAAC,CAAC,CAAA;YACF,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;YACpB,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;QAC7B,CAAC,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,YAAY,kBAAU;YAAE,MAAM,CAAC,CAAA;QACpC,YAAM,CAAC,KAAK,CAAC,gDAAgD,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QACpF,MAAM,IAAI,kBAAU,CAAC,UAAU,EAAE,2CAA2C,CAAC,CAAA;IAC/E,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACvB,MAAM,IAAI,kBAAU,CAAC,mBAAmB,EAAE,8BAA8B,CAAC,CAAA;IAC3E,CAAC;IAED,kFAAkF;IAClF,qFAAqF;IACrF,mFAAmF;IACnF,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,eAAe,CAAC,SAAS,CAAC,CAAA;IACrC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,YAAM,CAAC,IAAI,CAAC,4DAA4D,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IACjG,CAAC;IAED,YAAM,CAAC,GAAG,CAAC,8BAA8B,QAAQ,gBAAgB,QAAQ,EAAE,CAAC,CAAA;IAC5E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/functions/dist/couples/submitOutcomeCallable.js b/functions/dist/couples/submitOutcomeCallable.js index cfc02408..43bea593 100644 --- a/functions/dist/couples/submitOutcomeCallable.js +++ b/functions/dist/couples/submitOutcomeCallable.js @@ -34,8 +34,9 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.submitOutcomeCallable = void 0; -const functions = __importStar(require("firebase-functions")); 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; @@ -56,89 +57,99 @@ function isValidScoreMap(value) { } return true; } -exports.submitOutcomeCallable = functions.https.onCall(async (data, context) => { +exports.submitOutcomeCallable = (0, https_1.onCall)(async (request) => { var _a, _b, _c; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; + const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.'); + throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.'); + 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 functions.https.HttpsError('invalid-argument', 'coupleId is required.'); + 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 functions.https.HttpsError('invalid-argument', `dayKey must be one of: ${DAY_KEYS.join(', ')}.`); + 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 functions.https.HttpsError('invalid-argument', `scores must contain ${SCORE_KEYS.join(', ')} with values ${MIN_SCORE}-${MAX_SCORE}.`); + 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 functions.https.HttpsError('not-found', 'Couple not found.'); + 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 functions.https.HttpsError('permission-denied', 'Caller is not a member of this couple.'); + 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); - const 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 functions.https.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); + 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.'); } - 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 }; - }); - console.log(`[submitOutcomeCallable] ${callerId} submitted ${dayKey} for couple ${coupleId}`); + 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 index ea74fdfe..e9154a71 100644 --- a/functions/dist/couples/submitOutcomeCallable.js.map +++ b/functions/dist/couples/submitOutcomeCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"submitOutcomeCallable.js","sourceRoot":"","sources":["../../src/couples/submitOutcomeCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"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/index.js b/functions/dist/index.js index da88d880..4cf6b871 100644 --- a/functions/dist/index.js +++ b/functions/dist/index.js @@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -exports.wrapReleaseKeyCallable = exports.onGamePartFinished = exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.aggregateOutcomeStats = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerRevealed = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = 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.sendPartnerAnsweredNotification = exports.sendDailyQuestionReminder = exports.onEntitlementChanged = exports.syncEntitlement = exports.revenueCatWebhook = void 0; +exports.wrapReleaseKeyCallable = exports.onGamePartFinished = exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.aggregateOutcomeStats = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerRevealed = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = 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"); @@ -50,9 +50,6 @@ 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 reminders_1 = require("./notifications/reminders"); -Object.defineProperty(exports, "sendDailyQuestionReminder", { enumerable: true, get: function () { return reminders_1.sendDailyQuestionReminder; } }); -Object.defineProperty(exports, "sendPartnerAnsweredNotification", { enumerable: true, get: function () { return reminders_1.sendPartnerAnsweredNotification; } }); var sendGentleReminderCallable_1 = require("./notifications/sendGentleReminderCallable"); Object.defineProperty(exports, "sendGentleReminderCallable", { enumerable: true, get: function () { return sendGentleReminderCallable_1.sendGentleReminderCallable; } }); var sendThinkingOfYouCallable_1 = require("./notifications/sendThinkingOfYouCallable"); diff --git a/functions/dist/index.js.map b/functions/dist/index.js.map index c3eb138e..69e1a50b 100644 --- a/functions/dist/index.js.map +++ b/functions/dist/index.js.map @@ -1 +1 @@ -{"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,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,uDAGkC;AAFhC,sHAAA,yBAAyB,OAAA;AACzB,4HAAA,+BAA+B,OAAA;AAEjC,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,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,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,mEAAqF;AAA5E,0HAAA,mBAAmB,OAAA;AAAE,yHAAA,kBAAkB,OAAA;AAEhD,8EAA4E;AAAnE,gIAAA,sBAAsB,OAAA;AAE/B,oFAAoF;AACpF,uEAAuE;AACvE,iFAAiF;AACjF,0DAA0D"} \ No newline at end of file +{"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,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,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,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,mEAAqF;AAA5E,0HAAA,mBAAmB,OAAA;AAAE,yHAAA,kBAAkB,OAAA;AAEhD,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/notifications/reminders.js b/functions/dist/notifications/reminders.js deleted file mode 100644 index 62c9b5e0..00000000 --- a/functions/dist/notifications/reminders.js +++ /dev/null @@ -1,102 +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.sendPartnerAnsweredNotification = exports.sendDailyQuestionReminder = void 0; -const functions = __importStar(require("firebase-functions")); -const admin = __importStar(require("firebase-admin")); -/** - * Callable function that queues a daily-question reminder. - * - * Expected body: { userId: string } - * Auth context supplies the caller uid; the request `userId` must match - * the caller to prevent cross-user notification spam. - * - * This is a placeholder scheduler. The actual daily scheduling will be - * handled by a Firestore trigger / Cloud Scheduler integration later. - */ -exports.sendDailyQuestionReminder = functions.https.onCall(async (data, context) => { - var _a, _b, _c; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Caller must be authenticated.'); - } - const userId = data.userId; - if (!userId || typeof userId !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'userId is required.'); - } - // Users may only request reminders for themselves. - if (userId !== callerId) { - throw new functions.https.HttpsError('permission-denied', 'Cannot send notifications to another user.'); - } - await writeNotificationRecord(userId, 'daily_question', { - title: (_b = data.title) !== null && _b !== void 0 ? _b : 'Your daily question is waiting!', - body: (_c = data.body) !== null && _c !== void 0 ? _c : "Tap to answer today's question together.", - }); - return { queued: true, type: 'daily_question' }; -}); -/** - * Callable function that queues a partner-answered notification. - * - * Expected body: { userId: string } - * Auth context must match the request `userId`. - */ -exports.sendPartnerAnsweredNotification = functions.https.onCall(async (data, context) => { - var _a, _b, _c; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; - if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Caller must be authenticated.'); - } - const userId = data.userId; - if (!userId || typeof userId !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'userId is required.'); - } - if (userId !== callerId) { - throw new functions.https.HttpsError('permission-denied', 'Cannot send notifications to another user.'); - } - await writeNotificationRecord(userId, 'partner_answered', { - title: (_b = data.title) !== null && _b !== void 0 ? _b : 'Your partner just answered!', - body: (_c = data.body) !== null && _c !== void 0 ? _c : 'See what your partner shared.', - }); - return { queued: true, type: 'partner_answered' }; -}); -async function writeNotificationRecord(userId, type, message) { - const db = admin.firestore(); - await db - .collection('users') - .doc(userId) - .collection('notification_queue') - .add(Object.assign(Object.assign({ type }, message), { read: false, sent: false, createdAt: admin.firestore.FieldValue.serverTimestamp() })); -} -//# sourceMappingURL=reminders.js.map \ No newline at end of file diff --git a/functions/dist/notifications/reminders.js.map b/functions/dist/notifications/reminders.js.map deleted file mode 100644 index 58879517..00000000 --- a/functions/dist/notifications/reminders.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"reminders.js","sourceRoot":"","sources":["../../src/notifications/reminders.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 index bda92fc9..0a80694d 100644 --- a/functions/dist/notifications/sendGentleReminderCallable.js +++ b/functions/dist/notifications/sendGentleReminderCallable.js @@ -34,9 +34,10 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.sendGentleReminderCallable = void 0; -const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); -const pruneTokens_1 = require("./pruneTokens"); +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 /** @@ -55,122 +56,100 @@ const GENTLE_REMINDER_WINDOW_MS = 60 * 60 * 1000; // 1 hour * 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 = functions.https.onCall(async (_data, context) => { - var _a, _b, _c, _d, _e; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; +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 functions.https.HttpsError('unauthenticated', 'Must be signed in.'); + throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.'); + 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 functions.https.HttpsError('failed-precondition', 'Not in a couple.'); + 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 functions.https.HttpsError('not-found', 'Couple not found.'); + 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 functions.https.HttpsError('failed-precondition', 'No partner found.'); + throw new https_1.HttpsError('failed-precondition', 'No partner found.'); } - // ── 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. + 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.`); } - 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 }; + // ── 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' }; } - tx.set(rateLimitRef, { - count: count + 1, - windowStart, - updatedAt: now, - }, { merge: true }); - return { allowed: true, count: count + 1, windowStart }; - }); - if (!throttleResult.allowed) { - throw new functions.https.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. Collect partner FCM tokens ──────────────────────────────────────── - const tokens = []; - const partnerDoc = await db.collection('users').doc(partnerId).get(); - if (partnerDoc.exists) { - const legacyToken = (_e = partnerDoc.data()) === null || _e === void 0 ? void 0 : _e.fcmToken; - if (typeof legacyToken === 'string' && legacyToken.length > 0) { - tokens.push(legacyToken); - } - } - const tokenSnap = await db - .collection('users') - .doc(partnerId) - .collection('fcmTokens') - .get(); - tokenSnap.docs.forEach((doc) => { - var _a; - const t = (_a = doc.data()) === null || _a === void 0 ? void 0 : _a.token; - if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) { - tokens.push(t); - } - }); - // ── 5. 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(), - }); - // ── 6. Claim the daily rate-limit lock ─────────────────────────────────── - await lockRef.set({ - sentBy: callerId, - sentAt: admin.firestore.FieldValue.serverTimestamp(), - }); - // ── 7. Send FCM push ───────────────────────────────────────────────────── - if (tokens.length > 0) { - const sendResults = await Promise.allSettled(tokens.map((token) => admin.messaging().send({ - token, + // ── 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.", @@ -180,15 +159,15 @@ exports.sendGentleReminderCallable = functions.https.onCall(async (_data, contex type: 'gentle_reminder', couple_id: coupleId, }, - }))); - sendResults.forEach((result, i) => { - if (result.status === 'rejected') { - console.warn(`[sendGentleReminderCallable] FCM failed for token ${tokens[i]}:`, result.reason); - } }); - await (0, pruneTokens_1.pruneDeadTokens)(db, partnerId, tokens, sendResults); + 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.'); } - console.log(`[sendGentleReminderCallable] reminder sent from ${callerId} to ${partnerId} in couple ${coupleId}`); - return { sent: true }; }); //# 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 index 3f5ae24d..3ea13369 100644 --- a/functions/dist/notifications/sendGentleReminderCallable.js.map +++ b/functions/dist/notifications/sendGentleReminderCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"sendGentleReminderCallable.js","sourceRoot":"","sources":["../../src/notifications/sendGentleReminderCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"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 index a50ae11b..03152748 100644 --- a/functions/dist/notifications/sendThinkingOfYouCallable.js +++ b/functions/dist/notifications/sendThinkingOfYouCallable.js @@ -34,10 +34,11 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.sendThinkingOfYouCallable = void 0; -const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); +const https_1 = require("firebase-functions/v2/https"); const quietHours_1 = require("./quietHours"); -const pruneTokens_1 = require("./pruneTokens"); +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 /** @@ -51,97 +52,86 @@ const THINKING_OF_YOU_WINDOW_MS = 24 * 60 * 60 * 1000; // rolling 24h * * Writes a partner-safe `notification_queue` entry (in-app + the "Together" feed) and an FCM push. */ -exports.sendThinkingOfYouCallable = functions.https.onCall(async (_data, context) => { - var _a, _b, _c, _d, _e; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; +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 functions.https.HttpsError('unauthenticated', 'Must be signed in.'); + throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.'); + 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 functions.https.HttpsError('failed-precondition', 'Not in a couple.'); + 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 functions.https.HttpsError('not-found', 'Couple not found.'); + 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 functions.https.HttpsError('failed-precondition', 'No partner found.'); + throw new https_1.HttpsError('failed-precondition', 'No partner found.'); } - // ── 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; + 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."); } - if (count >= THINKING_OF_YOU_MAX_PER_DAY) { - return { allowed: false }; + // ── 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 }; } - tx.set(rateLimitRef, { count: count + 1, windowStart, updatedAt: now }, { merge: true }); - return { allowed: true }; - }); - if (!throttle.allowed) { - throw new functions.https.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(); - if ((0, quietHours_1.recipientInQuietHours)(partnerDoc.data())) { - console.log(`[sendThinkingOfYouCallable] ${partnerId} in quiet hours — push suppressed (in-app kept)`); - return { sent: true }; - } - // ── FCM push ───────────────────────────────────────────────────────────── - const tokens = []; - const legacy = (_e = partnerDoc.data()) === null || _e === void 0 ? void 0 : _e.fcmToken; - if (typeof legacy === 'string' && legacy.length > 0) - tokens.push(legacy); - const tokenSnap = await db.collection('users').doc(partnerId).collection('fcmTokens').get(); - tokenSnap.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); - }); - if (tokens.length > 0) { - const results = await Promise.allSettled(tokens.map((token) => admin.messaging().send({ - token, + // ── 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 }, - }))); - results.forEach((r, i) => { - if (r.status === 'rejected') { - console.warn(`[sendThinkingOfYouCallable] FCM failed for token ${tokens[i]}:`, r.reason); - } - }); - await (0, pruneTokens_1.pruneDeadTokens)(db, partnerId, tokens, results); + }, 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.'); } - console.log(`[sendThinkingOfYouCallable] sent from ${callerId} to ${partnerId} in couple ${coupleId}`); - return { sent: true }; }); //# 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 index 05171a8b..07c273f8 100644 --- a/functions/dist/notifications/sendThinkingOfYouCallable.js.map +++ b/functions/dist/notifications/sendThinkingOfYouCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"sendThinkingOfYouCallable.js","sourceRoot":"","sources":["../../src/notifications/sendThinkingOfYouCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"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/questions/assignDailyQuestion.js b/functions/dist/questions/assignDailyQuestion.js index 431806c7..6d09d9e2 100644 --- a/functions/dist/questions/assignDailyQuestion.js +++ b/functions/dist/questions/assignDailyQuestion.js @@ -37,9 +37,9 @@ exports.assignDailyQuestionCallable = exports.assignDailyQuestion = void 0; exports.formatCstDate = formatCstDate; exports.parseCstDate = parseCstDate; exports.timestampAt6PmCst = timestampAt6PmCst; -const functions = __importStar(require("firebase-functions")); 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. @@ -101,28 +101,29 @@ exports.assignDailyQuestion = (0, scheduler_1.onSchedule)({ schedule: '0 23 * * * * Body: { coupleId: string, date?: string } */ -exports.assignDailyQuestionCallable = functions.https.onCall(async (data, context) => { +exports.assignDailyQuestionCallable = (0, https_1.onCall)(async (request) => { var _a, _b, _c, _d; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; + const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Caller must be authenticated.'); + throw new https_1.HttpsError('unauthenticated', 'Caller must be authenticated.'); } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.'); + 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 functions.https.HttpsError('invalid-argument', 'coupleId is required.'); + 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 functions.https.HttpsError('not-found', 'Couple not found.'); + 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 functions.https.HttpsError('permission-denied', 'Caller is not a couple member.'); + 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 @@ -131,14 +132,14 @@ exports.assignDailyQuestionCallable = functions.https.onCall(async (data, contex 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 functions.https.HttpsError('invalid-argument', 'date must be YYYY-MM-DD.'); + throw new https_1.HttpsError('invalid-argument', 'date must be YYYY-MM-DD.'); } if (date !== today) { - throw new functions.https.HttpsError('invalid-argument', 'Daily question can only be assigned for 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 functions.https.HttpsError('internal', 'No active questions available.'); + throw new https_1.HttpsError('internal', 'No active questions available.'); } const nextDay = nextCstDateString(date); const docRef = db @@ -155,10 +156,12 @@ exports.assignDailyQuestionCallable = functions.https.onCall(async (data, contex }); } 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 functions.https.HttpsError('already-exists', `Daily question already assigned for ${date}.`); + throw new https_1.HttpsError('already-exists', `Daily question already assigned for ${date}.`); } - throw new functions.https.HttpsError('internal', 'Failed to assign daily question.'); + throw new https_1.HttpsError('internal', 'Failed to assign daily question.'); } return { success: true, coupleId, date, questionId }; }); diff --git a/functions/dist/questions/assignDailyQuestion.js.map b/functions/dist/questions/assignDailyQuestion.js.map index d970bdc6..9e23d263 100644 --- a/functions/dist/questions/assignDailyQuestion.js.map +++ b/functions/dist/questions/assignDailyQuestion.js.map @@ -1 +1 @@ -{"version":3,"file":"assignDailyQuestion.js","sourceRoot":"","sources":["../../src/questions/assignDailyQuestion.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"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/releaseKey/wrapReleaseKeyCallable.js b/functions/dist/releaseKey/wrapReleaseKeyCallable.js index a29da85d..f4cbf1c2 100644 --- a/functions/dist/releaseKey/wrapReleaseKeyCallable.js +++ b/functions/dist/releaseKey/wrapReleaseKeyCallable.js @@ -34,25 +34,27 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.wrapReleaseKeyCallable = void 0; -const functions = __importStar(require("firebase-functions")); 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 = functions.https.onCall(async (data, context) => { +exports.wrapReleaseKeyCallable = (0, https_1.onCall)({ memory: '512MiB' }, async (request) => { var _a, _b, _c, _d, _e; - const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; + const callerId = (_a = request.auth) === null || _a === void 0 ? void 0 : _a.uid; if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.'); + throw new https_1.HttpsError('unauthenticated', 'Must be signed in.'); } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.'); + 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 functions.https.HttpsError('invalid-argument', 'recipientUserId is required.'); + throw new https_1.HttpsError('invalid-argument', 'recipientUserId is required.'); } if (!oneTimeKeyB64 || typeof oneTimeKeyB64 !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'oneTimeKey is required.'); + 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; @@ -60,40 +62,40 @@ exports.wrapReleaseKeyCallable = functions.https.onCall(async (data, context) => oneTimeKey = Buffer.from(oneTimeKeyB64, 'base64'); } catch (_f) { - throw new functions.https.HttpsError('invalid-argument', 'oneTimeKey is not valid base64.'); + throw new https_1.HttpsError('invalid-argument', 'oneTimeKey is not valid base64.'); } if (oneTimeKey.length !== 32) { - throw new functions.https.HttpsError('invalid-argument', `oneTimeKey must be 32 bytes; got ${oneTimeKey.length}.`); + 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 functions.https.HttpsError('failed-precondition', 'Caller is not paired.'); + 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 functions.https.HttpsError('not-found', 'Couple not found.'); + 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 functions.https.HttpsError('permission-denied', 'Caller is not a member of this couple.'); + throw new https_1.HttpsError('permission-denied', 'Caller is not a member of this couple.'); } if (!coupleUserIds.includes(recipientUserId)) { - throw new functions.https.HttpsError('permission-denied', 'recipientUserId is not the caller\'s partner.'); + throw new https_1.HttpsError('permission-denied', 'recipientUserId is not the caller\'s partner.'); } if (recipientUserId === callerId) { - throw new functions.https.HttpsError('permission-denied', 'Cannot wrap a release key for yourself.'); + 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 functions.https.HttpsError('failed-precondition', 'Recipient has not registered a release-key device. Ask them to open the app first.'); + 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 functions.https.HttpsError('failed-precondition', 'Recipient device is missing a public key.'); + 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 @@ -105,7 +107,7 @@ exports.wrapReleaseKeyCallable = functions.https.onCall(async (data, context) => tinkAead = tink.aead; } catch (_g) { - throw new functions.https.HttpsError('internal', 'Tink crypto library is not available on the server.'); + 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; @@ -115,8 +117,8 @@ exports.wrapReleaseKeyCallable = functions.https.onCall(async (data, context) => hybridEncrypt = publicHandle.getPrimitive(tinkAead.hybrid.HybridEncrypt); } catch (err) { - console.warn(`[wrapReleaseKeyCallable] public key parse failed for ${recipientUserId}:`, err); - throw new functions.https.HttpsError('failed-precondition', 'Recipient public key could not be parsed.'); + 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; @@ -125,8 +127,8 @@ exports.wrapReleaseKeyCallable = functions.https.onCall(async (data, context) => ciphertext = Buffer.from(hybridEncrypt.encrypt(oneTimeKey, Buffer.from(aad, 'utf-8'))); } catch (err) { - console.warn(`[wrapReleaseKeyCallable] wrap failed for ${recipientUserId}:`, err); - throw new functions.https.HttpsError('internal', 'Failed to wrap release key.'); + 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. @@ -144,7 +146,7 @@ exports.wrapReleaseKeyCallable = functions.https.onCall(async (data, context) => await db.collection('users').doc(callerId).collection('notification_queue').add(Object.assign(Object.assign({}, auditFields), { type: 'wrap_release_key', read: true })); } catch (err) { - console.warn(`[wrapReleaseKeyCallable] audit log failed for ${callerId}:`, 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. @@ -154,7 +156,7 @@ exports.wrapReleaseKeyCallable = functions.https.onCall(async (data, context) => ciphertext: keyboxB64, mac: '', }; - console.log(`[wrapReleaseKeyCallable] ${callerId} wrapped release key for ${recipientUserId} in couple ${coupleId}`); + 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 index d8ae0f69..7147cca8 100644 --- a/functions/dist/releaseKey/wrapReleaseKeyCallable.js.map +++ b/functions/dist/releaseKey/wrapReleaseKeyCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"wrapReleaseKeyCallable.js","sourceRoot":"","sources":["../../src/releaseKey/wrapReleaseKeyCallable.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AA8CvC,MAAM,WAAW,GAAG,oBAAoB,CAAA;AAE3B,QAAA,sBAAsB,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,IAA6B,EAAE,OAAO,EAAE,EAAE;;IAC5G,MAAM,QAAQ,GAAG,MAAA,OAAO,CAAC,IAAI,0CAAE,GAAG,CAAA;IAClC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,CAAA;IAC/E,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACjB,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,qBAAqB,EAAE,kCAAkC,CAAC,CAAA;IACjG,CAAC;IAED,MAAM,eAAe,GAAG,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,eAAe,CAAA;IAC7C,MAAM,aAAa,GAAG,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,UAAU,CAAA;IACtC,IAAI,CAAC,eAAe,IAAI,OAAO,eAAe,KAAK,QAAQ,EAAE,CAAC;QAC5D,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,kBAAkB,EAAE,8BAA8B,CAAC,CAAA;IAC1F,CAAC;IACD,IAAI,CAAC,aAAa,IAAI,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC;QACxD,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,kBAAkB,EAAE,yBAAyB,CAAC,CAAA;IACrF,CAAC;IAED,0EAA0E;IAC1E,IAAI,UAAkB,CAAA;IACtB,IAAI,CAAC;QACH,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAA;IACnD,CAAC;IAAC,WAAM,CAAC;QACP,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,kBAAkB,EAAE,iCAAiC,CAAC,CAAA;IAC7F,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QAC7B,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,kBAAkB,EAClB,oCAAoC,UAAU,CAAC,MAAM,GAAG,CACzD,CAAA;IACH,CAAC;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,sEAAsE;IACtE,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IAClE,MAAM,QAAQ,GAAG,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,QAA8B,CAAA;IACjE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,qBAAqB,EAAE,uBAAuB,CAAC,CAAA;IACtF,CAAC;IAED,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,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAA;IACxE,CAAC;IACD,MAAM,aAAa,GAAG,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAA+B,CAAA;IACvE,IAAI,CAAC,aAAa,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,mBAAmB,EAAE,wCAAwC,CAAC,CAAA;IACrG,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,mBAAmB,EACnB,+CAA+C,CAChD,CAAA;IACH,CAAC;IACD,IAAI,eAAe,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,mBAAmB,EAAE,yCAAyC,CAAC,CAAA;IACtG,CAAC;IAED,iFAAiF;IACjF,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IAC9G,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACtB,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,qBAAqB,EACrB,oFAAoF,CACrF,CAAA;IACH,CAAC;IACD,MAAM,YAAY,GAAG,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,SAA+B,CAAA;IACtE,IAAI,CAAC,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;QACtD,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,qBAAqB,EACrB,2CAA2C,CAC5C,CAAA;IACH,CAAC;IAED,uFAAuF;IACvF,0FAA0F;IAC1F,wCAAwC;IACxC,IAAI,QAAa,CAAA;IACjB,IAAI,CAAC;QACH,8DAA8D;QAC9D,MAAM,IAAI,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAA;QAChD,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAA;IACtB,CAAC;IAAC,WAAM,CAAC;QACP,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,EAAE,qDAAqD,CAAC,CAAA;IACzG,CAAC;IAED,6EAA6E;IAC7E,IAAI,aAAkB,CAAA;IACtB,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,WAAW,CAAC,CAAA;QACnF,MAAM,YAAY,GAAG,QAAQ,CAAC,qBAAqB,CAAC,IAAI,CACtD,QAAQ,CAAC,gBAAgB,CAAC,SAAS,CAAC,YAAY,CAAC,CAClD,CAAA;QACD,aAAa,GAAG,YAAY,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IAC1E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CAAC,wDAAwD,eAAe,GAAG,EAAE,GAAG,CAAC,CAAA;QAC7F,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,qBAAqB,EACrB,2CAA2C,CAC5C,CAAA;IACH,CAAC;IAED,yBAAyB;IACzB,MAAM,GAAG,GAAG,MAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,GAA0B,mCAAI,WAAW,CAAA;IAC5D,IAAI,UAAkB,CAAA;IACtB,IAAI,CAAC;QACH,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACxF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CAAC,4CAA4C,eAAe,GAAG,EAAE,GAAG,CAAC,CAAA;QACjF,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,EAAE,6BAA6B,CAAC,CAAA;IACjF,CAAC;IAED,qFAAqF;IACrF,wEAAwE;IACxE,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;IAElD,MAAM,WAAW,GAAG;QAClB,MAAM,EAAE,kBAAkB;QAC1B,KAAK,EAAE,QAAQ;QACf,SAAS,EAAE,eAAe;QAC1B,QAAQ;QACR,GAAG;QACH,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KACxD,CAAA;IAED,gEAAgE;IAChE,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC,GAAG,iCAC1E,WAAW,KACd,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE,IAAI,IACV,CAAA;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CAAC,iDAAiD,QAAQ,GAAG,EAAE,GAAG,CAAC,CAAA;IACjF,CAAC;IAED,wFAAwF;IACxF,wEAAwE;IACxE,MAAM,QAAQ,GAA2B;QACvC,MAAM,EAAE,aAAa,SAAS,EAAE;QAChC,kBAAkB,EAAE,EAAE;QACtB,UAAU,EAAE,SAAS;QACrB,GAAG,EAAE,EAAE;KACR,CAAA;IAED,OAAO,CAAC,GAAG,CACT,4BAA4B,QAAQ,4BAA4B,eAAe,cAAc,QAAQ,EAAE,CACxG,CAAA;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAC,CAAA"} \ No newline at end of file +{"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 index 20d1a9a2..d483c8a4 100644 --- a/functions/dist/security/checkDeviceIntegrity.js +++ b/functions/dist/security/checkDeviceIntegrity.js @@ -1,43 +1,14 @@ "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.checkDeviceIntegrity = void 0; -const functions = __importStar(require("firebase-functions")); +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. @@ -58,16 +29,17 @@ const PLAY_INTEGRITY_URL = `https://playintegrity.googleapis.com/v1/${PACKAGE_NA * (returns passed: false). Configure the API and grant the service account * the "Play Integrity API User" IAM role before deploying to production. */ -exports.checkDeviceIntegrity = functions.https.onCall(async (data, context) => { - if (!context.auth) { - throw new functions.https.HttpsError('unauthenticated', 'Caller must be authenticated.'); +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 (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.'); + if (!request.app) { + throw new https_1.HttpsError('failed-precondition', 'App Check verification required.'); } - const token = data === null || data === void 0 ? void 0 : data.token; + const token = (_a = request.data) === null || _a === void 0 ? void 0 : _a.token; if (!token || typeof token !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'token is required.'); + throw new https_1.HttpsError('invalid-argument', 'token is required.'); } try { const verdicts = await decodeIntegrityToken(token); @@ -76,7 +48,7 @@ exports.checkDeviceIntegrity = functions.https.onCall(async (data, context) => { return { passed, verdicts }; } catch (err) { - console.error('[checkDeviceIntegrity] verification failed:', 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. @@ -93,6 +65,7 @@ async function decodeIntegrityToken(token) { 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 : []); } diff --git a/functions/dist/security/checkDeviceIntegrity.js.map b/functions/dist/security/checkDeviceIntegrity.js.map index 1ea04cde..658163bb 100644 --- a/functions/dist/security/checkDeviceIntegrity.js.map +++ b/functions/dist/security/checkDeviceIntegrity.js.map @@ -1 +1 @@ -{"version":3,"file":"checkDeviceIntegrity.js","sourceRoot":"","sources":["../../src/security/checkDeviceIntegrity.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,6DAAgD;AAEhD,MAAM,YAAY,GAAG,YAAY,CAAA;AACjC,MAAM,kBAAkB,GACtB,2CAA2C,YAAY,uBAAuB,CAAA;AAEhF;;;;;;;;;;;;;;;;;;;GAmBG;AACU,QAAA,oBAAoB,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,CACxD,KAAK,EAAE,IAAwB,EAAE,OAAO,EAAE,EAAE;IAC1C,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAClB,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,iBAAiB,EACjB,+BAA+B,CAChC,CAAA;IACH,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACjB,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,qBAAqB,EACrB,kCAAkC,CACnC,CAAA;IACH,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,KAAK,CAAA;IACzB,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxC,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAClC,kBAAkB,EAClB,oBAAoB,CACrB,CAAA;IACH,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,OAAO,CAAC,KAAK,CAAC,6CAA6C,EAAE,GAAG,CAAC,CAAA;QACjE,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;KACjC,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 +{"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/src/billing/syncEntitlement.ts b/functions/src/billing/syncEntitlement.ts index 26cf9e40..4d276e03 100644 --- a/functions/src/billing/syncEntitlement.ts +++ b/functions/src/billing/syncEntitlement.ts @@ -1,5 +1,6 @@ -import * as functions from 'firebase-functions' +import { onCall, HttpsError } from 'firebase-functions/v2/https' import { applyEntitlementSync, EntitlementState } from './entitlementLogic' +import { logger } from '../log' /** * Callable function that forces a re-sync of entitlements for the @@ -14,24 +15,18 @@ import { applyEntitlementSync, EntitlementState } from './entitlementLogic' * state from the existing Firestore document and rewrites it, ensuring * consistent field shapes. */ -export const syncEntitlement = functions.https.onCall(async (data, context) => { - if (!context.auth?.uid) { - throw new functions.https.HttpsError( - 'unauthenticated', - 'Caller must be authenticated.' - ) +export const syncEntitlement = onCall(async (request) => { + if (!request.auth?.uid) { + throw new HttpsError('unauthenticated', 'Caller must be authenticated.') } - const userId = context.auth.uid + const userId = request.auth.uid try { const state = await applyEntitlementSync(userId) return state satisfies EntitlementState } catch (err) { - console.error('[syncEntitlement] failed:', err) - throw new functions.https.HttpsError( - 'internal', - 'Entitlement sync failed.' - ) + logger.error('[syncEntitlement] failed', { error: String(err) }) + throw new HttpsError('internal', 'Entitlement sync failed.') } }) diff --git a/functions/src/couples/acceptInviteCallable.ts b/functions/src/couples/acceptInviteCallable.ts index 77239629..d8c9e311 100644 --- a/functions/src/couples/acceptInviteCallable.ts +++ b/functions/src/couples/acceptInviteCallable.ts @@ -1,6 +1,7 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { pruneDeadTokens } from '../notifications/pruneTokens' +import { onCall, HttpsError, CallableRequest } from 'firebase-functions/v2/https' +import { sendPushToUser } from '../notifications/push' +import { logger } from '../log' /** * HTTPS callable that mediates invite acceptance. @@ -34,18 +35,18 @@ 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 -export const acceptInviteCallable = functions.https.onCall(async (data: Record, context) => { - const callerId = context.auth?.uid +export const acceptInviteCallable = onCall(async (request: CallableRequest>) => { + const callerId = request.auth?.uid if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.') + throw new HttpsError('unauthenticated', 'Must be signed in.') } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.') + if (!request.app) { + throw new HttpsError('failed-precondition', 'App Check verification required.') } - const code = data?.code + const code = request.data?.code if (!code || typeof code !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'code is required.') + throw new HttpsError('invalid-argument', 'code is required.') } const db = admin.firestore() @@ -61,7 +62,7 @@ export const acceptInviteCallable = functions.https.onCall(async (data: Record= ACCEPT_RATE_LIMIT_MAX) { - throw new functions.https.HttpsError('resource-exhausted', 'Too many code attempts. Try again later.') + throw new 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. @@ -77,14 +78,14 @@ export const acceptInviteCallable = functions.https.onCall(async (data: Record - console.warn('[acceptInviteCallable] partner_joined FCM failed:', e) + // 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) => + logger.warn('[acceptInviteCallable] partner_joined FCM failed', { error: String(e) }) ) return { @@ -189,41 +197,15 @@ async function notifyPartnerJoined( createdAt: admin.firestore.FieldValue.serverTimestamp(), }) - const tokens = await getUserTokens(db, inviterUserId) - if (tokens.length === 0) return - - const results = await Promise.allSettled( - tokens.map((token) => - admin.messaging().send({ - token, - 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, - }, - }) - ) - ) - await pruneDeadTokens(db, inviterUserId, tokens, results) -} - -async function getUserTokens( - db: admin.firestore.Firestore, - userId: string -): Promise { - const tokens: string[] = [] - const userDoc = await db.collection('users').doc(userId).get() - const legacy = userDoc.data()?.fcmToken - if (typeof legacy === 'string' && legacy.length > 0) tokens.push(legacy) - - const snap = await db.collection('users').doc(userId).collection('fcmTokens').get() - snap.docs.forEach((doc) => { - const t = doc.data()?.token - if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) tokens.push(t) + await 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, + }, }) - return tokens } diff --git a/functions/src/couples/createInviteCallable.ts b/functions/src/couples/createInviteCallable.ts index 7bf2a92d..491e86f6 100644 --- a/functions/src/couples/createInviteCallable.ts +++ b/functions/src/couples/createInviteCallable.ts @@ -1,5 +1,6 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { onCall, HttpsError, CallableRequest } from 'firebase-functions/v2/https' +import { logger } from '../log' /** * HTTPS callable that creates a secure invite code. @@ -47,21 +48,22 @@ function generateCode(): string { return code } -export const createInviteCallable = functions.https.onCall(async (data: Record, context) => { - const callerId = context.auth?.uid +export const createInviteCallable = onCall(async (request: CallableRequest>) => { + const callerId = request.auth?.uid if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.') + throw new HttpsError('unauthenticated', 'Must be signed in.') } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.') + if (!request.app) { + throw new 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 && callerDoc.data()?.coupleId != null) { - throw new functions.https.HttpsError('failed-precondition', 'Caller is already paired.') + throw new HttpsError('failed-precondition', 'Caller is already paired.') } const callerDisplayName = callerDoc.data()?.displayName as string | undefined @@ -78,7 +80,7 @@ export const createInviteCallable = functions.https.onCall(async (data: Record= RATE_LIMIT_MAX) { - throw new functions.https.HttpsError('resource-exhausted', 'Too many invites created. Try again later.') + throw new HttpsError('resource-exhausted', 'Too many invites created. Try again later.') } const clientCode = data?.code as string | undefined @@ -90,16 +92,16 @@ export const createInviteCallable = functions.https.onCall(async (data: Record { - const callerId = context.auth?.uid +export const leaveCoupleCallable = onCall(async (request) => { + const callerId = request.auth?.uid if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.') + throw new HttpsError('unauthenticated', 'Must be signed in.') } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.') + if (!request.app) { + throw new HttpsError('failed-precondition', 'App Check verification required.') } const db = admin.firestore() @@ -39,43 +40,55 @@ export const leaveCoupleCallable = functions.https.onCall(async (_data, context) // 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. - const result = await db.runTransaction(async (tx) => { - const coupleSnap = await tx.get(coupleRef) - if (!coupleSnap.exists) { - const callerRef = db.collection('users').doc(callerId) - const callerSnap = await tx.get(callerRef) - if (callerSnap.data()?.coupleId === coupleId) { - tx.update(callerRef, { coupleId: null }) + let result: { membership: boolean } + try { + result = await db.runTransaction(async (tx) => { + const coupleSnap = await tx.get(coupleRef) + if (!coupleSnap.exists) { + const callerRef = db.collection('users').doc(callerId) + const callerSnap = await tx.get(callerRef) + if (callerSnap.data()?.coupleId === coupleId) { + tx.update(callerRef, { coupleId: null }) + } + return { membership: true } } + + const userIds = (coupleSnap.data()?.userIds ?? []) as string[] + 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) => { + if (snap.data()?.coupleId === coupleId) { + tx.update(db.collection('users').doc(userIds[i]), { coupleId: null }) + } + }) + tx.delete(coupleRef) return { membership: true } - } - - const userIds = (coupleSnap.data()?.userIds ?? []) as string[] - 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) => { - if (snap.data()?.coupleId === coupleId) { - tx.update(db.collection('users').doc(userIds[i]), { coupleId: null }) - } }) - tx.delete(coupleRef) - return { membership: true } - }) + } catch (e) { + if (e instanceof HttpsError) throw e + logger.error('[leaveCoupleCallable] leave transaction failed', { error: String(e) }) + throw new HttpsError('internal', 'Failed to leave couple. Please try again.') + } if (!result.membership) { - throw new functions.https.HttpsError('permission-denied', 'Not a member of this couple.') + throw new HttpsError('permission-denied', 'Not a member of this couple.') } // Couple doc is deleted in the transaction; sweep any subcollections left behind. - // Idempotent if a concurrent caller already removed them. - await db.recursiveDelete(coupleRef) + // 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) { + logger.warn('[leaveCoupleCallable] subcollection sweep failed (ignored)', { error: String(e) }) + } - console.log(`[leaveCoupleCallable] user ${callerId} left couple ${coupleId}`) + logger.log(`[leaveCoupleCallable] user ${callerId} left couple ${coupleId}`) return { success: true } }) diff --git a/functions/src/couples/submitOutcomeCallable.ts b/functions/src/couples/submitOutcomeCallable.ts index c94e2ece..fbbf7aaa 100644 --- a/functions/src/couples/submitOutcomeCallable.ts +++ b/functions/src/couples/submitOutcomeCallable.ts @@ -1,5 +1,6 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { onCall, HttpsError, CallableRequest } from 'firebase-functions/v2/https' +import { logger } from '../log' /** * Cloud Function: submitOutcomeCallable @@ -54,23 +55,24 @@ function isValidScoreMap(value: unknown): value is Record { return true } -export const submitOutcomeCallable = functions.https.onCall(async (data: Record, context) => { - const callerId = context.auth?.uid +export const submitOutcomeCallable = onCall(async (request: CallableRequest>) => { + const callerId = request.auth?.uid if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.') + throw new HttpsError('unauthenticated', 'Must be signed in.') } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.') + if (!request.app) { + throw new HttpsError('failed-precondition', 'App Check verification required.') } + const data = request.data const coupleId = data?.coupleId if (typeof coupleId !== 'string' || coupleId.length === 0) { - throw new functions.https.HttpsError('invalid-argument', 'coupleId is required.') + throw new HttpsError('invalid-argument', 'coupleId is required.') } const dayKey = data?.dayKey if (!isValidDayKey(dayKey)) { - throw new functions.https.HttpsError( + throw new HttpsError( 'invalid-argument', `dayKey must be one of: ${DAY_KEYS.join(', ')}.` ) @@ -78,7 +80,7 @@ export const submitOutcomeCallable = functions.https.onCall(async (data: Record< const scores = data?.scores if (!isValidScoreMap(scores)) { - throw new functions.https.HttpsError( + throw new HttpsError( 'invalid-argument', `scores must contain ${SCORE_KEYS.join(', ')} with values ${MIN_SCORE}-${MAX_SCORE}.` ) @@ -90,81 +92,88 @@ export const submitOutcomeCallable = functions.https.onCall(async (data: Record< // Caller must be a member of the couple. const coupleDoc = await coupleRef.get() if (!coupleDoc.exists) { - throw new functions.https.HttpsError('not-found', 'Couple not found.') + throw new HttpsError('not-found', 'Couple not found.') } const userIds = (coupleDoc.data()?.userIds ?? []) as string[] if (!userIds.includes(callerId)) { - throw new functions.https.HttpsError('permission-denied', 'Caller is not a member of this couple.') + throw new HttpsError('permission-denied', 'Caller is not a member of this couple.') } const now = admin.firestore.Timestamp.now() const outcomeRef = coupleRef.collection('outcomes').doc(dayKey) - const result = await db.runTransaction(async (tx) => { - const existing = await tx.get(outcomeRef) - const existingData = existing.exists ? (existing.data() ?? {}) : {} + let result: { dayKey: OutcomeDayKey; answeredBy: string[] } + try { + result = await db.runTransaction(async (tx) => { + const existing = await tx.get(outcomeRef) + const existingData = existing.exists ? (existing.data() ?? {}) : {} - // 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 functions.https.HttpsError( - 'failed-precondition', - 'Baseline (day_0) outcome must be submitted before follow-up outcomes.' - ) - } - } - - const answeredBy = (existingData.answeredBy ?? []) as string[] - if (!answeredBy.includes(callerId)) { - answeredBy.push(callerId) - } - - const payload: Record = { - dayKey, - answeredBy, - updatedAt: now, - createdAt: (existingData.createdAt as admin.firestore.Timestamp | undefined) ?? 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 = (baselineSnap.data()?.baseline ?? {}) as Record - const delta: Record = {} - for (const key of SCORE_KEYS) { - delta[key] = (scores[key] ?? 0) - (baseline[key] ?? 0) + // 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 HttpsError( + 'failed-precondition', + 'Baseline (day_0) outcome must be submitted before follow-up outcomes.' + ) } - payload.delta = delta } - } - tx.set(outcomeRef, payload, { merge: true }) + const answeredBy = (existingData.answeredBy ?? []) as string[] + if (!answeredBy.includes(callerId)) { + answeredBy.push(callerId) + } - // Per-user mirror for cross-relationship stats. - const userOutcomeRef = db.collection('users').doc(callerId).collection('outcomes').doc(dayKey) - tx.set(userOutcomeRef, { - dayKey, - coupleId, - ...(dayKey === 'day_0' ? { baseline: scores } : { scores, delta: payload.delta }), - answeredBy: [callerId], - createdAt: (existingData.createdAt as admin.firestore.Timestamp | undefined) ?? now, - updatedAt: now, - }, { merge: true }) + const payload: Record = { + dayKey, + answeredBy, + updatedAt: now, + createdAt: (existingData.createdAt as admin.firestore.Timestamp | undefined) ?? now, + } - return { dayKey, answeredBy } - }) + if (dayKey === 'day_0') { + payload.baseline = scores + } else { + payload.scores = scores + } - console.log(`[submitOutcomeCallable] ${callerId} submitted ${dayKey} for couple ${coupleId}`) + // 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 = (baselineSnap.data()?.baseline ?? {}) as Record + const delta: Record = {} + for (const key of SCORE_KEYS) { + delta[key] = (scores[key] ?? 0) - (baseline[key] ?? 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, { + dayKey, + coupleId, + ...(dayKey === 'day_0' ? { baseline: scores } : { scores, delta: payload.delta }), + answeredBy: [callerId], + createdAt: (existingData.createdAt as admin.firestore.Timestamp | undefined) ?? now, + updatedAt: now, + }, { merge: true }) + + return { dayKey, answeredBy } + }) + } catch (e) { + if (e instanceof HttpsError) throw e + logger.error('[submitOutcomeCallable] outcome transaction failed', { error: String(e) }) + throw new HttpsError('internal', 'Failed to submit outcome. Please try again.') + } + + logger.log(`[submitOutcomeCallable] ${callerId} submitted ${dayKey} for couple ${coupleId}`) return { success: true, ...result } }) diff --git a/functions/src/index.ts b/functions/src/index.ts index 60d1d1a9..ea0d36df 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -13,10 +13,6 @@ if (admin.apps.length === 0) { export { revenueCatWebhook } from './billing/revenueCatWebhook' export { syncEntitlement } from './billing/syncEntitlement' export { onEntitlementChanged } from './billing/onEntitlementChanged' -export { - sendDailyQuestionReminder, - sendPartnerAnsweredNotification, -} from './notifications/reminders' export { sendGentleReminderCallable } from './notifications/sendGentleReminderCallable' export { sendThinkingOfYouCallable } from './notifications/sendThinkingOfYouCallable' export { diff --git a/functions/src/notifications/reminders.ts b/functions/src/notifications/reminders.ts deleted file mode 100644 index d7e43d05..00000000 --- a/functions/src/notifications/reminders.ts +++ /dev/null @@ -1,94 +0,0 @@ -import * as functions from 'firebase-functions' -import * as admin from 'firebase-admin' - -type ReminderType = 'daily_question' | 'partner_answered' | 'streak' - -interface NotificationPayload { - userId: string - type: ReminderType - title?: string - body?: string -} - -/** - * Callable function that queues a daily-question reminder. - * - * Expected body: { userId: string } - * Auth context supplies the caller uid; the request `userId` must match - * the caller to prevent cross-user notification spam. - * - * This is a placeholder scheduler. The actual daily scheduling will be - * handled by a Firestore trigger / Cloud Scheduler integration later. - */ -export const sendDailyQuestionReminder = functions.https.onCall(async (data: NotificationPayload, context) => { - const callerId = context.auth?.uid - if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Caller must be authenticated.') - } - - const userId = data.userId - if (!userId || typeof userId !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'userId is required.') - } - - // Users may only request reminders for themselves. - if (userId !== callerId) { - throw new functions.https.HttpsError('permission-denied', 'Cannot send notifications to another user.') - } - - await writeNotificationRecord(userId, 'daily_question', { - title: data.title ?? 'Your daily question is waiting!', - body: data.body ?? "Tap to answer today's question together.", - }) - - return { queued: true, type: 'daily_question' } -}) - -/** - * Callable function that queues a partner-answered notification. - * - * Expected body: { userId: string } - * Auth context must match the request `userId`. - */ -export const sendPartnerAnsweredNotification = functions.https.onCall(async (data: NotificationPayload, context) => { - const callerId = context.auth?.uid - if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Caller must be authenticated.') - } - - const userId = data.userId - if (!userId || typeof userId !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'userId is required.') - } - - if (userId !== callerId) { - throw new functions.https.HttpsError('permission-denied', 'Cannot send notifications to another user.') - } - - await writeNotificationRecord(userId, 'partner_answered', { - title: data.title ?? 'Your partner just answered!', - body: data.body ?? 'See what your partner shared.', - }) - - return { queued: true, type: 'partner_answered' } -}) - -async function writeNotificationRecord( - userId: string, - type: ReminderType, - message: { title: string; body: string } -): Promise { - const db = admin.firestore() - - await db - .collection('users') - .doc(userId) - .collection('notification_queue') - .add({ - type, - ...message, - read: false, - sent: false, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }) -} diff --git a/functions/src/notifications/sendGentleReminderCallable.ts b/functions/src/notifications/sendGentleReminderCallable.ts index c313b9fe..4d555e01 100644 --- a/functions/src/notifications/sendGentleReminderCallable.ts +++ b/functions/src/notifications/sendGentleReminderCallable.ts @@ -1,6 +1,7 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { pruneDeadTokens } from './pruneTokens' +import { onCall, HttpsError } from 'firebase-functions/v2/https' +import { sendPushToUser } from './push' +import { logger } from '../log' const GENTLE_REMINDER_MAX_PER_HOUR = 5 const GENTLE_REMINDER_WINDOW_MS = 60 * 60 * 1000 // 1 hour @@ -21,13 +22,13 @@ const GENTLE_REMINDER_WINDOW_MS = 60 * 60 * 1000 // 1 hour * The notification is both an FCM push (for the system tray) and an entry in * the partner's notification_queue (for in-app display). */ -export const sendGentleReminderCallable = functions.https.onCall(async (_data, context) => { - const callerId = context.auth?.uid +export const sendGentleReminderCallable = onCall(async (request) => { + const callerId = request.auth?.uid if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.') + throw new HttpsError('unauthenticated', 'Must be signed in.') } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.') + if (!request.app) { + throw new HttpsError('failed-precondition', 'App Check verification required.') } const db = admin.firestore() @@ -37,162 +38,127 @@ export const sendGentleReminderCallable = functions.https.onCall(async (_data, c const userDoc = await db.collection('users').doc(callerId).get() const coupleId = userDoc.data()?.coupleId as string | undefined if (!coupleId) { - throw new functions.https.HttpsError('failed-precondition', 'Not in a couple.') + throw new HttpsError('failed-precondition', 'Not in a couple.') } const coupleDoc = await db.collection('couples').doc(coupleId).get() if (!coupleDoc.exists) { - throw new functions.https.HttpsError('not-found', 'Couple not found.') + throw new HttpsError('not-found', 'Couple not found.') } const userIds = (coupleDoc.data()?.userIds ?? []) as string[] const partnerId = userIds.find((id) => id !== callerId) if (!partnerId) { - throw new functions.https.HttpsError('failed-precondition', 'No partner found.') + throw new HttpsError('failed-precondition', 'No partner found.') } - // ── 2. Server-side per-user throttle: 5 per hour (rolling window) ──────── + 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 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() + const throttleResult = await db.runTransaction(async (tx) => { + const snap = await tx.get(rateLimitRef) + const data = snap.data() - let windowStart: admin.firestore.Timestamp - let count: number + let windowStart: admin.firestore.Timestamp + let count: number - if (!snap.exists || !data) { - windowStart = now - count = 0 - } else { - windowStart = data.windowStart as admin.firestore.Timestamp - 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. + if (!snap.exists || !data) { windowStart = now count = 0 + } else { + windowStart = data.windowStart as admin.firestore.Timestamp + 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 / 60_000)) - return { allowed: false, retryAfterMinutes } - } + 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 / 60_000)) + return { allowed: false as const, retryAfterMinutes } + } - tx.set(rateLimitRef, { - count: count + 1, - windowStart, - updatedAt: now, - }, { merge: true }) + tx.set(rateLimitRef, { + count: count + 1, + windowStart, + updatedAt: now, + }, { merge: true }) - return { allowed: true, count: count + 1, windowStart } - }) - - if (!throttleResult.allowed) { - throw new functions.https.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. Collect partner FCM tokens ──────────────────────────────────────── - - const tokens: string[] = [] - const partnerDoc = await db.collection('users').doc(partnerId).get() - if (partnerDoc.exists) { - const legacyToken = partnerDoc.data()?.fcmToken - if (typeof legacyToken === 'string' && legacyToken.length > 0) { - tokens.push(legacyToken) - } - } - - const tokenSnap = await db - .collection('users') - .doc(partnerId) - .collection('fcmTokens') - .get() - tokenSnap.docs.forEach((doc) => { - const t = doc.data()?.token - if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) { - tokens.push(t) - } - }) - - // ── 5. 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(), + return { allowed: true as const } }) - // ── 6. Claim the daily rate-limit lock ─────────────────────────────────── - - await lockRef.set({ - sentBy: callerId, - sentAt: admin.firestore.FieldValue.serverTimestamp(), - }) - - // ── 7. Send FCM push ───────────────────────────────────────────────────── - - if (tokens.length > 0) { - const sendResults = await Promise.allSettled( - tokens.map((token) => - admin.messaging().send({ - token, - 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, - }, - }) + if (!throttleResult.allowed) { + throw new HttpsError( + 'resource-exhausted', + `Too many gentle reminders. Try again in ${throttleResult.retryAfterMinutes} minutes.` ) - ) + } - sendResults.forEach((result, i) => { - if (result.status === 'rejected') { - console.warn( - `[sendGentleReminderCallable] FCM failed for token ${tokens[i]}:`, - result.reason - ) - } + // ── 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(), }) - await pruneDeadTokens(db, partnerId, tokens, sendResults) - } + // ── 6. Send FCM push ───────────────────────────────────────────────────── - console.log( - `[sendGentleReminderCallable] reminder sent from ${callerId} to ${partnerId} in couple ${coupleId}` - ) - return { sent: true } + await 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, + }, + }) + + logger.log( + `[sendGentleReminderCallable] reminder sent from ${callerId} to ${partnerId} in couple ${coupleId}` + ) + return { sent: true } + } catch (e) { + if (e instanceof HttpsError) throw e + logger.error('[sendGentleReminderCallable] failed', { error: String(e) }) + throw new HttpsError('internal', 'Failed to send reminder. Please try again.') + } }) diff --git a/functions/src/notifications/sendThinkingOfYouCallable.ts b/functions/src/notifications/sendThinkingOfYouCallable.ts index d06169dc..97a13943 100644 --- a/functions/src/notifications/sendThinkingOfYouCallable.ts +++ b/functions/src/notifications/sendThinkingOfYouCallable.ts @@ -1,7 +1,8 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { onCall, HttpsError } from 'firebase-functions/v2/https' import { recipientInQuietHours } from './quietHours' -import { pruneDeadTokens } from './pruneTokens' +import { sendPushToUser } from './push' +import { logger } from '../log' const THINKING_OF_YOU_MAX_PER_DAY = 10 const THINKING_OF_YOU_WINDOW_MS = 24 * 60 * 60 * 1000 // rolling 24h @@ -17,13 +18,13 @@ const THINKING_OF_YOU_WINDOW_MS = 24 * 60 * 60 * 1000 // rolling 24h * * Writes a partner-safe `notification_queue` entry (in-app + the "Together" feed) and an FCM push. */ -export const sendThinkingOfYouCallable = functions.https.onCall(async (_data, context) => { - const callerId = context.auth?.uid +export const sendThinkingOfYouCallable = onCall(async (request) => { + const callerId = request.auth?.uid if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.') + throw new HttpsError('unauthenticated', 'Must be signed in.') } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.') + if (!request.app) { + throw new HttpsError('failed-precondition', 'App Check verification required.') } const db = admin.firestore() @@ -32,92 +33,80 @@ export const sendThinkingOfYouCallable = functions.https.onCall(async (_data, co const userDoc = await db.collection('users').doc(callerId).get() const coupleId = userDoc.data()?.coupleId as string | undefined if (!coupleId) { - throw new functions.https.HttpsError('failed-precondition', 'Not in a couple.') + throw new HttpsError('failed-precondition', 'Not in a couple.') } const coupleDoc = await db.collection('couples').doc(coupleId).get() if (!coupleDoc.exists) { - throw new functions.https.HttpsError('not-found', 'Couple not found.') + throw new HttpsError('not-found', 'Couple not found.') } const userIds = (coupleDoc.data()?.userIds ?? []) as string[] const partnerId = userIds.find((id) => id !== callerId) if (!partnerId) { - throw new functions.https.HttpsError('failed-precondition', 'No partner found.') + throw new HttpsError('failed-precondition', 'No partner found.') } - // ── 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 as admin.firestore.Timestamp - count = typeof data.count === 'number' ? data.count : 0 - if (now.toMillis() - windowStart.toMillis() >= THINKING_OF_YOU_WINDOW_MS) { - windowStart = now - count = 0 + 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 as admin.firestore.Timestamp + 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 functions.https.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() - if (recipientInQuietHours(partnerDoc.data())) { - console.log(`[sendThinkingOfYouCallable] ${partnerId} in quiet hours — push suppressed (in-app kept)`) - return { sent: true } - } - - // ── FCM push ───────────────────────────────────────────────────────────── - const tokens: string[] = [] - const legacy = partnerDoc.data()?.fcmToken - if (typeof legacy === 'string' && legacy.length > 0) tokens.push(legacy) - const tokenSnap = await db.collection('users').doc(partnerId).collection('fcmTokens').get() - tokenSnap.docs.forEach((d) => { - const t = d.data()?.token - if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) tokens.push(t) - }) - - if (tokens.length > 0) { - const results = await Promise.allSettled( - tokens.map((token) => - admin.messaging().send({ - token, - 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 }, - }) - ) - ) - results.forEach((r, i) => { - if (r.status === 'rejected') { - console.warn(`[sendThinkingOfYouCallable] FCM failed for token ${tokens[i]}:`, r.reason) + if (count >= THINKING_OF_YOU_MAX_PER_DAY) { + return { allowed: false as const } } + tx.set(rateLimitRef, { count: count + 1, windowStart, updatedAt: now }, { merge: true }) + return { allowed: true as const } }) - await pruneDeadTokens(db, partnerId, tokens, results) - } + if (!throttle.allowed) { + throw new HttpsError('resource-exhausted', "You've sent a few already — give it a moment.") + } - console.log(`[sendThinkingOfYouCallable] sent from ${callerId} to ${partnerId} in couple ${coupleId}`) - return { sent: true } + // ── 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 (recipientInQuietHours(partnerData)) { + logger.log(`[sendThinkingOfYouCallable] ${partnerId} in quiet hours — push suppressed (in-app kept)`) + return { sent: true } + } + + // ── FCM push ───────────────────────────────────────────────────────────── + await 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, + ) + + logger.log(`[sendThinkingOfYouCallable] sent from ${callerId} to ${partnerId} in couple ${coupleId}`) + return { sent: true } + } catch (e) { + if (e instanceof HttpsError) throw e + logger.error('[sendThinkingOfYouCallable] failed', { error: String(e) }) + throw new HttpsError('internal', 'Failed to send. Please try again.') + } }) diff --git a/functions/src/questions/assignDailyQuestion.ts b/functions/src/questions/assignDailyQuestion.ts index 5a7adbf8..8c4dec9f 100644 --- a/functions/src/questions/assignDailyQuestion.ts +++ b/functions/src/questions/assignDailyQuestion.ts @@ -1,6 +1,6 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { onSchedule } from 'firebase-functions/v2/scheduler' +import { onCall, HttpsError, CallableRequest } from 'firebase-functions/v2/https' import { logger } from '../log' /** @@ -72,30 +72,31 @@ export const assignDailyQuestion = onSchedule( * * Body: { coupleId: string, date?: string } */ -export const assignDailyQuestionCallable = functions.https.onCall(async (data: Record, context) => { - const callerId = context.auth?.uid +export const assignDailyQuestionCallable = onCall(async (request: CallableRequest>) => { + const callerId = request.auth?.uid if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Caller must be authenticated.') + throw new HttpsError('unauthenticated', 'Caller must be authenticated.') } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.') + if (!request.app) { + throw new HttpsError('failed-precondition', 'App Check verification required.') } + const data = request.data const coupleId = data?.coupleId if (!coupleId || typeof coupleId !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'coupleId is required.') + throw new HttpsError('invalid-argument', 'coupleId is required.') } const db = admin.firestore() const coupleDoc = await db.collection('couples').doc(coupleId).get() if (!coupleDoc.exists) { - throw new functions.https.HttpsError('not-found', 'Couple not found.') + throw new HttpsError('not-found', 'Couple not found.') } // Caller must be a member of the couple. const userIds = (coupleDoc.data()?.userIds ?? []) as string[] if (!userIds.includes(callerId)) { - throw new functions.https.HttpsError('permission-denied', 'Caller is not a couple member.') + throw new HttpsError('permission-denied', 'Caller is not a couple member.') } // Security review Batch 2: constrain the client-supplied date. Only today's CST date @@ -105,14 +106,14 @@ export const assignDailyQuestionCallable = functions.https.onCall(async (data: R const today = cstDateString() const date = data?.date && typeof data.date === 'string' ? data.date : today if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { - throw new functions.https.HttpsError('invalid-argument', 'date must be YYYY-MM-DD.') + throw new HttpsError('invalid-argument', 'date must be YYYY-MM-DD.') } if (date !== today) { - throw new functions.https.HttpsError('invalid-argument', 'Daily question can only be assigned for today.') + throw new HttpsError('invalid-argument', 'Daily question can only be assigned for today.') } const questionId = await pickDailyQuestionId(today) if (!questionId) { - throw new functions.https.HttpsError('internal', 'No active questions available.') + throw new HttpsError('internal', 'No active questions available.') } const nextDay = nextCstDateString(date) @@ -130,10 +131,11 @@ export const assignDailyQuestionCallable = functions.https.onCall(async (data: R expiresAt: timestampAt6PmCst(nextDay), }) } catch (err: any) { + if (err instanceof HttpsError) throw err if (err?.code === 6 || err?.message?.includes('ALREADY_EXISTS')) { - throw new functions.https.HttpsError('already-exists', `Daily question already assigned for ${date}.`) + throw new HttpsError('already-exists', `Daily question already assigned for ${date}.`) } - throw new functions.https.HttpsError('internal', 'Failed to assign daily question.') + throw new HttpsError('internal', 'Failed to assign daily question.') } return { success: true, coupleId, date, questionId } diff --git a/functions/src/releaseKey/wrapReleaseKeyCallable.ts b/functions/src/releaseKey/wrapReleaseKeyCallable.ts index 7d53589b..0930f71c 100644 --- a/functions/src/releaseKey/wrapReleaseKeyCallable.ts +++ b/functions/src/releaseKey/wrapReleaseKeyCallable.ts @@ -1,5 +1,6 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { onCall, HttpsError, CallableRequest } from 'firebase-functions/v2/https' +import { logger } from '../log' /** * HTTPS callable that wraps an iOS one-time AES-256 answer key for an Android partner. @@ -47,155 +48,150 @@ export interface WrapReleaseKeyResponse { const DEFAULT_AAD = 'closer_release_key' -export const wrapReleaseKeyCallable = functions.https.onCall(async (data: Record, context) => { - const callerId = context.auth?.uid - if (!callerId) { - throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.') - } - if (!context.app) { - throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.') - } +export const wrapReleaseKeyCallable = onCall( + { memory: '512MiB' }, + async (request: CallableRequest>) => { + const callerId = request.auth?.uid + if (!callerId) { + throw new HttpsError('unauthenticated', 'Must be signed in.') + } + if (!request.app) { + throw new HttpsError('failed-precondition', 'App Check verification required.') + } + const data = request.data - const recipientUserId = data?.recipientUserId - const oneTimeKeyB64 = data?.oneTimeKey - if (!recipientUserId || typeof recipientUserId !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'recipientUserId is required.') - } - if (!oneTimeKeyB64 || typeof oneTimeKeyB64 !== 'string') { - throw new functions.https.HttpsError('invalid-argument', 'oneTimeKey is required.') - } + const recipientUserId = data?.recipientUserId + const oneTimeKeyB64 = data?.oneTimeKey + if (!recipientUserId || typeof recipientUserId !== 'string') { + throw new HttpsError('invalid-argument', 'recipientUserId is required.') + } + if (!oneTimeKeyB64 || typeof oneTimeKeyB64 !== 'string') { + throw new HttpsError('invalid-argument', 'oneTimeKey is required.') + } - // Validate the oneTimeKey is well-formed base64 of a 32-byte AES-256 key. - let oneTimeKey: Buffer - try { - oneTimeKey = Buffer.from(oneTimeKeyB64, 'base64') - } catch { - throw new functions.https.HttpsError('invalid-argument', 'oneTimeKey is not valid base64.') - } - if (oneTimeKey.length !== 32) { - throw new functions.https.HttpsError( - 'invalid-argument', - `oneTimeKey must be 32 bytes; got ${oneTimeKey.length}.` + // Validate the oneTimeKey is well-formed base64 of a 32-byte AES-256 key. + let oneTimeKey: Buffer + try { + oneTimeKey = Buffer.from(oneTimeKeyB64, 'base64') + } catch { + throw new HttpsError('invalid-argument', 'oneTimeKey is not valid base64.') + } + if (oneTimeKey.length !== 32) { + throw new 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 = callerDoc.data()?.coupleId as string | undefined + if (!coupleId) { + throw new HttpsError('failed-precondition', 'Caller is not paired.') + } + + const coupleDoc = await db.collection('couples').doc(coupleId).get() + if (!coupleDoc.exists) { + throw new HttpsError('not-found', 'Couple not found.') + } + const coupleUserIds = coupleDoc.data()?.userIds as string[] | undefined + if (!coupleUserIds || !coupleUserIds.includes(callerId)) { + throw new HttpsError('permission-denied', 'Caller is not a member of this couple.') + } + if (!coupleUserIds.includes(recipientUserId)) { + throw new HttpsError('permission-denied', 'recipientUserId is not the caller\'s partner.') + } + if (recipientUserId === callerId) { + throw new 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 HttpsError( + 'failed-precondition', + 'Recipient has not registered a release-key device. Ask them to open the app first.' + ) + } + const publicKeyB64 = deviceDoc.data()?.publicKey as string | undefined + if (!publicKeyB64 || typeof publicKeyB64 !== 'string') { + throw new 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: any + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const tink = require('@tink-crypto/tink-crypto') + tinkAead = tink.aead + } catch { + throw new HttpsError('internal', 'Tink crypto library is not available on the server.') + } + + // Decode the recipient's public keyset and create a HybridEncrypt primitive. + let hybridEncrypt: any + 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) { + logger.warn(`[wrapReleaseKeyCallable] public key parse failed for ${recipientUserId}`, { error: String(err) }) + throw new HttpsError('failed-precondition', 'Recipient public key could not be parsed.') + } + + // Wrap the one-time key. + const aad = (data?.aad as string | undefined) ?? DEFAULT_AAD + let ciphertext: Buffer + try { + ciphertext = Buffer.from(hybridEncrypt.encrypt(oneTimeKey, Buffer.from(aad, 'utf-8'))) + } catch (err) { + logger.warn(`[wrapReleaseKeyCallable] wrap failed for ${recipientUserId}`, { error: String(err) }) + throw new 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({ + ...auditFields, + type: 'wrap_release_key', + read: true, + }) + } catch (err) { + 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: WrapReleaseKeyResponse = { + keybox: `keybox:v1:${keyboxB64}`, + ephemeralPublicKey: '', + ciphertext: keyboxB64, + mac: '', + } + + logger.log( + `[wrapReleaseKeyCallable] ${callerId} wrapped release key for ${recipientUserId} in couple ${coupleId}` ) - } - 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 = callerDoc.data()?.coupleId as string | undefined - if (!coupleId) { - throw new functions.https.HttpsError('failed-precondition', 'Caller is not paired.') + return response } - - const coupleDoc = await db.collection('couples').doc(coupleId).get() - if (!coupleDoc.exists) { - throw new functions.https.HttpsError('not-found', 'Couple not found.') - } - const coupleUserIds = coupleDoc.data()?.userIds as string[] | undefined - if (!coupleUserIds || !coupleUserIds.includes(callerId)) { - throw new functions.https.HttpsError('permission-denied', 'Caller is not a member of this couple.') - } - if (!coupleUserIds.includes(recipientUserId)) { - throw new functions.https.HttpsError( - 'permission-denied', - 'recipientUserId is not the caller\'s partner.' - ) - } - if (recipientUserId === callerId) { - throw new functions.https.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 functions.https.HttpsError( - 'failed-precondition', - 'Recipient has not registered a release-key device. Ask them to open the app first.' - ) - } - const publicKeyB64 = deviceDoc.data()?.publicKey as string | undefined - if (!publicKeyB64 || typeof publicKeyB64 !== 'string') { - throw new functions.https.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: any - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const tink = require('@tink-crypto/tink-crypto') - tinkAead = tink.aead - } catch { - throw new functions.https.HttpsError('internal', 'Tink crypto library is not available on the server.') - } - - // Decode the recipient's public keyset and create a HybridEncrypt primitive. - let hybridEncrypt: any - 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) { - console.warn(`[wrapReleaseKeyCallable] public key parse failed for ${recipientUserId}:`, err) - throw new functions.https.HttpsError( - 'failed-precondition', - 'Recipient public key could not be parsed.' - ) - } - - // Wrap the one-time key. - const aad = (data?.aad as string | undefined) ?? DEFAULT_AAD - let ciphertext: Buffer - try { - ciphertext = Buffer.from(hybridEncrypt.encrypt(oneTimeKey, Buffer.from(aad, 'utf-8'))) - } catch (err) { - console.warn(`[wrapReleaseKeyCallable] wrap failed for ${recipientUserId}:`, err) - throw new functions.https.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({ - ...auditFields, - type: 'wrap_release_key', - read: true, - }) - } catch (err) { - console.warn(`[wrapReleaseKeyCallable] audit log failed for ${callerId}:`, 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: WrapReleaseKeyResponse = { - keybox: `keybox:v1:${keyboxB64}`, - ephemeralPublicKey: '', - ciphertext: keyboxB64, - mac: '', - } - - console.log( - `[wrapReleaseKeyCallable] ${callerId} wrapped release key for ${recipientUserId} in couple ${coupleId}` - ) - - return response -}) +) diff --git a/functions/src/security/checkDeviceIntegrity.ts b/functions/src/security/checkDeviceIntegrity.ts index 7cbadbd6..83a87a0b 100644 --- a/functions/src/security/checkDeviceIntegrity.ts +++ b/functions/src/security/checkDeviceIntegrity.ts @@ -1,9 +1,13 @@ -import * as functions from 'firebase-functions' +import { onCall, HttpsError, CallableRequest } from 'firebase-functions/v2/https' import { GoogleAuth } from 'google-auth-library' +import { logger } from '../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 = 10_000 /** * Verifies a Play Integrity API token server-side and returns whether the @@ -25,27 +29,19 @@ const PLAY_INTEGRITY_URL = * (returns passed: false). Configure the API and grant the service account * the "Play Integrity API User" IAM role before deploying to production. */ -export const checkDeviceIntegrity = functions.https.onCall( - async (data: { token?: string }, context) => { - if (!context.auth) { - throw new functions.https.HttpsError( - 'unauthenticated', - 'Caller must be authenticated.' - ) +export const checkDeviceIntegrity = onCall( + { memory: '512MiB' }, + async (request: CallableRequest<{ token?: string }>) => { + if (!request.auth) { + throw new HttpsError('unauthenticated', 'Caller must be authenticated.') } - if (!context.app) { - throw new functions.https.HttpsError( - 'failed-precondition', - 'App Check verification required.' - ) + if (!request.app) { + throw new HttpsError('failed-precondition', 'App Check verification required.') } - const token = data?.token + const token = request.data?.token if (!token || typeof token !== 'string') { - throw new functions.https.HttpsError( - 'invalid-argument', - 'token is required.' - ) + throw new HttpsError('invalid-argument', 'token is required.') } try { @@ -55,7 +51,7 @@ export const checkDeviceIntegrity = functions.https.onCall( verdicts.includes('MEETS_STRONG_INTEGRITY') return { passed, verdicts } } catch (err) { - console.error('[checkDeviceIntegrity] verification failed:', err) + 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. @@ -81,6 +77,7 @@ async function decodeIntegrityToken(token: string): Promise { url: PLAY_INTEGRITY_URL, method: 'POST', data: { integrity_token: token }, + timeout: PLAY_INTEGRITY_TIMEOUT_MS, }) return ( response.data.tokenPayloadExternal?.deviceIntegrity