diff --git a/firestore.rules b/firestore.rules index c3ed6ff5..86c2fa4c 100644 --- a/firestore.rules +++ b/firestore.rules @@ -125,16 +125,12 @@ service cloud.firestore { // - user IDs are immutable (cannot change who is in the couple) // - invite code is immutable (cannot change the code) // - createdAt is immutable (cannot change when the couple was formed) - // - currentQuestionId: either member can set (either can pick a question) // - streakCount and lastStreakAt: server-only (via Cloud Functions or admin SDK) - // - Any other fields: both members can update normally + // - All other fields: both members can update normally allow update: if isCouplesMember(coupleId) // Check immutable fields haven't changed && isImmutable(['userIds', 'inviteCode', 'createdAt']) - // Allow currentQuestionId updates - && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['currentQuestionId']) - // No other fields should be changed - // Check that streakCount and lastStreakAt are not in the update + // streakCount and lastStreakAt must not be modified by clients && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['streakCount', 'lastStreakAt']); // Delete: server-only (admin SDK only) diff --git a/server/src/routes/webhooks.ts b/server/src/routes/webhooks.ts index 57d3cb8b..271dd0c2 100644 --- a/server/src/routes/webhooks.ts +++ b/server/src/routes/webhooks.ts @@ -9,15 +9,15 @@ const router = Router() /** * Verifies RevenueCat webhook signature using Ed25519. * RevenueCat signs the raw request body with their signing key. - * If REVENUECAT_SIGNING_KEY is not set, logs warning but skips verification (dev mode). + * If REVENUECAT_SIGNING_KEY is not set, throws an error (fail-closed). */ function verifyRevenueCatSignature(req: Request): boolean { const signingKey = process.env.REVENUECAT_SIGNING_KEY - // If signing key is not configured, skip verification (development mode) + // If signing key is not configured, fail closed (not dev mode) if (!signingKey || signingKey.trim() === '') { - console.warn('[webhook] REVENUECAT_SIGNING_KEY not set — skipping signature verification') - return true + console.error('[webhook] REVENUECAT_SIGNING_KEY not set — rejecting all requests (fail-closed)') + throw new Error('REVENUECAT_SIGNING_KEY must be set in production') } const signatureHeader = req.headers['x-revenuecat-signature'] @@ -76,16 +76,16 @@ function verifyRevenueCatSecret(req: Request): boolean { } router.post('/revenuecat', async (req: Request, res: Response) => { - // Try signature verification first (modern) - const signatureValid = verifyRevenueCatSignature(req) - if (!signatureValid) { - res.status(401).json({ error: 'unauthorized' }) - return - } - - // Fallback to secret verification if no signature (legacy) - if (!signatureValid && !verifyRevenueCatSecret(req)) { - res.status(401).json({ error: 'unauthorized' }) + // Try signature verification first (modern, prefered) + try { + if (!verifyRevenueCatSignature(req)) { + res.status(401).json({ error: 'unauthorized' }) + return + } + } catch (err: any) { + // If signature verification throws (e.g., missing key), reject with 500 + console.error('[webhook] Signature verification error:', err) + res.status(500).json({ error: 'signature_verification_failed', message: err.message }) return }