refactor(functions): B4+B5 migrate webhook to v2 + pin onUserDelete on v1

B4 — revenueCatWebhook: functions.https.onRequest → firebase-functions/v2/https onRequest,
with REVENUECAT_SIGNING_KEY bound as a Secret Manager secret via defineSecret (injected into
process.env, so the Ed25519 verify + process-before-ack/500-retry logic is unchanged). Request
type retyped to the v2 Request; console → logger. The key must be seeded in Secret Manager at
deploy (runbook) — it isn't in the repo.

B5 — onUserDelete: kept on the v1 API (2nd gen has no auth.user().onDelete), imported explicitly
from firebase-functions/v1 and wrapped in runWith({ timeoutSeconds: 300, memory: '512MB' }) for
its dual recursiveDelete + Storage sweep. Adopts shared getUserTokens/sendPushToUser + logger;
preserves the original "only notify if the partner has a live token" behavior.
(wrapReleaseKey's HttpsError→v2 swap already landed in B3.)

Build clean; 70 tests green. dist rebuilt. Still on firebase-functions v5.1.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-08 00:00:29 -05:00
parent e0c2d67373
commit a5af32d3d2
6 changed files with 144 additions and 151 deletions

View File

@ -35,8 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
Object.defineProperty(exports, "__esModule", { value: true });
exports.revenueCatWebhook = void 0;
const crypto = __importStar(require("crypto"));
const functions = __importStar(require("firebase-functions"));
const https_1 = require("firebase-functions/v2/https");
const params_1 = require("firebase-functions/params");
const entitlementLogic_1 = require("./entitlementLogic");
const log_1 = require("../log");
/**
* RevenueCat webhook handler.
*
@ -46,14 +48,16 @@ const entitlementLogic_1 = require("./entitlementLogic");
* Authentication:
* - Verifies the Ed25519 signature in the X-Signature request header.
* - REVENUECAT_SIGNING_KEY must be set to the base64-encoded DER/SPKI Ed25519
* public key from your RevenueCat project dashboard.
* public key from your RevenueCat project dashboard. It is bound as a Secret
* Manager secret (below) and injected into process.env at runtime.
* - Missing key 500 (our config error). Invalid/absent signature 401.
*
* Processing:
* - Parses the RevenueCat event payload.
* - Writes entitlement state to Firestore at users/{userId}/entitlements.
*/
exports.revenueCatWebhook = functions.https.onRequest(async (req, res) => {
const revenueCatSigningKey = (0, params_1.defineSecret)('REVENUECAT_SIGNING_KEY');
exports.revenueCatWebhook = (0, https_1.onRequest)({ secrets: [revenueCatSigningKey] }, async (req, res) => {
var _a;
if (req.method !== 'POST') {
res.status(405).json({ error: 'method_not_allowed' });
@ -64,7 +68,7 @@ exports.revenueCatWebhook = functions.https.onRequest(async (req, res) => {
}
catch (err) {
if (err instanceof ConfigError) {
console.error('[revenueCatWebhook] configuration error:', err.message);
log_1.logger.error('[revenueCatWebhook] configuration error', { error: err.message });
res.status(500).json({ error: 'internal_error' });
return;
}
@ -84,7 +88,7 @@ exports.revenueCatWebhook = functions.https.onRequest(async (req, res) => {
await (0, entitlementLogic_1.applyEntitlementEvent)(event);
}
catch (err) {
console.error('[revenueCatWebhook] entitlement sync failed:', err);
log_1.logger.error('[revenueCatWebhook] entitlement sync failed', { error: String(err) });
res.status(500).json({ error: 'processing_failed' });
return;
}

View File

@ -1 +1 @@
{"version":3,"file":"revenueCatWebhook.js","sourceRoot":"","sources":["../../src/billing/revenueCatWebhook.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"revenueCatWebhook.js","sourceRoot":"","sources":["../../src/billing/revenueCatWebhook.ts"],"names":[],"mappings":"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"}

View File

@ -34,12 +34,19 @@ var __importStar = (this && this.__importStar) || (function () {
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.onUserDelete = void 0;
const functions = __importStar(require("firebase-functions"));
const admin = __importStar(require("firebase-admin"));
const pruneTokens_1 = require("../notifications/pruneTokens");
const v1_1 = require("firebase-functions/v1");
const push_1 = require("../notifications/push");
const log_1 = require("../log");
/**
* Auth deletion trigger cascades account deletion server-side.
*
* This is the ONE function kept on the v1 (1st-gen) API: 2nd gen has no
* `auth.user().onDelete` equivalent, so it is imported explicitly from
* firebase-functions/v1 and coexists with the v2 functions in this codebase.
* Global v2 options (setGlobalOptions) do not apply to it, so the timeout/memory
* for its dual recursiveDelete + Storage sweep are set here via runWith().
*
* Fires when a Firebase Auth user is deleted (via client deleteAccount() or
* Admin SDK). The Admin SDK bypasses Firestore rules, so this function can
* reach everything the client cannot.
@ -53,7 +60,9 @@ const pruneTokens_1 = require("../notifications/pruneTokens");
* 3. Recursively delete the user doc + all subcollections
* (entitlements, fcmTokens, notification_queue).
*/
exports.onUserDelete = functions.auth.user().onDelete(async (user) => {
exports.onUserDelete = (0, v1_1.runWith)({ timeoutSeconds: 300, memory: '512MB' })
.auth.user()
.onDelete(async (user) => {
var _a, _b, _c, _d;
const uid = user.uid;
const db = admin.firestore();
@ -79,7 +88,7 @@ exports.onUserDelete = functions.auth.user().onDelete(async (user) => {
await partnerRef.update({ coupleId: null });
// Send push notification to the partner.
await notifyPartner(partnerId, partnerDoc, db).catch((err) => {
console.warn(`[onUserDelete] FCM notification to partner ${partnerId} failed:`, err);
log_1.logger.warn(`[onUserDelete] FCM notification to partner ${partnerId} failed`, { error: String(err) });
});
}
}
@ -92,29 +101,17 @@ exports.onUserDelete = functions.auth.user().onDelete(async (user) => {
await admin.storage().bucket().deleteFiles({ prefix: `users/${uid}/` });
}
catch (err) {
console.warn(`[onUserDelete] Storage cleanup failed for ${uid}:`, err);
log_1.logger.warn(`[onUserDelete] Storage cleanup failed for ${uid}`, { error: String(err) });
}
// ── 3. User doc + subcollections ──────────────────────────────────────────
await db.recursiveDelete(userDocRef);
console.log(`[onUserDelete] completed cleanup for user ${uid}`);
log_1.logger.log(`[onUserDelete] completed cleanup for user ${uid}`);
});
async function notifyPartner(partnerId, partnerDoc, db) {
var _a;
const messaging = admin.messaging();
// Collect FCM tokens (legacy field + fcmTokens subcollection).
const tokens = [];
const legacyToken = (_a = partnerDoc.data()) === null || _a === void 0 ? void 0 : _a.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);
}
});
const partnerData = partnerDoc.data();
// Preserve the original behavior: only write the in-app record + push if the partner has a
// live device token (nothing to deliver otherwise).
const tokens = await (0, push_1.getUserTokens)(db, partnerId, partnerData);
if (tokens.length === 0)
return;
// Write an in-app notification record.
@ -129,20 +126,13 @@ async function notifyPartner(partnerId, partnerDoc, db) {
read: false,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
});
const sendResults = await Promise.allSettled(tokens.map((token) => messaging.send({
token,
await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, {
notification: {
title: 'Your partner deleted their account',
body: 'You are no longer paired. Tap to create a new invite.',
},
android: { notification: { channelId: 'partner_activity' } }, // E-OBS
data: { type: 'partner_deleted_account' },
})));
sendResults.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`[onUserDelete] FCM send to token ${tokens[i]} failed:`, result.reason);
}
});
await (0, pruneTokens_1.pruneDeadTokens)(db, partnerId, tokens, sendResults);
}, partnerData);
}
//# sourceMappingURL=onUserDelete.js.map

View File

@ -1 +1 @@
{"version":3,"file":"onUserDelete.js","sourceRoot":"","sources":["../../src/users/onUserDelete.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"onUserDelete.js","sourceRoot":"","sources":["../../src/users/onUserDelete.ts"],"names":[],"mappings":"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"}

View File

@ -1,6 +1,8 @@
import * as crypto from 'crypto'
import * as functions from 'firebase-functions'
import { onRequest, Request } from 'firebase-functions/v2/https'
import { defineSecret } from 'firebase-functions/params'
import { applyEntitlementEvent, EntitlementEvent } from './entitlementLogic'
import { logger } from '../log'
/**
* RevenueCat webhook handler.
@ -11,14 +13,19 @@ import { applyEntitlementEvent, EntitlementEvent } from './entitlementLogic'
* Authentication:
* - Verifies the Ed25519 signature in the X-Signature request header.
* - REVENUECAT_SIGNING_KEY must be set to the base64-encoded DER/SPKI Ed25519
* public key from your RevenueCat project dashboard.
* public key from your RevenueCat project dashboard. It is bound as a Secret
* Manager secret (below) and injected into process.env at runtime.
* - Missing key 500 (our config error). Invalid/absent signature 401.
*
* Processing:
* - Parses the RevenueCat event payload.
* - Writes entitlement state to Firestore at users/{userId}/entitlements.
*/
export const revenueCatWebhook = functions.https.onRequest(async (req, res) => {
const revenueCatSigningKey = defineSecret('REVENUECAT_SIGNING_KEY')
export const revenueCatWebhook = onRequest(
{ secrets: [revenueCatSigningKey] },
async (req, res) => {
if (req.method !== 'POST') {
res.status(405).json({ error: 'method_not_allowed' })
return
@ -28,7 +35,7 @@ export const revenueCatWebhook = functions.https.onRequest(async (req, res) => {
verifySignature(req)
} catch (err) {
if (err instanceof ConfigError) {
console.error('[revenueCatWebhook] configuration error:', err.message)
logger.error('[revenueCatWebhook] configuration error', { error: err.message })
res.status(500).json({ error: 'internal_error' })
return
}
@ -49,13 +56,14 @@ export const revenueCatWebhook = functions.https.onRequest(async (req, res) => {
try {
await applyEntitlementEvent(event)
} catch (err) {
console.error('[revenueCatWebhook] entitlement sync failed:', err)
logger.error('[revenueCatWebhook] entitlement sync failed', { error: String(err) })
res.status(500).json({ error: 'processing_failed' })
return
}
res.status(200).json({ received: true })
})
}
)
class ConfigError extends Error {}
class AuthError extends Error {}
@ -67,7 +75,7 @@ class AuthError extends Error {}
* these are deployment problems, not auth failures.
* Throws AuthError for any missing or invalid signature these are 401s.
*/
function verifySignature(req: functions.https.Request): void {
function verifySignature(req: Request): void {
const signingKey = process.env.REVENUECAT_SIGNING_KEY
if (!signingKey) {
throw new ConfigError('REVENUECAT_SIGNING_KEY environment variable is not set')

View File

@ -1,10 +1,17 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { pruneDeadTokens } from '../notifications/pruneTokens'
import { auth, runWith } from 'firebase-functions/v1'
import { getUserTokens, sendPushToUser } from '../notifications/push'
import { logger } from '../log'
/**
* Auth deletion trigger cascades account deletion server-side.
*
* This is the ONE function kept on the v1 (1st-gen) API: 2nd gen has no
* `auth.user().onDelete` equivalent, so it is imported explicitly from
* firebase-functions/v1 and coexists with the v2 functions in this codebase.
* Global v2 options (setGlobalOptions) do not apply to it, so the timeout/memory
* for its dual recursiveDelete + Storage sweep are set here via runWith().
*
* Fires when a Firebase Auth user is deleted (via client deleteAccount() or
* Admin SDK). The Admin SDK bypasses Firestore rules, so this function can
* reach everything the client cannot.
@ -18,7 +25,9 @@ import { pruneDeadTokens } from '../notifications/pruneTokens'
* 3. Recursively delete the user doc + all subcollections
* (entitlements, fcmTokens, notification_queue).
*/
export const onUserDelete = functions.auth.user().onDelete(async (user) => {
export const onUserDelete = runWith({ timeoutSeconds: 300, memory: '512MB' })
.auth.user()
.onDelete(async (user: auth.UserRecord) => {
const uid = user.uid
const db = admin.firestore()
@ -50,7 +59,7 @@ export const onUserDelete = functions.auth.user().onDelete(async (user) => {
// Send push notification to the partner.
await notifyPartner(partnerId, partnerDoc, db).catch((err) => {
console.warn(`[onUserDelete] FCM notification to partner ${partnerId} failed:`, err)
logger.warn(`[onUserDelete] FCM notification to partner ${partnerId} failed`, { error: String(err) })
})
}
}
@ -65,37 +74,26 @@ export const onUserDelete = functions.auth.user().onDelete(async (user) => {
try {
await admin.storage().bucket().deleteFiles({ prefix: `users/${uid}/` })
} catch (err) {
console.warn(`[onUserDelete] Storage cleanup failed for ${uid}:`, err)
logger.warn(`[onUserDelete] Storage cleanup failed for ${uid}`, { error: String(err) })
}
// ── 3. User doc + subcollections ──────────────────────────────────────────
await db.recursiveDelete(userDocRef)
console.log(`[onUserDelete] completed cleanup for user ${uid}`)
})
logger.log(`[onUserDelete] completed cleanup for user ${uid}`)
})
async function notifyPartner(
partnerId: string,
partnerDoc: admin.firestore.DocumentSnapshot,
db: admin.firestore.Firestore
): Promise<void> {
const messaging = admin.messaging()
// Collect FCM tokens (legacy field + fcmTokens subcollection).
const tokens: string[] = []
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)
}
})
const partnerData = partnerDoc.data()
// Preserve the original behavior: only write the in-app record + push if the partner has a
// live device token (nothing to deliver otherwise).
const tokens = await getUserTokens(db, partnerId, partnerData)
if (tokens.length === 0) return
// Write an in-app notification record.
@ -111,25 +109,18 @@ async function notifyPartner(
createdAt: admin.firestore.FieldValue.serverTimestamp(),
})
const sendResults = await Promise.allSettled(
tokens.map((token) =>
messaging.send({
token,
await sendPushToUser(
db,
admin.messaging(),
partnerId,
{
notification: {
title: 'Your partner deleted their account',
body: 'You are no longer paired. Tap to create a new invite.',
},
android: { notification: { channelId: 'partner_activity' } }, // E-OBS
data: { type: 'partner_deleted_account' },
})
},
partnerData,
)
)
sendResults.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`[onUserDelete] FCM send to token ${tokens[i]} failed:`, result.reason)
}
})
await pruneDeadTokens(db, partnerId, tokens, sendResults)
}