Closer/functions/dist/backup/onRestoreRequested.js

192 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.onRestoreFulfilled = exports.onRestoreRequested = exports.SELF_ALERT_DEDUPE_MS = void 0;
exports.selfAlertAllowed = selfAlertAllowed;
exports.isRestoreReadyTransition = isRestoreReadyTransition;
const functions = __importStar(require("firebase-functions"));
const admin = __importStar(require("firebase-admin"));
const quietHours_1 = require("../notifications/quietHours");
const CHANNEL = 'partner_activity';
// Suppress duplicate "was this you?" pushes when a request doc is rapidly deleted+recreated (a compromised
// account could loop that to spam both partners). The in-app queue entry is still written every time.
exports.SELF_ALERT_DEDUPE_MS = 60 * 1000;
/** Pure dedupe decision: allow a self-alert push only if none was sent within the window. */
function selfAlertAllowed(lastAlertAt, now) {
if (typeof lastAlertAt !== 'number')
return true;
return now - lastAlertAt >= exports.SELF_ALERT_DEDUPE_MS;
}
/** Pure guard: fire the completion alert only on the single REQUESTED→READY status transition. */
function isRestoreReadyTransition(beforeStatus, afterStatus) {
return afterStatus === 'READY' && beforeStatus !== 'READY';
}
/** All FCM tokens for a user: the legacy single field + the multi-device `fcmTokens` subcollection. */
async function gatherTokens(db, uid) {
var _a;
const tokens = [];
const userDoc = await db.collection('users').doc(uid).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 tokenSnap = await db.collection('users').doc(uid).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);
});
return tokens;
}
/**
* Write the durable in-app queue entry (always) and — unless quiet hours suppress it — push to every device.
* `bypassQuietHours` is for security signals (the "was this you?" self-alert) that must not be silenced.
*/
async function queueAndPush(db, uid, opts) {
const { type, title, body, coupleId, bypassQuietHours } = opts;
await db.collection('users').doc(uid).collection('notification_queue').add({
type, title, body, read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(),
});
const userDoc = await db.collection('users').doc(uid).get();
if (!userDoc.exists)
return;
if (!bypassQuietHours && (0, quietHours_1.recipientInQuietHours)(userDoc.data()))
return;
const tokens = await gatherTokens(db, uid);
if (tokens.length === 0)
return;
const results = await Promise.allSettled(tokens.map((token) => admin.messaging().send({
notification: { title, body },
data: { type, couple_id: coupleId },
token,
android: { notification: { channelId: CHANNEL } },
})));
results.forEach((r, i) => {
if (r.status === 'rejected')
console.warn(`[restore] FCM failed for ${tokens[i]}:`, r.reason);
});
}
/**
* Fires when a member starts a partner-assisted restore
* (`couples/{coupleId}/restore_requests/{recipientUid}` created on a new/wiped device). Sends TWO
* notifications, isolated so one failing never blocks the other:
* 1. To the OTHER partner — "help them restore" (high-signal; only quiet hours suppress it).
* 2. To the RECIPIENT'S OWN devices — a security "was this you?" alert. If the real owner still holds a
* device (a phished password without device loss), this is how they learn a restore is happening.
* No key material is read or logged — the request carries only a public key + a nonce.
*/
exports.onRestoreRequested = functions.firestore
.document('couples/{coupleId}/restore_requests/{recipientUid}')
.onCreate(async (_snap, context) => {
var _a, _b, _c;
const { coupleId, recipientUid } = context.params;
const db = admin.firestore();
const coupleRef = db.collection('couples').doc(coupleId);
const coupleDoc = await coupleRef.get();
if (!coupleDoc.exists)
return;
const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []);
// The recipient must be a member; notify the OTHER member (the partner who can help).
if (!userIds.includes(recipientUid))
return;
const partnerId = userIds.find((u) => u !== recipientUid);
if (!partnerId)
return;
// Audit trail (no key material — actor/recipient/timestamp only).
console.log(`[onRestoreRequested] couple=${coupleId} recipient=${recipientUid} partner=${partnerId}`);
// 1) Partner "help them restore" — routine quiet hours apply.
try {
await queueAndPush(db, partnerId, {
type: 'restore_requested',
title: 'Help your partner restore 💜',
body: 'Theyre setting up on a new device. Tap to help.',
coupleId,
});
}
catch (e) {
console.warn('[onRestoreRequested] partner notify failed:', e);
}
// 2) Recipient self-alert — security signal, bypasses quiet hours, deduped against create-loops.
try {
const now = Date.now();
if (selfAlertAllowed((_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.lastRestoreSelfAlertAt, now)) {
await coupleRef.set({ lastRestoreSelfAlertAt: now }, { merge: true });
await queueAndPush(db, recipientUid, {
type: 'restore_self_alert',
title: 'New device is restoring your history',
body: 'If this wasnt you, secure your account now.',
coupleId,
bypassQuietHours: true,
});
}
}
catch (e) {
console.warn('[onRestoreRequested] self-alert failed:', e);
}
});
/**
* Fires when the partner writes the keybox (status flips to READY) — the moment the couple key actually
* transfers. Alerts the RECIPIENT'S OWN devices that a restore just completed, the strongest "it happened"
* signal for the real owner. Guarded to the single REQUESTED→READY transition (ignores decline/expire).
*/
exports.onRestoreFulfilled = functions.firestore
.document('couples/{coupleId}/restore_requests/{recipientUid}')
.onUpdate(async (change, context) => {
var _a, _b;
const before = change.before.data();
const after = change.after.data();
if (!isRestoreReadyTransition(before === null || before === void 0 ? void 0 : before.status, after === null || after === void 0 ? void 0 : after.status))
return;
const { coupleId, recipientUid } = context.params;
const db = admin.firestore();
const coupleDoc = await db.collection('couples').doc(coupleId).get();
const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []);
if (!userIds.includes(recipientUid))
return;
console.log(`[onRestoreFulfilled] couple=${coupleId} recipient=${recipientUid}`);
try {
await queueAndPush(db, recipientUid, {
type: 'restore_self_alert',
title: 'Your history was just restored',
body: 'A new device now has access. If this wasnt you, secure your account now.',
coupleId,
bypassQuietHours: true,
});
}
catch (e) {
console.warn('[onRestoreFulfilled] self-alert failed:', e);
}
});
//# sourceMappingURL=onRestoreRequested.js.map