Closer/functions/dist/index.js

125 lines
12 KiB
JavaScript
Raw Normal View History

"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 });
feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3) A couple-key compromise currently exposes everything, forever, because the key never changes. This adds the rotation ceremony: a fresh AES-256-GCM key becomes the keyset's primary while the old keys stay for reads. The keyset is a Tink keyring and every enc:v1: blob carries its key-id internally, so all history keeps decrypting with zero wire-format changes — none of the 25 isCiphertext rule sites move. Phase 1 protects FUTURE content only (a stolen keyset still contains the old key); forward secrecy for history is phase 2, which builds on the keyGeneration plumbing laid here. The Security-screen copy says so plainly. The ceremony (CoupleRepositoryImpl.rotateCoupleKey): read the couple fresh so concurrent rotations collide at the rules instead of overwriting each other → prepareRotation builds the rotated keyset and re-wraps it under the SAME phrase (fail-closed with a typed error when this device lacks keyset or phrase; nothing persisted anywhere) → ONE merge write lands the new wrap + a strictly-increasing keyGeneration atomically, so the partner can never observe a bumped generation pointing at the old wrap → only then commitRotation stores locally. A failed server write leaves the device coherent on the old key; a crash after it self-heals through the same adoption path as the partner. Adoption (CoupleEncryptionManager.adoptRotationIfNeeded, hooked into Home's healing block, synchronously before the screen settles — until the rotated keyset is stored, new content renders locked): couple.keyGeneration ahead of the local generation → unwrap the published wrap with the locally-stored phrase → replace the keyset. Replaced only on success, never deleted on failure, so old content survives anything. No phrase on this device → needsRecovery, and both recovery flows already deliver the rotated keyset for free (phrase entry unwraps the current wrap; partner-assist exports the current keyset). Same phrase both sides is the entire distribution trick — no new ceremony, no partner action. Server: onCoupleKeyRotated (couples/{id} update, pure isKeyGenerationIncrease edge guard so streak/rhythm/re-wrap updates never fire it, and a rules-forbidden downgrade or redelivered stale event never alerts) sends both members the 🔑 security alert through the house pipeline, bypassing quiet hours like the restore self-alerts. The push is also functional: the partner's closed app can't read new-key content until it next loads Home — the tap takes them there. Rules: isUpdatingRecoveryWrap admits keyGeneration, strictly increasing (monotonic like encryptionVersion), untouched for plain phrase re-wraps. Tests (real Tink, mocks stop at storage): history readable after rotation + NEW writes unreadable by the old keyset — mutation-checked by dropping setPrimary, which kills exactly that test (a rotation that forgets setPrimary passes everything else while protecting nothing) — same-phrase unwrap reads both eras (the partner's whole adoption, proven), prepare persists nothing until commit, fail-closed without phrase/keyset, adoption state machine incl. corrupt-wrap. Android suite green, assembleDebug clean, functions 105/105, tsc clean. Deploy (scoped): firebase deploy --only firestore:rules and --only functions:onCoupleKeyRotated. Live verify follows deploys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 02:49:50 -05:00
exports.wrapReleaseKeyCallable = exports.onDesireSyncPartFinished = exports.onHowWellPartFinished = exports.onWheelPartFinished = exports.onThisOrThatPartFinished = exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.aggregateOutcomeStats = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleKeyRotated = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerRevealed = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = exports.cleanupExpiredRestoreRequests = exports.onRestoreFulfilled = exports.onRestoreRequested = exports.onDateHistoryCreated = exports.onDateReflectionRevealed = exports.onDateReflectionWritten = exports.notifyOnDateMatch = exports.checkDeviceIntegrity = exports.sendReengagementReminder = exports.sendStreakReminder = exports.sendDailyQuestionProactiveReminder = exports.unlockDueMemoryCapsules = exports.sendChallengeDayReminders = exports.sendThinkingOfYouCallable = exports.sendGentleReminderCallable = exports.onEntitlementChanged = exports.syncEntitlement = exports.revenueCatWebhook = void 0;
// Global v2 options (region pin, instance cap) — MUST be imported before any function module so
// setGlobalOptions runs before the functions below are defined.
require("./options");
const admin = __importStar(require("firebase-admin"));
// Initialize the Admin SDK once for every function in this codebase.
// Handlers call admin.firestore()/messaging() lazily at invocation time, so a
// single idempotent init here is sufficient and avoids "already exists" errors.
if (admin.apps.length === 0) {
admin.initializeApp();
}
// RevenueCat entitlement webhook. Auth = HMAC-SHA256 over the raw body (see revenueCatWebhook.ts).
// Binds defineSecret('REVENUECAT_WEBHOOK_SECRET') — the integration's signing secret from the
// RevenueCat dashboard — which is validated for the whole codebase at deploy time, so the secret
// must exist before any deploy. Seed it with:
// firebase functions:secrets:set REVENUECAT_WEBHOOK_SECRET
var revenueCatWebhook_1 = require("./billing/revenueCatWebhook");
Object.defineProperty(exports, "revenueCatWebhook", { enumerable: true, get: function () { return revenueCatWebhook_1.revenueCatWebhook; } });
var syncEntitlement_1 = require("./billing/syncEntitlement");
Object.defineProperty(exports, "syncEntitlement", { enumerable: true, get: function () { return syncEntitlement_1.syncEntitlement; } });
var onEntitlementChanged_1 = require("./billing/onEntitlementChanged");
Object.defineProperty(exports, "onEntitlementChanged", { enumerable: true, get: function () { return onEntitlementChanged_1.onEntitlementChanged; } });
var sendGentleReminderCallable_1 = require("./notifications/sendGentleReminderCallable");
Object.defineProperty(exports, "sendGentleReminderCallable", { enumerable: true, get: function () { return sendGentleReminderCallable_1.sendGentleReminderCallable; } });
var sendThinkingOfYouCallable_1 = require("./notifications/sendThinkingOfYouCallable");
Object.defineProperty(exports, "sendThinkingOfYouCallable", { enumerable: true, get: function () { return sendThinkingOfYouCallable_1.sendThinkingOfYouCallable; } });
var gameRetention_1 = require("./notifications/gameRetention");
Object.defineProperty(exports, "sendChallengeDayReminders", { enumerable: true, get: function () { return gameRetention_1.sendChallengeDayReminders; } });
Object.defineProperty(exports, "unlockDueMemoryCapsules", { enumerable: true, get: function () { return gameRetention_1.unlockDueMemoryCapsules; } });
var dailyQuestionReminder_1 = require("./notifications/dailyQuestionReminder");
Object.defineProperty(exports, "sendDailyQuestionProactiveReminder", { enumerable: true, get: function () { return dailyQuestionReminder_1.sendDailyQuestionProactiveReminder; } });
var streakReminder_1 = require("./notifications/streakReminder");
Object.defineProperty(exports, "sendStreakReminder", { enumerable: true, get: function () { return streakReminder_1.sendStreakReminder; } });
var reengagement_1 = require("./notifications/reengagement");
Object.defineProperty(exports, "sendReengagementReminder", { enumerable: true, get: function () { return reengagement_1.sendReengagementReminder; } });
var checkDeviceIntegrity_1 = require("./security/checkDeviceIntegrity");
Object.defineProperty(exports, "checkDeviceIntegrity", { enumerable: true, get: function () { return checkDeviceIntegrity_1.checkDeviceIntegrity; } });
var createDateMatch_1 = require("./dates/createDateMatch");
Object.defineProperty(exports, "notifyOnDateMatch", { enumerable: true, get: function () { return createDateMatch_1.notifyOnDateMatch; } });
var onDateReflectionWritten_1 = require("./dates/onDateReflectionWritten");
Object.defineProperty(exports, "onDateReflectionWritten", { enumerable: true, get: function () { return onDateReflectionWritten_1.onDateReflectionWritten; } });
var onDateReflectionRevealed_1 = require("./dates/onDateReflectionRevealed");
Object.defineProperty(exports, "onDateReflectionRevealed", { enumerable: true, get: function () { return onDateReflectionRevealed_1.onDateReflectionRevealed; } });
var onDateHistoryCreated_1 = require("./dates/onDateHistoryCreated");
Object.defineProperty(exports, "onDateHistoryCreated", { enumerable: true, get: function () { return onDateHistoryCreated_1.onDateHistoryCreated; } });
var onRestoreRequested_1 = require("./backup/onRestoreRequested");
Object.defineProperty(exports, "onRestoreRequested", { enumerable: true, get: function () { return onRestoreRequested_1.onRestoreRequested; } });
Object.defineProperty(exports, "onRestoreFulfilled", { enumerable: true, get: function () { return onRestoreRequested_1.onRestoreFulfilled; } });
var cleanupRestoreRequests_1 = require("./backup/cleanupRestoreRequests");
Object.defineProperty(exports, "cleanupExpiredRestoreRequests", { enumerable: true, get: function () { return cleanupRestoreRequests_1.cleanupExpiredRestoreRequests; } });
var assignDailyQuestion_1 = require("./questions/assignDailyQuestion");
Object.defineProperty(exports, "assignDailyQuestion", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestion; } });
Object.defineProperty(exports, "assignDailyQuestionCallable", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestionCallable; } });
var onAnswerWritten_1 = require("./questions/onAnswerWritten");
Object.defineProperty(exports, "onAnswerWritten", { enumerable: true, get: function () { return onAnswerWritten_1.onAnswerWritten; } });
var onAnswerRevealed_1 = require("./questions/onAnswerRevealed");
Object.defineProperty(exports, "onAnswerRevealed", { enumerable: true, get: function () { return onAnswerRevealed_1.onAnswerRevealed; } });
var onMessageWritten_1 = require("./questions/onMessageWritten");
Object.defineProperty(exports, "onMessageWritten", { enumerable: true, get: function () { return onMessageWritten_1.onMessageWritten; } });
var onCoupleLeave_1 = require("./couples/onCoupleLeave");
Object.defineProperty(exports, "onCoupleLeave", { enumerable: true, get: function () { return onCoupleLeave_1.onCoupleLeave; } });
feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3) A couple-key compromise currently exposes everything, forever, because the key never changes. This adds the rotation ceremony: a fresh AES-256-GCM key becomes the keyset's primary while the old keys stay for reads. The keyset is a Tink keyring and every enc:v1: blob carries its key-id internally, so all history keeps decrypting with zero wire-format changes — none of the 25 isCiphertext rule sites move. Phase 1 protects FUTURE content only (a stolen keyset still contains the old key); forward secrecy for history is phase 2, which builds on the keyGeneration plumbing laid here. The Security-screen copy says so plainly. The ceremony (CoupleRepositoryImpl.rotateCoupleKey): read the couple fresh so concurrent rotations collide at the rules instead of overwriting each other → prepareRotation builds the rotated keyset and re-wraps it under the SAME phrase (fail-closed with a typed error when this device lacks keyset or phrase; nothing persisted anywhere) → ONE merge write lands the new wrap + a strictly-increasing keyGeneration atomically, so the partner can never observe a bumped generation pointing at the old wrap → only then commitRotation stores locally. A failed server write leaves the device coherent on the old key; a crash after it self-heals through the same adoption path as the partner. Adoption (CoupleEncryptionManager.adoptRotationIfNeeded, hooked into Home's healing block, synchronously before the screen settles — until the rotated keyset is stored, new content renders locked): couple.keyGeneration ahead of the local generation → unwrap the published wrap with the locally-stored phrase → replace the keyset. Replaced only on success, never deleted on failure, so old content survives anything. No phrase on this device → needsRecovery, and both recovery flows already deliver the rotated keyset for free (phrase entry unwraps the current wrap; partner-assist exports the current keyset). Same phrase both sides is the entire distribution trick — no new ceremony, no partner action. Server: onCoupleKeyRotated (couples/{id} update, pure isKeyGenerationIncrease edge guard so streak/rhythm/re-wrap updates never fire it, and a rules-forbidden downgrade or redelivered stale event never alerts) sends both members the 🔑 security alert through the house pipeline, bypassing quiet hours like the restore self-alerts. The push is also functional: the partner's closed app can't read new-key content until it next loads Home — the tap takes them there. Rules: isUpdatingRecoveryWrap admits keyGeneration, strictly increasing (monotonic like encryptionVersion), untouched for plain phrase re-wraps. Tests (real Tink, mocks stop at storage): history readable after rotation + NEW writes unreadable by the old keyset — mutation-checked by dropping setPrimary, which kills exactly that test (a rotation that forgets setPrimary passes everything else while protecting nothing) — same-phrase unwrap reads both eras (the partner's whole adoption, proven), prepare persists nothing until commit, fail-closed without phrase/keyset, adoption state machine incl. corrupt-wrap. Android suite green, assembleDebug clean, functions 105/105, tsc clean. Deploy (scoped): firebase deploy --only firestore:rules and --only functions:onCoupleKeyRotated. Live verify follows deploys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 02:49:50 -05:00
var onCoupleKeyRotated_1 = require("./couples/onCoupleKeyRotated");
Object.defineProperty(exports, "onCoupleKeyRotated", { enumerable: true, get: function () { return onCoupleKeyRotated_1.onCoupleKeyRotated; } });
var leaveCoupleCallable_1 = require("./couples/leaveCoupleCallable");
Object.defineProperty(exports, "leaveCoupleCallable", { enumerable: true, get: function () { return leaveCoupleCallable_1.leaveCoupleCallable; } });
var acceptInviteCallable_1 = require("./couples/acceptInviteCallable");
Object.defineProperty(exports, "acceptInviteCallable", { enumerable: true, get: function () { return acceptInviteCallable_1.acceptInviteCallable; } });
var createInviteCallable_1 = require("./couples/createInviteCallable");
Object.defineProperty(exports, "createInviteCallable", { enumerable: true, get: function () { return createInviteCallable_1.createInviteCallable; } });
var submitOutcomeCallable_1 = require("./couples/submitOutcomeCallable");
Object.defineProperty(exports, "submitOutcomeCallable", { enumerable: true, get: function () { return submitOutcomeCallable_1.submitOutcomeCallable; } });
var aggregateOutcomes_1 = require("./couples/aggregateOutcomes");
Object.defineProperty(exports, "aggregateOutcomeStats", { enumerable: true, get: function () { return aggregateOutcomes_1.aggregateOutcomeStats; } });
var scheduledOutcomesReminder_1 = require("./couples/scheduledOutcomesReminder");
Object.defineProperty(exports, "scheduledOutcomesReminder", { enumerable: true, get: function () { return scheduledOutcomesReminder_1.scheduledOutcomesReminder; } });
var onUserDelete_1 = require("./users/onUserDelete");
Object.defineProperty(exports, "onUserDelete", { enumerable: true, get: function () { return onUserDelete_1.onUserDelete; } });
var onGameSessionUpdate_1 = require("./games/onGameSessionUpdate");
Object.defineProperty(exports, "onGameSessionUpdate", { enumerable: true, get: function () { return onGameSessionUpdate_1.onGameSessionUpdate; } });
Object.defineProperty(exports, "onThisOrThatPartFinished", { enumerable: true, get: function () { return onGameSessionUpdate_1.onThisOrThatPartFinished; } });
Object.defineProperty(exports, "onWheelPartFinished", { enumerable: true, get: function () { return onGameSessionUpdate_1.onWheelPartFinished; } });
Object.defineProperty(exports, "onHowWellPartFinished", { enumerable: true, get: function () { return onGameSessionUpdate_1.onHowWellPartFinished; } });
Object.defineProperty(exports, "onDesireSyncPartFinished", { enumerable: true, get: function () { return onGameSessionUpdate_1.onDesireSyncPartFinished; } });
var wrapReleaseKeyCallable_1 = require("./releaseKey/wrapReleaseKeyCallable");
Object.defineProperty(exports, "wrapReleaseKeyCallable", { enumerable: true, get: function () { return wrapReleaseKeyCallable_1.wrapReleaseKeyCallable; } });
// NOTE (security review Batch 2): the unauthenticated public `health` HTTP endpoint
// was removed to shrink attack surface. Deployment can be verified via
// `firebase functions:list`. If an uptime probe is ever needed, re-add it behind
// auth / a shared secret rather than as an open endpoint.
//# sourceMappingURL=index.js.map