Closer/functions/dist/backup/onRestoreRequested.js

173 lines
8.7 KiB
JavaScript
Raw Permalink 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 admin = __importStar(require("firebase-admin"));
const firestore_1 = require("firebase-functions/v2/firestore");
const quietHours_1 = require("../notifications/quietHours");
const push_1 = require("../notifications/push");
const log_1 = require("../log");
const CHANNEL = 'partner_activity';
// Suppress duplicate pushes when a request doc is rapidly deleted+recreated (a compromised account
// could loop that to spam both partners) and when an event is redelivered. The in-app queue entry is
// still written every time. A genuine re-request after the window still notifies — so this is a time
// window, NOT a permanent per-recipient marker (which would block legitimate re-requests).
exports.SELF_ALERT_DEDUPE_MS = 60 * 1000;
/** Pure dedupe decision: allow an 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';
}
/**
* 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;
const userData = userDoc.data();
if (!bypassQuietHours && (0, quietHours_1.recipientInQuietHours)(userData))
return;
await (0, push_1.sendPushToUser)(db, admin.messaging(), uid, {
notification: { title, body },
data: { type, couple_id: coupleId },
android: { notification: { channelId: CHANNEL } },
}, userData);
}
/**
* 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 = (0, firestore_1.onDocumentCreated)('couples/{coupleId}/restore_requests/{recipientUid}', async (event) => {
var _a, _b, _c, _d;
const { coupleId, recipientUid } = event.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).
log_1.logger.log(`[onRestoreRequested] couple=${coupleId} recipient=${recipientUid} partner=${partnerId}`);
// 1) Partner "help them restore" — routine quiet hours apply. Deduped against redelivery and
// request recreate-loops with the same window used for the self-alert.
try {
const now = Date.now();
if (selfAlertAllowed((_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.lastRestorePartnerAlertAt, now)) {
await coupleRef.set({ lastRestorePartnerAlertAt: now }, { merge: true });
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) {
log_1.logger.warn('[onRestoreRequested] partner notify failed', { error: String(e) });
}
// 2) Recipient self-alert — security signal, bypasses quiet hours, deduped against create-loops.
try {
const now = Date.now();
if (selfAlertAllowed((_d = coupleDoc.data()) === null || _d === void 0 ? void 0 : _d.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) {
log_1.logger.warn('[onRestoreRequested] self-alert failed', { error: String(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 = (0, firestore_1.onDocumentUpdated)('couples/{coupleId}/restore_requests/{recipientUid}', async (event) => {
var _a, _b, _c, _d;
const before = (_a = event.data) === null || _a === void 0 ? void 0 : _a.before.data();
const after = (_b = event.data) === null || _b === void 0 ? void 0 : _b.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 } = event.params;
const db = admin.firestore();
const coupleDoc = await db.collection('couples').doc(coupleId).get();
const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []);
if (!userIds.includes(recipientUid))
return;
log_1.logger.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) {
log_1.logger.warn('[onRestoreFulfilled] self-alert failed', { error: String(e) });
}
});
//# sourceMappingURL=onRestoreRequested.js.map