import { prisma } from "../db"; import { revealDb } from "./db"; import { logOperatorEvent, type OperatorSession } from "./session"; import { sendTo } from "../mail"; /* Revealing a facility's coordinator contacts. * * The console's ordinary role cannot read a coordinator's name, email or phone — the production * probe proves it. Reading one is therefore not a display option but a separate act with four * parts, all of them here and nowhere else: * * 1. a typed reason, kept in full; * 2. a RevealGrant row on the server — the window is thirty minutes and it is the row that says * so, not a claim in a cookie, so it can be shortened or revoked by deleting it; * 3. an OperatorEvent, in the trail that outlives the facility; * 4. an email to the owner, every time. A trail nobody reads is a record, not a control. * * The read itself goes through revealDb(), the ops_reveal role: four columns of one table. A * future edit that widens the select fails at the database, exactly as it would in a projection. * * Scope is the three contact columns of a Facility. Nothing about a wearer is revealable at any * level, and nothing in this module knows a wearer's table exists. */ export const REVEAL_MINUTES = 30; /** One grant covers the three contact columns together; they are one decision and one email. */ export const REVEAL_FIELD = "contacts"; export const REASON_MIN = 8; export const REASON_MAX = 400; export type Revealed = { coordinator: string; coordinatorEmail: string; coordinatorPhone: string; reason: string; at: Date; expiresAt: Date; }; /** The operator's own unexpired grant for this facility, if any. Grants are per operator: one * person's reason does not open the contacts for another. */ export async function activeReveal(operatorId: string, facilityId: string) { return prisma.revealGrant.findFirst({ where: { operatorId, facilityId, field: REVEAL_FIELD, expiresAt: { gt: new Date() } }, orderBy: { expiresAt: "desc" }, select: { id: true, reason: true, at: true, expiresAt: true }, }); } /** The contacts, only while a grant is active. Without one this returns null without touching * the reveal role at all — the grant check comes first, and the read is conditional on it. */ export async function revealedContacts(operatorId: string, facilityId: string): Promise { const g = await activeReveal(operatorId, facilityId); if (!g) return null; const f = await revealDb().facility.findUnique({ where: { id: facilityId }, select: { coordinator: true, coordinatorEmail: true, coordinatorPhone: true }, }); if (!f) return null; return { ...f, reason: g.reason, at: g.at, expiresAt: g.expiresAt }; } /** Where the copy of every reveal goes. The owner's address, settable apart from the operator * who did the revealing so that a second operator's reveals still reach the owner. */ export function alertAddress(fallback: string) { return (process.env.OPS_ALERT_TO || fallback).trim(); } export async function grantReveal(args: { operator: OperatorSession; facilityId: string; facilityName: string; reason: string; ip: string; }): Promise<{ expiresAt: Date; mailed: boolean }> { const { operator, facilityId, facilityName, ip } = args; const reason = args.reason.trim().slice(0, REASON_MAX); const now = new Date(); const expiresAt = new Date(now.getTime() + REVEAL_MINUTES * 60 * 1000); await prisma.revealGrant.create({ data: { operatorId: operator.id, facilityId, field: REVEAL_FIELD, reason, at: now, expiresAt }, }); logOperatorEvent({ operatorId: operator.id, action: "ops:reveal", facilityId, subject: facilityName, detail: reason, ip }); // The email says who, which facility, why and until when — never the contacts themselves. It // is a notice that a reveal happened, and it must be safe to sit in a mailbox. const when = now.toLocaleString("en-AU", { timeZone: "Australia/Brisbane", dateStyle: "medium", timeStyle: "short" }); const until = expiresAt.toLocaleTimeString("en-AU", { timeZone: "Australia/Brisbane", hour: "2-digit", minute: "2-digit" }); const text = [ `${operator.name} (${operator.email}) revealed the coordinator contacts of a facility on the ThreadCount operations console.`, "", `Facility: ${facilityName}`, `When: ${when} (Brisbane)`, `Until: ${until}`, `From: ${ip || "unknown address"}`, `Reason: ${reason}`, "", "This is written to the operator trail. If it wasn't you, sign the operator out and change the password.", ].join("\n"); const mailed = await sendTo(alertAddress(operator.email), `[ops] Contacts revealed — ${facilityName}`, text); if (!mailed) logOperatorEvent({ operatorId: operator.id, action: "ops:reveal.unmailed", facilityId, subject: facilityName, ip }); return { expiresAt, mailed }; }