@aglyn/tenant-data-admin 1.0.0-beta.231 → 1.0.0-beta.233

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { deploymentEnvironmentLabel } from "@aglyn/aglyn/app-utils/deployment-shape";
17
+ import { beaconLoggingTarget } from "./client-error-report.js";
18
+ /**
19
+ * Every operator alert that fires is also written to Cloud Logging
20
+ * (AGL-3683), so the platform's Cloud Monitoring alert policies can page on
21
+ * it and route it to their channels (email, Slack) beside the uptime checks.
22
+ * Vercel stdout never reaches GCP Logging, so a `console.error` is not
23
+ * enough. This writes through the same admin credential the error beacon
24
+ * uses, which `/api/health/error-beacon` already probes.
25
+ *
26
+ * One entry per alert that fires, after its dedupe. That is a handful a day,
27
+ * so it needs no budget. An install with no admin credential writes nothing.
28
+ * Never throws.
29
+ */ export const OPERATOR_ALERT_LOG_ID = 'operator-alerts';
30
+ const WRITE_TIMEOUT_MS = 5000;
31
+ /** A must-know is an error, a should-know a warning, routine a notice. */ const TIER_SEVERITY = {
32
+ must: 'ERROR',
33
+ should: 'WARNING',
34
+ low: 'NOTICE'
35
+ };
36
+ export async function writeOperatorAlertLog(alert) {
37
+ try {
38
+ var _TIER_SEVERITY_alert_tier;
39
+ const target = await beaconLoggingTarget();
40
+ if (!target) return false;
41
+ const response = await fetch('https://logging.googleapis.com/v2/entries:write', {
42
+ method: 'POST',
43
+ headers: {
44
+ Authorization: `Bearer ${target.token}`,
45
+ 'Content-Type': 'application/json'
46
+ },
47
+ body: JSON.stringify({
48
+ logName: `projects/${target.projectId}/logs/${OPERATOR_ALERT_LOG_ID}`,
49
+ resource: {
50
+ type: 'global'
51
+ },
52
+ entries: [
53
+ {
54
+ severity: (_TIER_SEVERITY_alert_tier = TIER_SEVERITY[alert.tier]) != null ? _TIER_SEVERITY_alert_tier : 'WARNING',
55
+ // The alert policy's documentation renders these, so the page
56
+ // says what broke without opening the console.
57
+ labels: {
58
+ type: alert.type,
59
+ tier: alert.tier,
60
+ category: alert.category
61
+ },
62
+ jsonPayload: {
63
+ message: `${alert.title}: ${alert.body}`,
64
+ type: alert.type,
65
+ title: alert.title,
66
+ body: alert.body,
67
+ link: alert.link || undefined,
68
+ orgId: alert.orgId,
69
+ hostId: alert.hostId,
70
+ // A developer's laptop on platform credentials writes here
71
+ // too; the policies match production only.
72
+ environment: deploymentEnvironmentLabel()
73
+ }
74
+ }
75
+ ]
76
+ }),
77
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS)
78
+ });
79
+ if (!response.ok) console.error(`[operator-alerts] cloud logging answered ${response.status}`);
80
+ return response.ok;
81
+ } catch (error) {
82
+ console.error('[operator-alerts] cloud logging unreachable', error);
83
+ return false;
84
+ }
85
+ }
86
+
87
+ //# sourceMappingURL=operator-alerts-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/operator-alerts-log.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { OperatorAlertTier } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport { deploymentEnvironmentLabel } from '@aglyn/aglyn/app-utils/deployment-shape'\nimport { beaconLoggingTarget } from './client-error-report'\nimport type { RenderedOperatorAlert } from './operator-alerts'\n\n/**\n * Every operator alert that fires is also written to Cloud Logging\n * (AGL-3683), so the platform's Cloud Monitoring alert policies can page on\n * it and route it to their channels (email, Slack) beside the uptime checks.\n * Vercel stdout never reaches GCP Logging, so a `console.error` is not\n * enough. This writes through the same admin credential the error beacon\n * uses, which `/api/health/error-beacon` already probes.\n *\n * One entry per alert that fires, after its dedupe. That is a handful a day,\n * so it needs no budget. An install with no admin credential writes nothing.\n * Never throws.\n */\nexport const OPERATOR_ALERT_LOG_ID = 'operator-alerts'\n\nconst WRITE_TIMEOUT_MS = 5_000\n\n/** A must-know is an error, a should-know a warning, routine a notice. */\nconst TIER_SEVERITY: Record<OperatorAlertTier, string> = {\n must: 'ERROR',\n should: 'WARNING',\n low: 'NOTICE',\n}\n\nexport async function writeOperatorAlertLog(alert: RenderedOperatorAlert): Promise<boolean> {\n try {\n const target = await beaconLoggingTarget()\n if (!target) return false\n const response = await fetch('https://logging.googleapis.com/v2/entries:write', {\n method: 'POST',\n headers: { Authorization: `Bearer ${target.token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n logName: `projects/${target.projectId}/logs/${OPERATOR_ALERT_LOG_ID}`,\n resource: { type: 'global' },\n entries: [\n {\n severity: TIER_SEVERITY[alert.tier] ?? 'WARNING',\n // The alert policy's documentation renders these, so the page\n // says what broke without opening the console.\n labels: { type: alert.type, tier: alert.tier, category: alert.category },\n jsonPayload: {\n message: `${alert.title}: ${alert.body}`,\n type: alert.type,\n title: alert.title,\n body: alert.body,\n link: alert.link || undefined,\n orgId: alert.orgId,\n hostId: alert.hostId,\n // A developer's laptop on platform credentials writes here\n // too; the policies match production only.\n environment: deploymentEnvironmentLabel(),\n },\n },\n ],\n }),\n signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),\n })\n if (!response.ok) console.error(`[operator-alerts] cloud logging answered ${response.status}`)\n return response.ok\n } catch (error) {\n console.error('[operator-alerts] cloud logging unreachable', error)\n return false\n }\n}\n"],"names":["deploymentEnvironmentLabel","beaconLoggingTarget","OPERATOR_ALERT_LOG_ID","WRITE_TIMEOUT_MS","TIER_SEVERITY","must","should","low","writeOperatorAlertLog","alert","target","response","fetch","method","headers","Authorization","token","body","JSON","stringify","logName","projectId","resource","type","entries","severity","tier","labels","category","jsonPayload","message","title","link","undefined","orgId","hostId","environment","signal","AbortSignal","timeout","ok","console","error","status"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,0BAA0B,QAAQ,0CAAyC;AACpF,SAASC,mBAAmB,QAAQ,2BAAuB;AAG3D;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,wBAAwB,kBAAiB;AAEtD,MAAMC,mBAAmB;AAEzB,wEAAwE,GACxE,MAAMC,gBAAmD;IACvDC,MAAM;IACNC,QAAQ;IACRC,KAAK;AACP;AAEA,OAAO,eAAeC,sBAAsBC,KAA4B;IACtE,IAAI;YAWgBL;QAVlB,MAAMM,SAAS,MAAMT;QACrB,IAAI,CAACS,QAAQ,OAAO;QACpB,MAAMC,WAAW,MAAMC,MAAM,mDAAmD;YAC9EC,QAAQ;YACRC,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEL,OAAOM,KAAK,EAAE;gBAAE,gBAAgB;YAAmB;YACvFC,MAAMC,KAAKC,SAAS,CAAC;gBACnBC,SAAS,CAAC,SAAS,EAAEV,OAAOW,SAAS,CAAC,MAAM,EAAEnB,uBAAuB;gBACrEoB,UAAU;oBAAEC,MAAM;gBAAS;gBAC3BC,SAAS;oBACP;wBACEC,QAAQ,GAAErB,4BAAAA,aAAa,CAACK,MAAMiB,IAAI,CAAC,YAAzBtB,4BAA6B;wBACvC,8DAA8D;wBAC9D,+CAA+C;wBAC/CuB,QAAQ;4BAAEJ,MAAMd,MAAMc,IAAI;4BAAEG,MAAMjB,MAAMiB,IAAI;4BAAEE,UAAUnB,MAAMmB,QAAQ;wBAAC;wBACvEC,aAAa;4BACXC,SAAS,GAAGrB,MAAMsB,KAAK,CAAC,EAAE,EAAEtB,MAAMQ,IAAI,EAAE;4BACxCM,MAAMd,MAAMc,IAAI;4BAChBQ,OAAOtB,MAAMsB,KAAK;4BAClBd,MAAMR,MAAMQ,IAAI;4BAChBe,MAAMvB,MAAMuB,IAAI,IAAIC;4BACpBC,OAAOzB,MAAMyB,KAAK;4BAClBC,QAAQ1B,MAAM0B,MAAM;4BACpB,2DAA2D;4BAC3D,2CAA2C;4BAC3CC,aAAapC;wBACf;oBACF;iBACD;YACH;YACAqC,QAAQC,YAAYC,OAAO,CAACpC;QAC9B;QACA,IAAI,CAACQ,SAAS6B,EAAE,EAAEC,QAAQC,KAAK,CAAC,CAAC,yCAAyC,EAAE/B,SAASgC,MAAM,EAAE;QAC7F,OAAOhC,SAAS6B,EAAE;IACpB,EAAE,OAAOE,OAAO;QACdD,QAAQC,KAAK,CAAC,+CAA+CA;QAC7D,OAAO;IACT;AACF"}
@@ -428,6 +428,9 @@ export function renderOperatorAlert(definition, options = {}) {
428
428
  outcome: 'console-only',
429
429
  type: typeId
430
430
  };
431
+ // Cloud Logging, for the platform's Cloud Monitoring policies (AGL-3683).
432
+ const { writeOperatorAlertLog } = await import("./operator-alerts-log.js");
433
+ await writeOperatorAlertLog(alert);
431
434
  if (delivery === 'digest') {
432
435
  await queueOperatorAlertForDigest(alert);
433
436
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/operator-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash, createHmac } from 'node:crypto'\nimport {\n effectiveOperatorAlertSetting,\n OPERATOR_ALERT_DEFAULT_NOTIFICATION_TYPE,\n OPERATOR_ALERT_TIER_LABELS,\n operatorAlertLevel,\n renderOperatorAlertTemplate,\n type AglynNotificationType,\n type NotificationLevel,\n type OperatorAlertContext,\n type OperatorAlertDefinition,\n type OperatorAlertSettings,\n type OperatorAlertTypeSetting,\n} from '@aglyn/aglyn/server'\nimport {\n getOperatorAlert,\n registerOperatorAlerts,\n} from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { isEmailConfigured, type SendEmailResult } from '@aglyn/shared-util-email'\nimport firebaseAdmin from './firebase-admin'\n\n/**\n * RAISING AN OPERATOR ALERT (AGL-3377): the one server entry point every\n * alert in the registry goes through.\n *\n * In order, for one call:\n *\n * 1. The type is looked up in the registry (core's catalog plus what plugins\n * registered). A plugin may pass its definition itself, which registers\n * it in this process if its declarations have not loaded yet.\n * 2. A `dedupeKey` is claimed against `operatorAlertState`, so a condition\n * that flaps inside the type's window is told once, and a type with\n * `minOccurrences` fires only once it has recurred that often.\n * 3. The staff console notification is written for every staff member, as\n * `notifyStaff` always has.\n * 4. Staff's switches (`platformSettings/operatorAlerts`, coded defaults\n * until changed) decide the rest: off is console only, `digest` queues it\n * for the once-a-day digest, `immediate` emails the operator now.\n * 5. The email is the `operator-alert` system email, to\n * `resolveStaffAlertRecipients()`; the optional out-of-band webhook\n * (`OPERATOR_ALERT_WEBHOOK_URL`) gets the same alert beside it, so a mail\n * provider that is itself the outage still reaches somebody.\n *\n * Never throws: an alert is raised from inside the failure it reports, and a\n * second failure there must not replace the first.\n */\n\n/** Where staff's per-type switches live. */\nexport const OPERATOR_ALERT_SETTINGS_COLLECTION = 'platformSettings'\nexport const OPERATOR_ALERT_SETTINGS_DOC = 'operatorAlerts'\n/** One document per (type, dedupeKey): when it last fired, how often since. */\nexport const OPERATOR_ALERT_STATE_COLLECTION = 'operatorAlertState'\n/** Alerts waiting for the daily digest. */\nexport const OPERATOR_ALERT_DIGEST_COLLECTION = 'operatorAlertDigest'\n\n/** The hour, UTC, the digest goes out when staff have not chosen one. */\nexport const OPERATOR_ALERT_DIGEST_DEFAULT_HOUR_UTC = 14\n\nexport interface OperatorAlertSettingsDoc extends OperatorAlertSettings {\n /** The hour of the day, UTC, the digest is sent. */\n digestHourUtc?: number\n updatedAtMs?: number\n updatedByEmail?: string | null\n}\n\nexport interface RaiseOperatorAlertOptions {\n /**\n * What makes two raises the SAME alert — a dispute id, an org and month, a\n * check id. Absent never dedupes: every raise is told.\n */\n dedupeKey?: string\n /** Values for the definition's `{{tokens}}`. Never personal data. */\n context?: OperatorAlertContext\n /** A console path or absolute URL; overrides the definition's link. */\n url?: string\n /** The title as written by the caller; overrides the rendered one. */\n subject?: string\n /** The body as written by the caller; overrides the rendered one. */\n body?: string\n /**\n * The console notification's level as the caller judged it (AGL-3437);\n * overrides the definition's. For a caller whose one alert type covers\n * news of more than one tone.\n */\n level?: NotificationLevel\n orgId?: string\n hostId?: string\n}\n\nexport type OperatorAlertOutcome =\n | 'deduped'\n | 'below-threshold'\n | 'console-only'\n | 'queued'\n | 'delivered'\n | 'undelivered'\n\nexport interface OperatorAlertResult {\n outcome: OperatorAlertOutcome\n type: string\n email?: SendEmailResult\n webhook?: OperatorAlertWebhookResult\n}\n\nexport type OperatorAlertWebhookResult =\n | { posted: true; status: number }\n | { posted: false; reason: 'unconfigured' | 'refused' | 'network'; status?: number }\n\n/** One alert, rendered, as it is delivered and queued. */\nexport interface RenderedOperatorAlert {\n type: string\n label: string\n tier: OperatorAlertDefinition['tier']\n category: OperatorAlertDefinition['category']\n title: string\n body: string\n /** Console path or absolute URL; `''` when there is none. */\n link: string\n notificationType: AglynNotificationType\n orgId?: string\n hostId?: string\n}\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\n// ── Settings ────────────────────────────────────────────────────────────────\n\nconst SETTINGS_TTL_MS = 15_000\nlet settingsCache: { at: number; doc: OperatorAlertSettingsDoc } | undefined\n\n/** Drop the in-process settings cache; the console calls it after a write. */\nexport function invalidateOperatorAlertSettingsCache(): void {\n settingsCache = undefined\n}\n\n/** Clamps a stored settings document to what the pipeline understands. */\nexport function normalizeOperatorAlertSettings(\n raw: unknown,\n): OperatorAlertSettingsDoc {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const types: Record<string, OperatorAlertTypeSetting> = {}\n const rawTypes = source['types']\n if (rawTypes && typeof rawTypes === 'object') {\n for (const [type, value] of Object.entries(rawTypes as Record<string, unknown>)) {\n if (!type || !value || typeof value !== 'object') continue\n const entry = value as Record<string, unknown>\n const setting: OperatorAlertTypeSetting = {}\n if (typeof entry['enabled'] === 'boolean') setting.enabled = entry['enabled']\n if (entry['delivery'] === 'immediate' || entry['delivery'] === 'digest') {\n setting.delivery = entry['delivery']\n }\n if (Object.keys(setting).length) types[type] = setting\n }\n }\n const hour = Number(source['digestHourUtc'])\n return {\n types,\n digestHourUtc:\n Number.isInteger(hour) && hour >= 0 && hour <= 23\n ? hour\n : OPERATOR_ALERT_DIGEST_DEFAULT_HOUR_UTC,\n ...(typeof source['updatedAtMs'] === 'number'\n ? { updatedAtMs: source['updatedAtMs'] as number }\n : {}),\n ...(typeof source['updatedByEmail'] === 'string'\n ? { updatedByEmail: source['updatedByEmail'] as string }\n : {}),\n }\n}\n\n/**\n * Staff's switches. An unreadable document is the coded defaults, uncached:\n * an alert must still go out when the settings cannot be read.\n */\nexport async function readOperatorAlertSettings(): Promise<OperatorAlertSettingsDoc> {\n const now = Date.now()\n if (settingsCache && now - settingsCache.at < SETTINGS_TTL_MS) return settingsCache.doc\n try {\n const snapshot = await firestore()\n .collection(OPERATOR_ALERT_SETTINGS_COLLECTION)\n .doc(OPERATOR_ALERT_SETTINGS_DOC)\n .get()\n const doc = normalizeOperatorAlertSettings(snapshot.exists ? snapshot.data() : null)\n settingsCache = { at: now, doc }\n return doc\n } catch (error) {\n console.error('[operator-alerts] settings unreadable; using the defaults', error)\n return normalizeOperatorAlertSettings(null)\n }\n}\n\n// ── Dedupe ──────────────────────────────────────────────────────────────────\n\n/** Keys this instance fired, and until when they stay quiet. */\nconst firedLocally = new Map<string, number>()\nconst FIRED_LOCALLY_MAX = 500\n\nfunction stateDocId(type: string, dedupeKey: string): string {\n return createHash('sha256').update(`${type}\\u0000${dedupeKey}`).digest('hex').slice(0, 40)\n}\n\nexport type OperatorAlertClaim =\n | { fire: true; repeats: number }\n | { fire: false; reason: 'deduped' | 'below-threshold' }\n\n/**\n * Whether a raise with this key fires now. One transaction on the key's\n * state document: inside the window after a fire it is counted and\n * suppressed; before it reaches `minOccurrences` it is counted and held.\n * A store that cannot be read FIRES — a duplicate beats a silence.\n */\nexport async function claimOperatorAlert(\n definition: Pick<OperatorAlertDefinition, 'type' | 'dedupeWindowMinutes' | 'minOccurrences'>,\n dedupeKey: string,\n now = Date.now(),\n): Promise<OperatorAlertClaim> {\n const windowMs = Math.max(0, definition.dedupeWindowMinutes) * 60_000\n const minOccurrences = Math.max(1, Math.floor(definition.minOccurrences ?? 1))\n if (!windowMs) return { fire: true, repeats: 0 }\n const localKey = `${definition.type}\\u0000${dedupeKey}`\n const quietUntil = firedLocally.get(localKey)\n if (quietUntil && quietUntil > now) return { fire: false, reason: 'deduped' }\n try {\n const ref = firestore()\n .collection(OPERATOR_ALERT_STATE_COLLECTION)\n .doc(stateDocId(definition.type, dedupeKey))\n const claim = await firestore().runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(ref)\n const state = (snapshot.exists ? snapshot.data() : {}) as Record<string, unknown>\n const lastFiredAtMs = Number(state['lastFiredAtMs'] ?? 0)\n const base = { type: definition.type, dedupeKey: dedupeKey.slice(0, 300), updatedAtMs: now }\n if (lastFiredAtMs && now - lastFiredAtMs < windowMs) {\n transaction.set(ref, { ...base, suppressed: Number(state['suppressed'] ?? 0) + 1 }, { merge: true })\n return { fire: false, reason: 'deduped' } as const\n }\n const windowStartMs = Number(state['windowStartMs'] ?? 0)\n const inWindow = windowStartMs && now - windowStartMs < windowMs\n const occurrences = (inWindow ? Number(state['occurrences'] ?? 0) : 0) + 1\n if (occurrences < minOccurrences) {\n transaction.set(\n ref,\n { ...base, occurrences, windowStartMs: inWindow ? windowStartMs : now },\n { merge: true },\n )\n return { fire: false, reason: 'below-threshold' } as const\n }\n transaction.set(\n ref,\n {\n ...base,\n lastFiredAtMs: now,\n occurrences: 0,\n windowStartMs: 0,\n suppressed: 0,\n fired: Number(state['fired'] ?? 0) + 1,\n },\n { merge: true },\n )\n return { fire: true, repeats: Number(state['suppressed'] ?? 0) } as const\n })\n if (claim.fire) {\n if (firedLocally.size >= FIRED_LOCALLY_MAX) firedLocally.clear()\n firedLocally.set(localKey, now + windowMs)\n }\n return claim\n } catch (error) {\n console.error(`[operator-alerts] dedupe unavailable for ${definition.type}; firing`, error)\n return { fire: true, repeats: 0 }\n }\n}\n\n// ── Out-of-band webhook ─────────────────────────────────────────────────────\n\nconst WEBHOOK_TIMEOUT_MS = 5_000\n\n/** The webhook URL, when one is configured and is http(s). */\nexport function operatorAlertWebhookUrl(): string {\n const value = String(process.env['OPERATOR_ALERT_WEBHOOK_URL'] ?? '').trim()\n return /^https?:\\/\\/\\S+$/i.test(value) ? value : ''\n}\n\n/**\n * The JSON the webhook receives. `text` is what a Slack-compatible incoming\n * webhook renders on its own; `alert` carries the fields for anything that\n * wants to route on them.\n */\nexport function operatorAlertWebhookPayload(\n alerts: readonly RenderedOperatorAlert[],\n options: { digest?: boolean; now?: number } = {},\n): Record<string, unknown> {\n const origin = consoleOrigin()\n const lines = alerts.map((alert) => {\n const link = operatorAlertAbsoluteLink(alert.link)\n return [\n `[${OPERATOR_ALERT_TIER_LABELS[alert.tier]}] ${alert.title}`,\n alert.body,\n link,\n ]\n .filter(Boolean)\n .join('\\n')\n })\n const text = options.digest\n ? [`Operator digest: ${alerts.length} alert${alerts.length === 1 ? '' : 's'}`, ...lines].join('\\n\\n')\n : lines.join('\\n\\n')\n const occurredAt = new Date(options.now ?? Date.now()).toISOString()\n const shaped = alerts.map((alert) => ({\n type: alert.type,\n tier: alert.tier,\n category: alert.category,\n title: alert.title,\n body: alert.body,\n url: operatorAlertAbsoluteLink(alert.link) || null,\n }))\n return {\n text,\n ...(options.digest ? { digest: shaped } : { alert: shaped[0] ?? null }),\n occurredAt,\n ...(origin ? { install: origin } : {}),\n }\n}\n\n/**\n * Posts alerts to `OPERATOR_ALERT_WEBHOOK_URL`, signed when\n * `OPERATOR_ALERT_WEBHOOK_SECRET` is set: `x-operator-alert-timestamp` is the\n * Unix time and `x-operator-alert-signature` is `sha256=` plus the hex HMAC\n * of `{timestamp}.{body}`. Unset means email only. Never throws.\n */\nexport async function postOperatorAlertWebhook(\n alerts: readonly RenderedOperatorAlert[],\n options: { digest?: boolean; now?: number } = {},\n): Promise<OperatorAlertWebhookResult> {\n const url = operatorAlertWebhookUrl()\n if (!url || !alerts.length) return { posted: false, reason: 'unconfigured' }\n const body = JSON.stringify(operatorAlertWebhookPayload(alerts, options))\n const headers: Record<string, string> = { 'content-type': 'application/json' }\n const secret = String(process.env['OPERATOR_ALERT_WEBHOOK_SECRET'] ?? '').trim()\n if (secret) {\n const timestamp = String(Math.floor((options.now ?? Date.now()) / 1000))\n headers['x-operator-alert-timestamp'] = timestamp\n headers['x-operator-alert-signature'] =\n 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')\n }\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body,\n redirect: 'manual',\n signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS),\n })\n if (response.ok) return { posted: true, status: response.status }\n console.error(`[operator-alerts] webhook answered ${response.status}`)\n return { posted: false, reason: 'refused', status: response.status }\n } catch (error) {\n console.error('[operator-alerts] webhook unreachable', error)\n return { posted: false, reason: 'network' }\n }\n}\n\n// ── Links ───────────────────────────────────────────────────────────────────\n\nfunction consoleOrigin(): string {\n return String(process.env['NEXT_PUBLIC_CONSOLE_URL'] ?? '').trim().replace(/\\/+$/, '')\n}\n\n/** A console path as an inbox or webhook can follow it, or `''`. */\nexport function operatorAlertAbsoluteLink(link: string | undefined | null): string {\n const value = String(link ?? '').trim()\n if (!value) return ''\n if (/^https?:\\/\\//i.test(value)) return value\n const origin = consoleOrigin()\n return origin && value.startsWith('/') ? `${origin}${value}` : ''\n}\n\n/**\n * The definition's link with its tokens filled, or `''` when a token it\n * needs is missing — `/admin/orgs/` would open the wrong page.\n */\nfunction renderLink(template: string | undefined, context: OperatorAlertContext | undefined): string {\n if (!template) return ''\n const missing = [...template.matchAll(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g)].some(([, name]) => {\n const value = context?.[name as string]\n return value === null || value === undefined || value === ''\n })\n return missing ? '' : renderOperatorAlertTemplate(template, context)\n}\n\n// ── Rendering ───────────────────────────────────────────────────────────────\n\n/** Stands in for a type nobody registered, so it is still told. */\nfunction unregisteredDefinition(type: string): OperatorAlertDefinition {\n return {\n type,\n label: type,\n description: 'An alert type this install has no registry entry for.',\n tier: 'should',\n category: 'ops',\n title: '{{title}}',\n body: '{{body}}',\n delivery: 'immediate',\n dedupeWindowMinutes: 60,\n defaultEnabled: true,\n }\n}\n\n/** Resolves the definition a call names, registering a plugin's own. */\nexport function resolveOperatorAlertDefinition(\n type: string | OperatorAlertDefinition,\n): OperatorAlertDefinition {\n if (typeof type !== 'string') {\n const known = getOperatorAlert(type.type)\n if (known) return known\n if (type.pluginId) {\n try {\n registerOperatorAlerts([type], { pluginId: type.pluginId })\n } catch (error) {\n console.error(`[operator-alerts] ${type.type} could not be registered`, error)\n }\n }\n return type\n }\n const known = getOperatorAlert(type)\n if (known) return known\n console.error(`[operator-alerts] \"${type}\" is not a registered alert type; telling it anyway`)\n return unregisteredDefinition(type)\n}\n\nexport function renderOperatorAlert(\n definition: OperatorAlertDefinition,\n options: RaiseOperatorAlertOptions = {},\n): RenderedOperatorAlert {\n const context: OperatorAlertContext = {\n ...(options.subject ? { title: options.subject } : {}),\n ...(options.body ? { body: options.body } : {}),\n ...(options.context ?? {}),\n }\n const title =\n String(options.subject ?? '').trim() ||\n renderOperatorAlertTemplate(definition.title, context) ||\n definition.label\n const body =\n String(options.body ?? '').trim() || renderOperatorAlertTemplate(definition.body, context)\n return {\n type: definition.type,\n label: definition.label,\n tier: definition.tier,\n category: definition.category,\n title,\n body,\n link: String(options.url ?? '').trim() || renderLink(definition.link, context),\n notificationType: definition.notificationType ?? OPERATOR_ALERT_DEFAULT_NOTIFICATION_TYPE,\n ...(options.orgId ? { orgId: options.orgId } : {}),\n ...(options.hostId ? { hostId: options.hostId } : {}),\n }\n}\n\n// ── The entry point ─────────────────────────────────────────────────────────\n\n/** Whether an email to the operator can go at all on this install. */\nasync function operatorEmailReachable(): Promise<boolean> {\n if (!isEmailConfigured()) return false\n const { resolveStaffAlertRecipients } = await import('./staff-alert-email')\n return (await resolveStaffAlertRecipients()).length > 0\n}\n\n/** See the module comment. Never throws. */\nexport async function raiseOperatorAlert(\n type: string | OperatorAlertDefinition,\n options: RaiseOperatorAlertOptions = {},\n): Promise<OperatorAlertResult> {\n const typeId = typeof type === 'string' ? type : type.type\n try {\n const definition = resolveOperatorAlertDefinition(type)\n const dedupeKey = String(options.dedupeKey ?? '').trim()\n let repeats = 0\n if (dedupeKey) {\n const claim = await claimOperatorAlert(definition, dedupeKey)\n if (claim.fire === false) return { outcome: claim.reason, type: typeId }\n repeats = claim.repeats\n }\n const alert = renderOperatorAlert(definition, options)\n if (repeats > 0) {\n alert.body = [alert.body, `Raised ${repeats} more time${repeats === 1 ? '' : 's'} since it was last told.`]\n .filter(Boolean)\n .join(' ')\n }\n const settings = await readOperatorAlertSettings()\n const { enabled, delivery } = effectiveOperatorAlertSetting(definition, settings)\n const emailNow = enabled && delivery === 'immediate' && (await operatorEmailReachable())\n\n const { notifyStaffConsole } = await import('./notifications')\n await notifyStaffConsole(\n {\n type: alert.notificationType,\n title: alert.title,\n ...(alert.body ? { body: alert.body } : {}),\n level: options.level ?? operatorAlertLevel(definition),\n ...(alert.link && alert.link.startsWith('/') ? { link: alert.link } : {}),\n ...(alert.orgId ? { orgId: alert.orgId } : {}),\n ...(alert.hostId ? { hostId: alert.hostId } : {}),\n },\n // The operator is emailed; a staff member who also switched email on\n // for this category is not mailed the same alert a second time.\n emailNow ? { skipEmail: true } : {},\n )\n if (!enabled) return { outcome: 'console-only', type: typeId }\n if (delivery === 'digest') {\n await queueOperatorAlertForDigest(alert)\n return { outcome: 'queued', type: typeId }\n }\n return await deliverOperatorAlert(alert, { emailNow })\n } catch (error) {\n console.error(`[operator-alerts] ${typeId} could not be raised`, error)\n return { outcome: 'undelivered', type: typeId }\n }\n}\n\n/** Emails and posts one alert now. */\nasync function deliverOperatorAlert(\n alert: RenderedOperatorAlert,\n options: { emailNow: boolean },\n): Promise<OperatorAlertResult> {\n const { sendOperatorAlertEmail } = await import('./staff-alert-email')\n const [email, webhook] = await Promise.all([\n options.emailNow\n ? sendOperatorAlertEmail({\n title: alert.title,\n body: alert.body,\n url: operatorAlertAbsoluteLink(alert.link),\n context: `operator-alert ${alert.type}`,\n })\n : Promise.resolve<SendEmailResult>({ sent: false, reason: 'unconfigured' }),\n postOperatorAlertWebhook([alert]),\n ])\n return {\n outcome: email.sent || webhook.posted ? 'delivered' : 'undelivered',\n type: alert.type,\n email,\n webhook,\n }\n}\n\n// ── The digest ──────────────────────────────────────────────────────────────\n\nasync function queueOperatorAlertForDigest(alert: RenderedOperatorAlert): Promise<void> {\n try {\n await firestore()\n .collection(OPERATOR_ALERT_DIGEST_COLLECTION)\n .doc(createResourceUid())\n .set({ ...alert, atMs: Date.now() })\n } catch (error) {\n // The queue is the only record the digest will have; with it gone, the\n // alert goes now rather than never.\n console.error(`[operator-alerts] digest queue unavailable; sending ${alert.type} now`, error)\n await deliverOperatorAlert(alert, { emailNow: await operatorEmailReachable() })\n }\n}\n\n/** At most this many alerts are listed in one digest; the rest are counted. */\nexport const OPERATOR_DIGEST_MAX_LISTED = 50\nconst DIGEST_READ_LIMIT = 500\n\nexport interface OperatorDigestResult {\n /** Whether a digest went out (or, on a dry run, would). */\n sent: boolean\n reason?: 'not-due' | 'already-sent' | 'empty' | 'dry-run' | 'unavailable'\n count: number\n email?: SendEmailResult\n webhook?: OperatorAlertWebhookResult\n}\n\nfunction dayKey(now: number): string {\n return new Date(now).toISOString().slice(0, 10)\n}\n\n/** The state document that claims one UTC day's digest. */\nexport function operatorDigestMarkerId(now: number): string {\n return `digest-${dayKey(now)}`\n}\n\n/** The digest's body: one paragraph per alert, newest last. */\nexport function composeOperatorDigestBody(\n alerts: readonly RenderedOperatorAlert[],\n total = alerts.length,\n): string {\n const listed = alerts.slice(0, OPERATOR_DIGEST_MAX_LISTED).map((alert) => {\n const link = operatorAlertAbsoluteLink(alert.link)\n return [`• ${alert.title}`, alert.body, link].filter(Boolean).join('\\n')\n })\n const rest = total - listed.length\n if (rest > 0) listed.push(`…and ${rest} more in the staff console.`)\n return listed.join('\\n\\n')\n}\n\n/**\n * Sends the day's digest once the configured hour has passed: every queued\n * alert in one `operator-alert-digest` email and one webhook post, then\n * clears what it sent. Once per UTC day, claimed by creating\n * `operatorAlertState/digest-{day}`, so two ticks never both send it.\n */\nexport async function sendOperatorAlertDigest(\n options: { now?: number; dryRun?: boolean; force?: boolean } = {},\n): Promise<OperatorDigestResult> {\n const now = options.now ?? Date.now()\n try {\n const settings = await readOperatorAlertSettings()\n const hour = settings.digestHourUtc ?? OPERATOR_ALERT_DIGEST_DEFAULT_HOUR_UTC\n if (!options.force && new Date(now).getUTCHours() < hour) {\n return { sent: false, reason: 'not-due', count: 0 }\n }\n const collection = firestore().collection(OPERATOR_ALERT_DIGEST_COLLECTION)\n const marker = firestore()\n .collection(OPERATOR_ALERT_STATE_COLLECTION)\n .doc(operatorDigestMarkerId(now))\n if (!options.force && (await marker.get()).exists) {\n return { sent: false, reason: 'already-sent', count: 0 }\n }\n const snapshot = await collection.orderBy('atMs', 'asc').limit(DIGEST_READ_LIMIT).get()\n const queued = snapshot.docs\n if (!queued.length) return { sent: false, reason: 'empty', count: 0 }\n if (options.dryRun) return { sent: false, reason: 'dry-run', count: queued.length }\n if (!options.force) {\n try {\n await marker.create({ atMs: now, count: queued.length })\n } catch {\n return { sent: false, reason: 'already-sent', count: 0 }\n }\n }\n const alerts = queued.map((doc: any) => doc.data() as RenderedOperatorAlert)\n const { sendOperatorAlertDigestEmail } = await import('./staff-alert-email')\n const [email, webhook] = await Promise.all([\n sendOperatorAlertDigestEmail({\n date: dayKey(now),\n count: alerts.length,\n body: composeOperatorDigestBody(alerts),\n url: operatorAlertAbsoluteLink('/admin/operator-alerts'),\n }),\n postOperatorAlertWebhook(alerts, { digest: true, now }),\n ])\n if (email.sent || webhook.posted) {\n const batch = firestore().batch()\n for (const doc of queued) batch.delete(doc.ref)\n await batch.commit()\n } else if (!options.force) {\n // Nothing reached anybody: release the day so the next tick retries.\n await marker.delete().catch(() => undefined)\n }\n return { sent: email.sent || webhook.posted, count: alerts.length, email, webhook }\n } catch (error) {\n console.error('[operator-alerts] digest failed', error)\n return { sent: false, reason: 'unavailable', count: 0 }\n }\n}\n\n\n// ── The staff page ──────────────────────────────────────────────────────────\n\n/** One registry entry as Staff → Operator alerts lists it. */\nexport interface OperatorAlertCatalogRow {\n type: string\n label: string\n description: string\n tier: OperatorAlertDefinition['tier']\n category: OperatorAlertDefinition['category']\n pluginId: string | null\n defaults: { enabled: boolean; delivery: OperatorAlertDefinition['delivery'] }\n effective: { enabled: boolean; delivery: OperatorAlertDefinition['delivery'] }\n /** Whether staff have answered for this type, rather than the default. */\n overridden: boolean\n}\n\n/** Every registered type with its default and its effective setting. */\nexport function operatorAlertCatalogRows(\n definitions: readonly OperatorAlertDefinition[],\n settings: OperatorAlertSettingsDoc,\n): OperatorAlertCatalogRow[] {\n return definitions.map((definition) => ({\n type: definition.type,\n label: definition.label,\n description: definition.description,\n tier: definition.tier,\n category: definition.category,\n pluginId: definition.pluginId ?? null,\n defaults: { enabled: definition.defaultEnabled, delivery: definition.delivery },\n effective: effectiveOperatorAlertSetting(definition, settings),\n overridden: Boolean(settings.types?.[definition.type]),\n }))\n}\n\n/**\n * Sends one `[Test]` alert of a type straight to the operator's inbox and\n * webhook, past dedupe and staff's switches, so an operator can prove the\n * channels reach them. Writes no console notification.\n */\nexport async function sendOperatorAlertTest(\n definition: OperatorAlertDefinition,\n): Promise<{ email: SendEmailResult; webhook: OperatorAlertWebhookResult }> {\n const sample: Record<string, string> = {}\n for (const [, name] of `${definition.title} ${definition.body} ${definition.link ?? ''}`.matchAll(\n /\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g,\n )) {\n sample[name as string] = `sample ${name}`\n }\n const alert = renderOperatorAlert(definition, { context: sample })\n alert.title = `[Test] ${alert.title}`\n alert.body = [\n alert.body,\n 'This is a test of the operator alert channels; nothing happened.',\n ].join(' ')\n const { sendOperatorAlertEmail } = await import('./staff-alert-email')\n const [email, webhook] = await Promise.all([\n isEmailConfigured()\n ? sendOperatorAlertEmail({\n title: alert.title,\n body: alert.body,\n url: operatorAlertAbsoluteLink(alert.link) || operatorAlertAbsoluteLink('/admin/operator-alerts'),\n context: `operator-alert-test ${definition.type}`,\n })\n : Promise.resolve<SendEmailResult>({ sent: false, reason: 'unconfigured' }),\n postOperatorAlertWebhook([alert]),\n ])\n return { email, webhook }\n}\n\n/** Where operator mail goes, named but never listed, for the staff page. */\nexport async function describeOperatorAlertRecipients(): Promise<{\n source: 'STAFF_ALERT_EMAIL' | 'NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL' | 'staff-accounts' | 'none'\n count: number\n}> {\n const { resolveStaffAlertRecipients } = await import('./staff-alert-email')\n const recipients = await resolveStaffAlertRecipients()\n if (!recipients.length) return { source: 'none', count: 0 }\n const configured = String(process.env['STAFF_ALERT_EMAIL'] ?? '').trim().toLowerCase()\n if (configured.includes('@') && recipients[0] === configured) {\n return { source: 'STAFF_ALERT_EMAIL', count: 1 }\n }\n const support = String(process.env['NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL'] ?? '').trim().toLowerCase()\n if (support.includes('@') && recipients[0] === support) {\n return { source: 'NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL', count: 1 }\n }\n return { source: 'staff-accounts', count: recipients.length }\n}\n"],"names":["createHash","createHmac","effectiveOperatorAlertSetting","OPERATOR_ALERT_DEFAULT_NOTIFICATION_TYPE","OPERATOR_ALERT_TIER_LABELS","operatorAlertLevel","renderOperatorAlertTemplate","getOperatorAlert","registerOperatorAlerts","createResourceUid","isEmailConfigured","firebaseAdmin","OPERATOR_ALERT_SETTINGS_COLLECTION","OPERATOR_ALERT_SETTINGS_DOC","OPERATOR_ALERT_STATE_COLLECTION","OPERATOR_ALERT_DIGEST_COLLECTION","OPERATOR_ALERT_DIGEST_DEFAULT_HOUR_UTC","firestore","app","SETTINGS_TTL_MS","settingsCache","invalidateOperatorAlertSettingsCache","undefined","normalizeOperatorAlertSettings","raw","source","types","rawTypes","type","value","Object","entries","entry","setting","enabled","delivery","keys","length","hour","Number","digestHourUtc","isInteger","updatedAtMs","updatedByEmail","readOperatorAlertSettings","now","Date","at","doc","snapshot","collection","get","exists","data","error","console","firedLocally","Map","FIRED_LOCALLY_MAX","stateDocId","dedupeKey","update","digest","slice","claimOperatorAlert","definition","windowMs","Math","max","dedupeWindowMinutes","minOccurrences","floor","fire","repeats","localKey","quietUntil","reason","ref","claim","runTransaction","transaction","state","lastFiredAtMs","base","set","suppressed","merge","windowStartMs","inWindow","occurrences","fired","size","clear","WEBHOOK_TIMEOUT_MS","operatorAlertWebhookUrl","process","String","env","trim","test","operatorAlertWebhookPayload","alerts","options","shaped","origin","consoleOrigin","lines","map","alert","link","operatorAlertAbsoluteLink","tier","title","body","filter","Boolean","join","text","occurredAt","toISOString","category","url","install","postOperatorAlertWebhook","posted","JSON","stringify","headers","secret","timestamp","response","fetch","method","redirect","signal","AbortSignal","timeout","ok","status","replace","startsWith","renderLink","template","context","missing","matchAll","some","name","unregisteredDefinition","label","description","defaultEnabled","resolveOperatorAlertDefinition","known","pluginId","renderOperatorAlert","subject","notificationType","orgId","hostId","operatorEmailReachable","resolveStaffAlertRecipients","raiseOperatorAlert","typeId","outcome","settings","emailNow","notifyStaffConsole","level","skipEmail","queueOperatorAlertForDigest","deliverOperatorAlert","sendOperatorAlertEmail","email","webhook","Promise","all","resolve","sent","atMs","OPERATOR_DIGEST_MAX_LISTED","DIGEST_READ_LIMIT","dayKey","operatorDigestMarkerId","composeOperatorDigestBody","total","listed","rest","push","sendOperatorAlertDigest","force","getUTCHours","count","marker","orderBy","limit","queued","docs","dryRun","create","sendOperatorAlertDigestEmail","date","batch","delete","commit","catch","operatorAlertCatalogRows","definitions","defaults","effective","overridden","sendOperatorAlertTest","sample","describeOperatorAlertRecipients","recipients","configured","toLowerCase","includes","support"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/operator-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash, createHmac } from 'node:crypto'\nimport {\n effectiveOperatorAlertSetting,\n OPERATOR_ALERT_DEFAULT_NOTIFICATION_TYPE,\n OPERATOR_ALERT_TIER_LABELS,\n operatorAlertLevel,\n renderOperatorAlertTemplate,\n type AglynNotificationType,\n type NotificationLevel,\n type OperatorAlertContext,\n type OperatorAlertDefinition,\n type OperatorAlertSettings,\n type OperatorAlertTypeSetting,\n} from '@aglyn/aglyn/server'\nimport {\n getOperatorAlert,\n registerOperatorAlerts,\n} from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { isEmailConfigured, type SendEmailResult } from '@aglyn/shared-util-email'\nimport firebaseAdmin from './firebase-admin'\n\n/**\n * RAISING AN OPERATOR ALERT (AGL-3377): the one server entry point every\n * alert in the registry goes through.\n *\n * In order, for one call:\n *\n * 1. The type is looked up in the registry (core's catalog plus what plugins\n * registered). A plugin may pass its definition itself, which registers\n * it in this process if its declarations have not loaded yet.\n * 2. A `dedupeKey` is claimed against `operatorAlertState`, so a condition\n * that flaps inside the type's window is told once, and a type with\n * `minOccurrences` fires only once it has recurred that often.\n * 3. The staff console notification is written for every staff member, as\n * `notifyStaff` always has.\n * 4. Staff's switches (`platformSettings/operatorAlerts`, coded defaults\n * until changed) decide the rest: off is console only, `digest` queues it\n * for the once-a-day digest, `immediate` emails the operator now.\n * 5. The email is the `operator-alert` system email, to\n * `resolveStaffAlertRecipients()`; the optional out-of-band webhook\n * (`OPERATOR_ALERT_WEBHOOK_URL`) gets the same alert beside it, so a mail\n * provider that is itself the outage still reaches somebody.\n *\n * Never throws: an alert is raised from inside the failure it reports, and a\n * second failure there must not replace the first.\n */\n\n/** Where staff's per-type switches live. */\nexport const OPERATOR_ALERT_SETTINGS_COLLECTION = 'platformSettings'\nexport const OPERATOR_ALERT_SETTINGS_DOC = 'operatorAlerts'\n/** One document per (type, dedupeKey): when it last fired, how often since. */\nexport const OPERATOR_ALERT_STATE_COLLECTION = 'operatorAlertState'\n/** Alerts waiting for the daily digest. */\nexport const OPERATOR_ALERT_DIGEST_COLLECTION = 'operatorAlertDigest'\n\n/** The hour, UTC, the digest goes out when staff have not chosen one. */\nexport const OPERATOR_ALERT_DIGEST_DEFAULT_HOUR_UTC = 14\n\nexport interface OperatorAlertSettingsDoc extends OperatorAlertSettings {\n /** The hour of the day, UTC, the digest is sent. */\n digestHourUtc?: number\n updatedAtMs?: number\n updatedByEmail?: string | null\n}\n\nexport interface RaiseOperatorAlertOptions {\n /**\n * What makes two raises the SAME alert — a dispute id, an org and month, a\n * check id. Absent never dedupes: every raise is told.\n */\n dedupeKey?: string\n /** Values for the definition's `{{tokens}}`. Never personal data. */\n context?: OperatorAlertContext\n /** A console path or absolute URL; overrides the definition's link. */\n url?: string\n /** The title as written by the caller; overrides the rendered one. */\n subject?: string\n /** The body as written by the caller; overrides the rendered one. */\n body?: string\n /**\n * The console notification's level as the caller judged it (AGL-3437);\n * overrides the definition's. For a caller whose one alert type covers\n * news of more than one tone.\n */\n level?: NotificationLevel\n orgId?: string\n hostId?: string\n}\n\nexport type OperatorAlertOutcome =\n | 'deduped'\n | 'below-threshold'\n | 'console-only'\n | 'queued'\n | 'delivered'\n | 'undelivered'\n\nexport interface OperatorAlertResult {\n outcome: OperatorAlertOutcome\n type: string\n email?: SendEmailResult\n webhook?: OperatorAlertWebhookResult\n}\n\nexport type OperatorAlertWebhookResult =\n | { posted: true; status: number }\n | { posted: false; reason: 'unconfigured' | 'refused' | 'network'; status?: number }\n\n/** One alert, rendered, as it is delivered and queued. */\nexport interface RenderedOperatorAlert {\n type: string\n label: string\n tier: OperatorAlertDefinition['tier']\n category: OperatorAlertDefinition['category']\n title: string\n body: string\n /** Console path or absolute URL; `''` when there is none. */\n link: string\n notificationType: AglynNotificationType\n orgId?: string\n hostId?: string\n}\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\n// ── Settings ────────────────────────────────────────────────────────────────\n\nconst SETTINGS_TTL_MS = 15_000\nlet settingsCache: { at: number; doc: OperatorAlertSettingsDoc } | undefined\n\n/** Drop the in-process settings cache; the console calls it after a write. */\nexport function invalidateOperatorAlertSettingsCache(): void {\n settingsCache = undefined\n}\n\n/** Clamps a stored settings document to what the pipeline understands. */\nexport function normalizeOperatorAlertSettings(\n raw: unknown,\n): OperatorAlertSettingsDoc {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const types: Record<string, OperatorAlertTypeSetting> = {}\n const rawTypes = source['types']\n if (rawTypes && typeof rawTypes === 'object') {\n for (const [type, value] of Object.entries(rawTypes as Record<string, unknown>)) {\n if (!type || !value || typeof value !== 'object') continue\n const entry = value as Record<string, unknown>\n const setting: OperatorAlertTypeSetting = {}\n if (typeof entry['enabled'] === 'boolean') setting.enabled = entry['enabled']\n if (entry['delivery'] === 'immediate' || entry['delivery'] === 'digest') {\n setting.delivery = entry['delivery']\n }\n if (Object.keys(setting).length) types[type] = setting\n }\n }\n const hour = Number(source['digestHourUtc'])\n return {\n types,\n digestHourUtc:\n Number.isInteger(hour) && hour >= 0 && hour <= 23\n ? hour\n : OPERATOR_ALERT_DIGEST_DEFAULT_HOUR_UTC,\n ...(typeof source['updatedAtMs'] === 'number'\n ? { updatedAtMs: source['updatedAtMs'] as number }\n : {}),\n ...(typeof source['updatedByEmail'] === 'string'\n ? { updatedByEmail: source['updatedByEmail'] as string }\n : {}),\n }\n}\n\n/**\n * Staff's switches. An unreadable document is the coded defaults, uncached:\n * an alert must still go out when the settings cannot be read.\n */\nexport async function readOperatorAlertSettings(): Promise<OperatorAlertSettingsDoc> {\n const now = Date.now()\n if (settingsCache && now - settingsCache.at < SETTINGS_TTL_MS) return settingsCache.doc\n try {\n const snapshot = await firestore()\n .collection(OPERATOR_ALERT_SETTINGS_COLLECTION)\n .doc(OPERATOR_ALERT_SETTINGS_DOC)\n .get()\n const doc = normalizeOperatorAlertSettings(snapshot.exists ? snapshot.data() : null)\n settingsCache = { at: now, doc }\n return doc\n } catch (error) {\n console.error('[operator-alerts] settings unreadable; using the defaults', error)\n return normalizeOperatorAlertSettings(null)\n }\n}\n\n// ── Dedupe ──────────────────────────────────────────────────────────────────\n\n/** Keys this instance fired, and until when they stay quiet. */\nconst firedLocally = new Map<string, number>()\nconst FIRED_LOCALLY_MAX = 500\n\nfunction stateDocId(type: string, dedupeKey: string): string {\n return createHash('sha256').update(`${type}\\u0000${dedupeKey}`).digest('hex').slice(0, 40)\n}\n\nexport type OperatorAlertClaim =\n | { fire: true; repeats: number }\n | { fire: false; reason: 'deduped' | 'below-threshold' }\n\n/**\n * Whether a raise with this key fires now. One transaction on the key's\n * state document: inside the window after a fire it is counted and\n * suppressed; before it reaches `minOccurrences` it is counted and held.\n * A store that cannot be read FIRES — a duplicate beats a silence.\n */\nexport async function claimOperatorAlert(\n definition: Pick<OperatorAlertDefinition, 'type' | 'dedupeWindowMinutes' | 'minOccurrences'>,\n dedupeKey: string,\n now = Date.now(),\n): Promise<OperatorAlertClaim> {\n const windowMs = Math.max(0, definition.dedupeWindowMinutes) * 60_000\n const minOccurrences = Math.max(1, Math.floor(definition.minOccurrences ?? 1))\n if (!windowMs) return { fire: true, repeats: 0 }\n const localKey = `${definition.type}\\u0000${dedupeKey}`\n const quietUntil = firedLocally.get(localKey)\n if (quietUntil && quietUntil > now) return { fire: false, reason: 'deduped' }\n try {\n const ref = firestore()\n .collection(OPERATOR_ALERT_STATE_COLLECTION)\n .doc(stateDocId(definition.type, dedupeKey))\n const claim = await firestore().runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(ref)\n const state = (snapshot.exists ? snapshot.data() : {}) as Record<string, unknown>\n const lastFiredAtMs = Number(state['lastFiredAtMs'] ?? 0)\n const base = { type: definition.type, dedupeKey: dedupeKey.slice(0, 300), updatedAtMs: now }\n if (lastFiredAtMs && now - lastFiredAtMs < windowMs) {\n transaction.set(ref, { ...base, suppressed: Number(state['suppressed'] ?? 0) + 1 }, { merge: true })\n return { fire: false, reason: 'deduped' } as const\n }\n const windowStartMs = Number(state['windowStartMs'] ?? 0)\n const inWindow = windowStartMs && now - windowStartMs < windowMs\n const occurrences = (inWindow ? Number(state['occurrences'] ?? 0) : 0) + 1\n if (occurrences < minOccurrences) {\n transaction.set(\n ref,\n { ...base, occurrences, windowStartMs: inWindow ? windowStartMs : now },\n { merge: true },\n )\n return { fire: false, reason: 'below-threshold' } as const\n }\n transaction.set(\n ref,\n {\n ...base,\n lastFiredAtMs: now,\n occurrences: 0,\n windowStartMs: 0,\n suppressed: 0,\n fired: Number(state['fired'] ?? 0) + 1,\n },\n { merge: true },\n )\n return { fire: true, repeats: Number(state['suppressed'] ?? 0) } as const\n })\n if (claim.fire) {\n if (firedLocally.size >= FIRED_LOCALLY_MAX) firedLocally.clear()\n firedLocally.set(localKey, now + windowMs)\n }\n return claim\n } catch (error) {\n console.error(`[operator-alerts] dedupe unavailable for ${definition.type}; firing`, error)\n return { fire: true, repeats: 0 }\n }\n}\n\n// ── Out-of-band webhook ─────────────────────────────────────────────────────\n\nconst WEBHOOK_TIMEOUT_MS = 5_000\n\n/** The webhook URL, when one is configured and is http(s). */\nexport function operatorAlertWebhookUrl(): string {\n const value = String(process.env['OPERATOR_ALERT_WEBHOOK_URL'] ?? '').trim()\n return /^https?:\\/\\/\\S+$/i.test(value) ? value : ''\n}\n\n/**\n * The JSON the webhook receives. `text` is what a Slack-compatible incoming\n * webhook renders on its own; `alert` carries the fields for anything that\n * wants to route on them.\n */\nexport function operatorAlertWebhookPayload(\n alerts: readonly RenderedOperatorAlert[],\n options: { digest?: boolean; now?: number } = {},\n): Record<string, unknown> {\n const origin = consoleOrigin()\n const lines = alerts.map((alert) => {\n const link = operatorAlertAbsoluteLink(alert.link)\n return [\n `[${OPERATOR_ALERT_TIER_LABELS[alert.tier]}] ${alert.title}`,\n alert.body,\n link,\n ]\n .filter(Boolean)\n .join('\\n')\n })\n const text = options.digest\n ? [`Operator digest: ${alerts.length} alert${alerts.length === 1 ? '' : 's'}`, ...lines].join('\\n\\n')\n : lines.join('\\n\\n')\n const occurredAt = new Date(options.now ?? Date.now()).toISOString()\n const shaped = alerts.map((alert) => ({\n type: alert.type,\n tier: alert.tier,\n category: alert.category,\n title: alert.title,\n body: alert.body,\n url: operatorAlertAbsoluteLink(alert.link) || null,\n }))\n return {\n text,\n ...(options.digest ? { digest: shaped } : { alert: shaped[0] ?? null }),\n occurredAt,\n ...(origin ? { install: origin } : {}),\n }\n}\n\n/**\n * Posts alerts to `OPERATOR_ALERT_WEBHOOK_URL`, signed when\n * `OPERATOR_ALERT_WEBHOOK_SECRET` is set: `x-operator-alert-timestamp` is the\n * Unix time and `x-operator-alert-signature` is `sha256=` plus the hex HMAC\n * of `{timestamp}.{body}`. Unset means email only. Never throws.\n */\nexport async function postOperatorAlertWebhook(\n alerts: readonly RenderedOperatorAlert[],\n options: { digest?: boolean; now?: number } = {},\n): Promise<OperatorAlertWebhookResult> {\n const url = operatorAlertWebhookUrl()\n if (!url || !alerts.length) return { posted: false, reason: 'unconfigured' }\n const body = JSON.stringify(operatorAlertWebhookPayload(alerts, options))\n const headers: Record<string, string> = { 'content-type': 'application/json' }\n const secret = String(process.env['OPERATOR_ALERT_WEBHOOK_SECRET'] ?? '').trim()\n if (secret) {\n const timestamp = String(Math.floor((options.now ?? Date.now()) / 1000))\n headers['x-operator-alert-timestamp'] = timestamp\n headers['x-operator-alert-signature'] =\n 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')\n }\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body,\n redirect: 'manual',\n signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS),\n })\n if (response.ok) return { posted: true, status: response.status }\n console.error(`[operator-alerts] webhook answered ${response.status}`)\n return { posted: false, reason: 'refused', status: response.status }\n } catch (error) {\n console.error('[operator-alerts] webhook unreachable', error)\n return { posted: false, reason: 'network' }\n }\n}\n\n// ── Links ───────────────────────────────────────────────────────────────────\n\nfunction consoleOrigin(): string {\n return String(process.env['NEXT_PUBLIC_CONSOLE_URL'] ?? '').trim().replace(/\\/+$/, '')\n}\n\n/** A console path as an inbox or webhook can follow it, or `''`. */\nexport function operatorAlertAbsoluteLink(link: string | undefined | null): string {\n const value = String(link ?? '').trim()\n if (!value) return ''\n if (/^https?:\\/\\//i.test(value)) return value\n const origin = consoleOrigin()\n return origin && value.startsWith('/') ? `${origin}${value}` : ''\n}\n\n/**\n * The definition's link with its tokens filled, or `''` when a token it\n * needs is missing — `/admin/orgs/` would open the wrong page.\n */\nfunction renderLink(template: string | undefined, context: OperatorAlertContext | undefined): string {\n if (!template) return ''\n const missing = [...template.matchAll(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g)].some(([, name]) => {\n const value = context?.[name as string]\n return value === null || value === undefined || value === ''\n })\n return missing ? '' : renderOperatorAlertTemplate(template, context)\n}\n\n// ── Rendering ───────────────────────────────────────────────────────────────\n\n/** Stands in for a type nobody registered, so it is still told. */\nfunction unregisteredDefinition(type: string): OperatorAlertDefinition {\n return {\n type,\n label: type,\n description: 'An alert type this install has no registry entry for.',\n tier: 'should',\n category: 'ops',\n title: '{{title}}',\n body: '{{body}}',\n delivery: 'immediate',\n dedupeWindowMinutes: 60,\n defaultEnabled: true,\n }\n}\n\n/** Resolves the definition a call names, registering a plugin's own. */\nexport function resolveOperatorAlertDefinition(\n type: string | OperatorAlertDefinition,\n): OperatorAlertDefinition {\n if (typeof type !== 'string') {\n const known = getOperatorAlert(type.type)\n if (known) return known\n if (type.pluginId) {\n try {\n registerOperatorAlerts([type], { pluginId: type.pluginId })\n } catch (error) {\n console.error(`[operator-alerts] ${type.type} could not be registered`, error)\n }\n }\n return type\n }\n const known = getOperatorAlert(type)\n if (known) return known\n console.error(`[operator-alerts] \"${type}\" is not a registered alert type; telling it anyway`)\n return unregisteredDefinition(type)\n}\n\nexport function renderOperatorAlert(\n definition: OperatorAlertDefinition,\n options: RaiseOperatorAlertOptions = {},\n): RenderedOperatorAlert {\n const context: OperatorAlertContext = {\n ...(options.subject ? { title: options.subject } : {}),\n ...(options.body ? { body: options.body } : {}),\n ...(options.context ?? {}),\n }\n const title =\n String(options.subject ?? '').trim() ||\n renderOperatorAlertTemplate(definition.title, context) ||\n definition.label\n const body =\n String(options.body ?? '').trim() || renderOperatorAlertTemplate(definition.body, context)\n return {\n type: definition.type,\n label: definition.label,\n tier: definition.tier,\n category: definition.category,\n title,\n body,\n link: String(options.url ?? '').trim() || renderLink(definition.link, context),\n notificationType: definition.notificationType ?? OPERATOR_ALERT_DEFAULT_NOTIFICATION_TYPE,\n ...(options.orgId ? { orgId: options.orgId } : {}),\n ...(options.hostId ? { hostId: options.hostId } : {}),\n }\n}\n\n// ── The entry point ─────────────────────────────────────────────────────────\n\n/** Whether an email to the operator can go at all on this install. */\nasync function operatorEmailReachable(): Promise<boolean> {\n if (!isEmailConfigured()) return false\n const { resolveStaffAlertRecipients } = await import('./staff-alert-email')\n return (await resolveStaffAlertRecipients()).length > 0\n}\n\n/** See the module comment. Never throws. */\nexport async function raiseOperatorAlert(\n type: string | OperatorAlertDefinition,\n options: RaiseOperatorAlertOptions = {},\n): Promise<OperatorAlertResult> {\n const typeId = typeof type === 'string' ? type : type.type\n try {\n const definition = resolveOperatorAlertDefinition(type)\n const dedupeKey = String(options.dedupeKey ?? '').trim()\n let repeats = 0\n if (dedupeKey) {\n const claim = await claimOperatorAlert(definition, dedupeKey)\n if (claim.fire === false) return { outcome: claim.reason, type: typeId }\n repeats = claim.repeats\n }\n const alert = renderOperatorAlert(definition, options)\n if (repeats > 0) {\n alert.body = [alert.body, `Raised ${repeats} more time${repeats === 1 ? '' : 's'} since it was last told.`]\n .filter(Boolean)\n .join(' ')\n }\n const settings = await readOperatorAlertSettings()\n const { enabled, delivery } = effectiveOperatorAlertSetting(definition, settings)\n const emailNow = enabled && delivery === 'immediate' && (await operatorEmailReachable())\n\n const { notifyStaffConsole } = await import('./notifications')\n await notifyStaffConsole(\n {\n type: alert.notificationType,\n title: alert.title,\n ...(alert.body ? { body: alert.body } : {}),\n level: options.level ?? operatorAlertLevel(definition),\n ...(alert.link && alert.link.startsWith('/') ? { link: alert.link } : {}),\n ...(alert.orgId ? { orgId: alert.orgId } : {}),\n ...(alert.hostId ? { hostId: alert.hostId } : {}),\n },\n // The operator is emailed; a staff member who also switched email on\n // for this category is not mailed the same alert a second time.\n emailNow ? { skipEmail: true } : {},\n )\n if (!enabled) return { outcome: 'console-only', type: typeId }\n // Cloud Logging, for the platform's Cloud Monitoring policies (AGL-3683).\n const { writeOperatorAlertLog } = await import('./operator-alerts-log')\n await writeOperatorAlertLog(alert)\n if (delivery === 'digest') {\n await queueOperatorAlertForDigest(alert)\n return { outcome: 'queued', type: typeId }\n }\n return await deliverOperatorAlert(alert, { emailNow })\n } catch (error) {\n console.error(`[operator-alerts] ${typeId} could not be raised`, error)\n return { outcome: 'undelivered', type: typeId }\n }\n}\n\n/** Emails and posts one alert now. */\nasync function deliverOperatorAlert(\n alert: RenderedOperatorAlert,\n options: { emailNow: boolean },\n): Promise<OperatorAlertResult> {\n const { sendOperatorAlertEmail } = await import('./staff-alert-email')\n const [email, webhook] = await Promise.all([\n options.emailNow\n ? sendOperatorAlertEmail({\n title: alert.title,\n body: alert.body,\n url: operatorAlertAbsoluteLink(alert.link),\n context: `operator-alert ${alert.type}`,\n })\n : Promise.resolve<SendEmailResult>({ sent: false, reason: 'unconfigured' }),\n postOperatorAlertWebhook([alert]),\n ])\n return {\n outcome: email.sent || webhook.posted ? 'delivered' : 'undelivered',\n type: alert.type,\n email,\n webhook,\n }\n}\n\n// ── The digest ──────────────────────────────────────────────────────────────\n\nasync function queueOperatorAlertForDigest(alert: RenderedOperatorAlert): Promise<void> {\n try {\n await firestore()\n .collection(OPERATOR_ALERT_DIGEST_COLLECTION)\n .doc(createResourceUid())\n .set({ ...alert, atMs: Date.now() })\n } catch (error) {\n // The queue is the only record the digest will have; with it gone, the\n // alert goes now rather than never.\n console.error(`[operator-alerts] digest queue unavailable; sending ${alert.type} now`, error)\n await deliverOperatorAlert(alert, { emailNow: await operatorEmailReachable() })\n }\n}\n\n/** At most this many alerts are listed in one digest; the rest are counted. */\nexport const OPERATOR_DIGEST_MAX_LISTED = 50\nconst DIGEST_READ_LIMIT = 500\n\nexport interface OperatorDigestResult {\n /** Whether a digest went out (or, on a dry run, would). */\n sent: boolean\n reason?: 'not-due' | 'already-sent' | 'empty' | 'dry-run' | 'unavailable'\n count: number\n email?: SendEmailResult\n webhook?: OperatorAlertWebhookResult\n}\n\nfunction dayKey(now: number): string {\n return new Date(now).toISOString().slice(0, 10)\n}\n\n/** The state document that claims one UTC day's digest. */\nexport function operatorDigestMarkerId(now: number): string {\n return `digest-${dayKey(now)}`\n}\n\n/** The digest's body: one paragraph per alert, newest last. */\nexport function composeOperatorDigestBody(\n alerts: readonly RenderedOperatorAlert[],\n total = alerts.length,\n): string {\n const listed = alerts.slice(0, OPERATOR_DIGEST_MAX_LISTED).map((alert) => {\n const link = operatorAlertAbsoluteLink(alert.link)\n return [`• ${alert.title}`, alert.body, link].filter(Boolean).join('\\n')\n })\n const rest = total - listed.length\n if (rest > 0) listed.push(`…and ${rest} more in the staff console.`)\n return listed.join('\\n\\n')\n}\n\n/**\n * Sends the day's digest once the configured hour has passed: every queued\n * alert in one `operator-alert-digest` email and one webhook post, then\n * clears what it sent. Once per UTC day, claimed by creating\n * `operatorAlertState/digest-{day}`, so two ticks never both send it.\n */\nexport async function sendOperatorAlertDigest(\n options: { now?: number; dryRun?: boolean; force?: boolean } = {},\n): Promise<OperatorDigestResult> {\n const now = options.now ?? Date.now()\n try {\n const settings = await readOperatorAlertSettings()\n const hour = settings.digestHourUtc ?? OPERATOR_ALERT_DIGEST_DEFAULT_HOUR_UTC\n if (!options.force && new Date(now).getUTCHours() < hour) {\n return { sent: false, reason: 'not-due', count: 0 }\n }\n const collection = firestore().collection(OPERATOR_ALERT_DIGEST_COLLECTION)\n const marker = firestore()\n .collection(OPERATOR_ALERT_STATE_COLLECTION)\n .doc(operatorDigestMarkerId(now))\n if (!options.force && (await marker.get()).exists) {\n return { sent: false, reason: 'already-sent', count: 0 }\n }\n const snapshot = await collection.orderBy('atMs', 'asc').limit(DIGEST_READ_LIMIT).get()\n const queued = snapshot.docs\n if (!queued.length) return { sent: false, reason: 'empty', count: 0 }\n if (options.dryRun) return { sent: false, reason: 'dry-run', count: queued.length }\n if (!options.force) {\n try {\n await marker.create({ atMs: now, count: queued.length })\n } catch {\n return { sent: false, reason: 'already-sent', count: 0 }\n }\n }\n const alerts = queued.map((doc: any) => doc.data() as RenderedOperatorAlert)\n const { sendOperatorAlertDigestEmail } = await import('./staff-alert-email')\n const [email, webhook] = await Promise.all([\n sendOperatorAlertDigestEmail({\n date: dayKey(now),\n count: alerts.length,\n body: composeOperatorDigestBody(alerts),\n url: operatorAlertAbsoluteLink('/admin/operator-alerts'),\n }),\n postOperatorAlertWebhook(alerts, { digest: true, now }),\n ])\n if (email.sent || webhook.posted) {\n const batch = firestore().batch()\n for (const doc of queued) batch.delete(doc.ref)\n await batch.commit()\n } else if (!options.force) {\n // Nothing reached anybody: release the day so the next tick retries.\n await marker.delete().catch(() => undefined)\n }\n return { sent: email.sent || webhook.posted, count: alerts.length, email, webhook }\n } catch (error) {\n console.error('[operator-alerts] digest failed', error)\n return { sent: false, reason: 'unavailable', count: 0 }\n }\n}\n\n\n// ── The staff page ──────────────────────────────────────────────────────────\n\n/** One registry entry as Staff → Operator alerts lists it. */\nexport interface OperatorAlertCatalogRow {\n type: string\n label: string\n description: string\n tier: OperatorAlertDefinition['tier']\n category: OperatorAlertDefinition['category']\n pluginId: string | null\n defaults: { enabled: boolean; delivery: OperatorAlertDefinition['delivery'] }\n effective: { enabled: boolean; delivery: OperatorAlertDefinition['delivery'] }\n /** Whether staff have answered for this type, rather than the default. */\n overridden: boolean\n}\n\n/** Every registered type with its default and its effective setting. */\nexport function operatorAlertCatalogRows(\n definitions: readonly OperatorAlertDefinition[],\n settings: OperatorAlertSettingsDoc,\n): OperatorAlertCatalogRow[] {\n return definitions.map((definition) => ({\n type: definition.type,\n label: definition.label,\n description: definition.description,\n tier: definition.tier,\n category: definition.category,\n pluginId: definition.pluginId ?? null,\n defaults: { enabled: definition.defaultEnabled, delivery: definition.delivery },\n effective: effectiveOperatorAlertSetting(definition, settings),\n overridden: Boolean(settings.types?.[definition.type]),\n }))\n}\n\n/**\n * Sends one `[Test]` alert of a type straight to the operator's inbox and\n * webhook, past dedupe and staff's switches, so an operator can prove the\n * channels reach them. Writes no console notification.\n */\nexport async function sendOperatorAlertTest(\n definition: OperatorAlertDefinition,\n): Promise<{ email: SendEmailResult; webhook: OperatorAlertWebhookResult }> {\n const sample: Record<string, string> = {}\n for (const [, name] of `${definition.title} ${definition.body} ${definition.link ?? ''}`.matchAll(\n /\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g,\n )) {\n sample[name as string] = `sample ${name}`\n }\n const alert = renderOperatorAlert(definition, { context: sample })\n alert.title = `[Test] ${alert.title}`\n alert.body = [\n alert.body,\n 'This is a test of the operator alert channels; nothing happened.',\n ].join(' ')\n const { sendOperatorAlertEmail } = await import('./staff-alert-email')\n const [email, webhook] = await Promise.all([\n isEmailConfigured()\n ? sendOperatorAlertEmail({\n title: alert.title,\n body: alert.body,\n url: operatorAlertAbsoluteLink(alert.link) || operatorAlertAbsoluteLink('/admin/operator-alerts'),\n context: `operator-alert-test ${definition.type}`,\n })\n : Promise.resolve<SendEmailResult>({ sent: false, reason: 'unconfigured' }),\n postOperatorAlertWebhook([alert]),\n ])\n return { email, webhook }\n}\n\n/** Where operator mail goes, named but never listed, for the staff page. */\nexport async function describeOperatorAlertRecipients(): Promise<{\n source: 'STAFF_ALERT_EMAIL' | 'NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL' | 'staff-accounts' | 'none'\n count: number\n}> {\n const { resolveStaffAlertRecipients } = await import('./staff-alert-email')\n const recipients = await resolveStaffAlertRecipients()\n if (!recipients.length) return { source: 'none', count: 0 }\n const configured = String(process.env['STAFF_ALERT_EMAIL'] ?? '').trim().toLowerCase()\n if (configured.includes('@') && recipients[0] === configured) {\n return { source: 'STAFF_ALERT_EMAIL', count: 1 }\n }\n const support = String(process.env['NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL'] ?? '').trim().toLowerCase()\n if (support.includes('@') && recipients[0] === support) {\n return { source: 'NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL', count: 1 }\n }\n return { source: 'staff-accounts', count: recipients.length }\n}\n"],"names":["createHash","createHmac","effectiveOperatorAlertSetting","OPERATOR_ALERT_DEFAULT_NOTIFICATION_TYPE","OPERATOR_ALERT_TIER_LABELS","operatorAlertLevel","renderOperatorAlertTemplate","getOperatorAlert","registerOperatorAlerts","createResourceUid","isEmailConfigured","firebaseAdmin","OPERATOR_ALERT_SETTINGS_COLLECTION","OPERATOR_ALERT_SETTINGS_DOC","OPERATOR_ALERT_STATE_COLLECTION","OPERATOR_ALERT_DIGEST_COLLECTION","OPERATOR_ALERT_DIGEST_DEFAULT_HOUR_UTC","firestore","app","SETTINGS_TTL_MS","settingsCache","invalidateOperatorAlertSettingsCache","undefined","normalizeOperatorAlertSettings","raw","source","types","rawTypes","type","value","Object","entries","entry","setting","enabled","delivery","keys","length","hour","Number","digestHourUtc","isInteger","updatedAtMs","updatedByEmail","readOperatorAlertSettings","now","Date","at","doc","snapshot","collection","get","exists","data","error","console","firedLocally","Map","FIRED_LOCALLY_MAX","stateDocId","dedupeKey","update","digest","slice","claimOperatorAlert","definition","windowMs","Math","max","dedupeWindowMinutes","minOccurrences","floor","fire","repeats","localKey","quietUntil","reason","ref","claim","runTransaction","transaction","state","lastFiredAtMs","base","set","suppressed","merge","windowStartMs","inWindow","occurrences","fired","size","clear","WEBHOOK_TIMEOUT_MS","operatorAlertWebhookUrl","process","String","env","trim","test","operatorAlertWebhookPayload","alerts","options","shaped","origin","consoleOrigin","lines","map","alert","link","operatorAlertAbsoluteLink","tier","title","body","filter","Boolean","join","text","occurredAt","toISOString","category","url","install","postOperatorAlertWebhook","posted","JSON","stringify","headers","secret","timestamp","response","fetch","method","redirect","signal","AbortSignal","timeout","ok","status","replace","startsWith","renderLink","template","context","missing","matchAll","some","name","unregisteredDefinition","label","description","defaultEnabled","resolveOperatorAlertDefinition","known","pluginId","renderOperatorAlert","subject","notificationType","orgId","hostId","operatorEmailReachable","resolveStaffAlertRecipients","raiseOperatorAlert","typeId","outcome","settings","emailNow","notifyStaffConsole","level","skipEmail","writeOperatorAlertLog","queueOperatorAlertForDigest","deliverOperatorAlert","sendOperatorAlertEmail","email","webhook","Promise","all","resolve","sent","atMs","OPERATOR_DIGEST_MAX_LISTED","DIGEST_READ_LIMIT","dayKey","operatorDigestMarkerId","composeOperatorDigestBody","total","listed","rest","push","sendOperatorAlertDigest","force","getUTCHours","count","marker","orderBy","limit","queued","docs","dryRun","create","sendOperatorAlertDigestEmail","date","batch","delete","commit","catch","operatorAlertCatalogRows","definitions","defaults","effective","overridden","sendOperatorAlertTest","sample","describeOperatorAlertRecipients","recipients","configured","toLowerCase","includes","support"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,UAAU,QAAQ,cAAa;AACpD,SACEC,6BAA6B,EAC7BC,wCAAwC,EACxCC,0BAA0B,EAC1BC,kBAAkB,EAClBC,2BAA2B,QAOtB,sBAAqB;AAC5B,SACEC,gBAAgB,EAChBC,sBAAsB,QACjB,8CAA6C;AACpD,SAASC,iBAAiB,QAAQ,6CAA4C;AAC9E,SAASC,iBAAiB,QAA8B,2BAA0B;AAClF,OAAOC,mBAAmB,sBAAkB;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GAED,0CAA0C,GAC1C,OAAO,MAAMC,qCAAqC,mBAAkB;AACpE,OAAO,MAAMC,8BAA8B,iBAAgB;AAC3D,6EAA6E,GAC7E,OAAO,MAAMC,kCAAkC,qBAAoB;AACnE,yCAAyC,GACzC,OAAO,MAAMC,mCAAmC,sBAAqB;AAErE,uEAAuE,GACvE,OAAO,MAAMC,yCAAyC,GAAE;AAmExD,MAAMC,YAAY,IAAMN,cAAcO,GAAG,GAAGD,SAAS;AAErD,+EAA+E;AAE/E,MAAME,kBAAkB;AACxB,IAAIC;AAEJ,4EAA4E,GAC5E,OAAO,SAASC;IACdD,gBAAgBE;AAClB;AAEA,wEAAwE,GACxE,OAAO,SAASC,+BACdC,GAAY;IAEZ,MAAMC,SAAUD,OAAO,OAAOA,QAAQ,WAAWA,MAAM,CAAC;IACxD,MAAME,QAAkD,CAAC;IACzD,MAAMC,WAAWF,MAAM,CAAC,QAAQ;IAChC,IAAIE,YAAY,OAAOA,aAAa,UAAU;QAC5C,KAAK,MAAM,CAACC,MAAMC,MAAM,IAAIC,OAAOC,OAAO,CAACJ,UAAsC;YAC/E,IAAI,CAACC,QAAQ,CAACC,SAAS,OAAOA,UAAU,UAAU;YAClD,MAAMG,QAAQH;YACd,MAAMI,UAAoC,CAAC;YAC3C,IAAI,OAAOD,KAAK,CAAC,UAAU,KAAK,WAAWC,QAAQC,OAAO,GAAGF,KAAK,CAAC,UAAU;YAC7E,IAAIA,KAAK,CAAC,WAAW,KAAK,eAAeA,KAAK,CAAC,WAAW,KAAK,UAAU;gBACvEC,QAAQE,QAAQ,GAAGH,KAAK,CAAC,WAAW;YACtC;YACA,IAAIF,OAAOM,IAAI,CAACH,SAASI,MAAM,EAAEX,KAAK,CAACE,KAAK,GAAGK;QACjD;IACF;IACA,MAAMK,OAAOC,OAAOd,MAAM,CAAC,gBAAgB;IAC3C,OAAO;QACLC;QACAc,eACED,OAAOE,SAAS,CAACH,SAASA,QAAQ,KAAKA,QAAQ,KAC3CA,OACAtB;OACF,OAAOS,MAAM,CAAC,cAAc,KAAK,WACjC;QAAEiB,aAAajB,MAAM,CAAC,cAAc;IAAW,IAC/C,CAAC,GACD,OAAOA,MAAM,CAAC,iBAAiB,KAAK,WACpC;QAAEkB,gBAAgBlB,MAAM,CAAC,iBAAiB;IAAW,IACrD,CAAC;AAET;AAEA;;;CAGC,GACD,OAAO,eAAemB;IACpB,MAAMC,MAAMC,KAAKD,GAAG;IACpB,IAAIzB,iBAAiByB,MAAMzB,cAAc2B,EAAE,GAAG5B,iBAAiB,OAAOC,cAAc4B,GAAG;IACvF,IAAI;QACF,MAAMC,WAAW,MAAMhC,YACpBiC,UAAU,CAACtC,oCACXoC,GAAG,CAACnC,6BACJsC,GAAG;QACN,MAAMH,MAAMzB,+BAA+B0B,SAASG,MAAM,GAAGH,SAASI,IAAI,KAAK;QAC/EjC,gBAAgB;YAAE2B,IAAIF;YAAKG;QAAI;QAC/B,OAAOA;IACT,EAAE,OAAOM,OAAO;QACdC,QAAQD,KAAK,CAAC,6DAA6DA;QAC3E,OAAO/B,+BAA+B;IACxC;AACF;AAEA,+EAA+E;AAE/E,8DAA8D,GAC9D,MAAMiC,eAAe,IAAIC;AACzB,MAAMC,oBAAoB;AAE1B,SAASC,WAAW/B,IAAY,EAAEgC,SAAiB;IACjD,OAAO5D,WAAW,UAAU6D,MAAM,CAAC,GAAGjC,KAAK,MAAM,EAAEgC,WAAW,EAAEE,MAAM,CAAC,OAAOC,KAAK,CAAC,GAAG;AACzF;AAMA;;;;;CAKC,GACD,OAAO,eAAeC,mBACpBC,UAA4F,EAC5FL,SAAiB,EACjBf,MAAMC,KAAKD,GAAG,EAAE;QAG8BoB;IAD9C,MAAMC,WAAWC,KAAKC,GAAG,CAAC,GAAGH,WAAWI,mBAAmB,IAAI;IAC/D,MAAMC,iBAAiBH,KAAKC,GAAG,CAAC,GAAGD,KAAKI,KAAK,EAACN,6BAAAA,WAAWK,cAAc,YAAzBL,6BAA6B;IAC3E,IAAI,CAACC,UAAU,OAAO;QAAEM,MAAM;QAAMC,SAAS;IAAE;IAC/C,MAAMC,WAAW,GAAGT,WAAWrC,IAAI,CAAC,MAAM,EAAEgC,WAAW;IACvD,MAAMe,aAAanB,aAAaL,GAAG,CAACuB;IACpC,IAAIC,cAAcA,aAAa9B,KAAK,OAAO;QAAE2B,MAAM;QAAOI,QAAQ;IAAU;IAC5E,IAAI;QACF,MAAMC,MAAM5D,YACTiC,UAAU,CAACpC,iCACXkC,GAAG,CAACW,WAAWM,WAAWrC,IAAI,EAAEgC;QACnC,MAAMkB,QAAQ,MAAM7D,YAAY8D,cAAc,CAAC,OAAOC;gBAGvBC,sBAMAA,sBAEUA,oBAiBrBA,cAImBA;YA/BrC,MAAMhC,WAAW,MAAM+B,YAAY7B,GAAG,CAAC0B;YACvC,MAAMI,QAAShC,SAASG,MAAM,GAAGH,SAASI,IAAI,KAAK,CAAC;YACpD,MAAM6B,gBAAgB3C,QAAO0C,uBAAAA,KAAK,CAAC,gBAAgB,YAAtBA,uBAA0B;YACvD,MAAME,OAAO;gBAAEvD,MAAMqC,WAAWrC,IAAI;gBAAEgC,WAAWA,UAAUG,KAAK,CAAC,GAAG;gBAAMrB,aAAaG;YAAI;YAC3F,IAAIqC,iBAAiBrC,MAAMqC,gBAAgBhB,UAAU;oBACAe;gBAAnDD,YAAYI,GAAG,CAACP,KAAK,aAAKM;oBAAME,YAAY9C,QAAO0C,qBAAAA,KAAK,CAAC,aAAa,YAAnBA,qBAAuB,KAAK;oBAAK;oBAAEK,OAAO;gBAAK;gBAClG,OAAO;oBAAEd,MAAM;oBAAOI,QAAQ;gBAAU;YAC1C;YACA,MAAMW,gBAAgBhD,QAAO0C,uBAAAA,KAAK,CAAC,gBAAgB,YAAtBA,uBAA0B;YACvD,MAAMO,WAAWD,iBAAiB1C,MAAM0C,gBAAgBrB;YACxD,MAAMuB,cAAc,AAACD,CAAAA,WAAWjD,QAAO0C,qBAAAA,KAAK,CAAC,cAAc,YAApBA,qBAAwB,KAAK,CAAA,IAAK;YACzE,IAAIQ,cAAcnB,gBAAgB;gBAChCU,YAAYI,GAAG,CACbP,KACA,aAAKM;oBAAMM;oBAAaF,eAAeC,WAAWD,gBAAgB1C;oBAClE;oBAAEyC,OAAO;gBAAK;gBAEhB,OAAO;oBAAEd,MAAM;oBAAOI,QAAQ;gBAAkB;YAClD;YACAI,YAAYI,GAAG,CACbP,KACA,aACKM;gBACHD,eAAerC;gBACf4C,aAAa;gBACbF,eAAe;gBACfF,YAAY;gBACZK,OAAOnD,QAAO0C,eAAAA,KAAK,CAAC,QAAQ,YAAdA,eAAkB,KAAK;gBAEvC;gBAAEK,OAAO;YAAK;YAEhB,OAAO;gBAAEd,MAAM;gBAAMC,SAASlC,QAAO0C,oBAAAA,KAAK,CAAC,aAAa,YAAnBA,oBAAuB;YAAG;QACjE;QACA,IAAIH,MAAMN,IAAI,EAAE;YACd,IAAIhB,aAAamC,IAAI,IAAIjC,mBAAmBF,aAAaoC,KAAK;YAC9DpC,aAAa4B,GAAG,CAACV,UAAU7B,MAAMqB;QACnC;QACA,OAAOY;IACT,EAAE,OAAOxB,OAAO;QACdC,QAAQD,KAAK,CAAC,CAAC,yCAAyC,EAAEW,WAAWrC,IAAI,CAAC,QAAQ,CAAC,EAAE0B;QACrF,OAAO;YAAEkB,MAAM;YAAMC,SAAS;QAAE;IAClC;AACF;AAEA,+EAA+E;AAE/E,MAAMoB,qBAAqB;AAE3B,4DAA4D,GAC5D,OAAO,SAASC;QACOC;IAArB,MAAMlE,QAAQmE,QAAOD,0CAAAA,QAAQE,GAAG,CAAC,6BAA6B,YAAzCF,0CAA6C,IAAIG,IAAI;IAC1E,OAAO,oBAAoBC,IAAI,CAACtE,SAASA,QAAQ;AACnD;AAEA;;;;CAIC,GACD,OAAO,SAASuE,4BACdC,MAAwC,EACxCC,UAA8C,CAAC,CAAC;QAgBpBA,cAWyBC;IAzBrD,MAAMC,SAASC;IACf,MAAMC,QAAQL,OAAOM,GAAG,CAAC,CAACC;QACxB,MAAMC,OAAOC,0BAA0BF,MAAMC,IAAI;QACjD,OAAO;YACL,CAAC,CAAC,EAAEzG,0BAA0B,CAACwG,MAAMG,IAAI,CAAC,CAAC,EAAE,EAAEH,MAAMI,KAAK,EAAE;YAC5DJ,MAAMK,IAAI;YACVJ;SACD,CACEK,MAAM,CAACC,SACPC,IAAI,CAAC;IACV;IACA,MAAMC,OAAOf,QAAQxC,MAAM,GACvB;QAAC,CAAC,iBAAiB,EAAEuC,OAAOhE,MAAM,CAAC,MAAM,EAAEgE,OAAOhE,MAAM,KAAK,IAAI,KAAK,KAAK;WAAKqE;KAAM,CAACU,IAAI,CAAC,UAC5FV,MAAMU,IAAI,CAAC;IACf,MAAME,aAAa,IAAIxE,MAAKwD,eAAAA,QAAQzD,GAAG,YAAXyD,eAAexD,KAAKD,GAAG,IAAI0E,WAAW;IAClE,MAAMhB,SAASF,OAAOM,GAAG,CAAC,CAACC,QAAW,CAAA;YACpChF,MAAMgF,MAAMhF,IAAI;YAChBmF,MAAMH,MAAMG,IAAI;YAChBS,UAAUZ,MAAMY,QAAQ;YACxBR,OAAOJ,MAAMI,KAAK;YAClBC,MAAML,MAAMK,IAAI;YAChBQ,KAAKX,0BAA0BF,MAAMC,IAAI,KAAK;QAChD,CAAA;IACA,OAAO;QACLQ;OACIf,QAAQxC,MAAM,GAAG;QAAEA,QAAQyC;IAAO,IAAI;QAAEK,KAAK,GAAEL,WAAAA,MAAM,CAAC,EAAE,YAATA,WAAa;IAAK;QACrEe;OACId,SAAS;QAAEkB,SAASlB;IAAO,IAAI,CAAC;AAExC;AAEA;;;;;CAKC,GACD,OAAO,eAAemB,yBACpBtB,MAAwC,EACxCC,UAA8C,CAAC,CAAC;QAM1BP;IAJtB,MAAM0B,MAAM3B;IACZ,IAAI,CAAC2B,OAAO,CAACpB,OAAOhE,MAAM,EAAE,OAAO;QAAEuF,QAAQ;QAAOhD,QAAQ;IAAe;IAC3E,MAAMqC,OAAOY,KAAKC,SAAS,CAAC1B,4BAA4BC,QAAQC;IAChE,MAAMyB,UAAkC;QAAE,gBAAgB;IAAmB;IAC7E,MAAMC,SAAShC,QAAOD,6CAAAA,QAAQE,GAAG,CAAC,gCAAgC,YAA5CF,6CAAgD,IAAIG,IAAI;IAC9E,IAAI8B,QAAQ;YAC2B1B;QAArC,MAAM2B,YAAYjC,OAAO7B,KAAKI,KAAK,CAAC,EAAC+B,eAAAA,QAAQzD,GAAG,YAAXyD,eAAexD,KAAKD,GAAG,MAAM;QAClEkF,OAAO,CAAC,6BAA6B,GAAGE;QACxCF,OAAO,CAAC,6BAA6B,GACnC,YAAY9H,WAAW,UAAU+H,QAAQnE,MAAM,CAAC,GAAGoE,UAAU,CAAC,EAAEhB,MAAM,EAAEnD,MAAM,CAAC;IACnF;IACA,IAAI;QACF,MAAMoE,WAAW,MAAMC,MAAMV,KAAK;YAChCW,QAAQ;YACRL;YACAd;YACAoB,UAAU;YACVC,QAAQC,YAAYC,OAAO,CAAC3C;QAC9B;QACA,IAAIqC,SAASO,EAAE,EAAE,OAAO;YAAEb,QAAQ;YAAMc,QAAQR,SAASQ,MAAM;QAAC;QAChEnF,QAAQD,KAAK,CAAC,CAAC,mCAAmC,EAAE4E,SAASQ,MAAM,EAAE;QACrE,OAAO;YAAEd,QAAQ;YAAOhD,QAAQ;YAAW8D,QAAQR,SAASQ,MAAM;QAAC;IACrE,EAAE,OAAOpF,OAAO;QACdC,QAAQD,KAAK,CAAC,yCAAyCA;QACvD,OAAO;YAAEsE,QAAQ;YAAOhD,QAAQ;QAAU;IAC5C;AACF;AAEA,+EAA+E;AAE/E,SAAS6B;QACOV;IAAd,OAAOC,QAAOD,uCAAAA,QAAQE,GAAG,CAAC,0BAA0B,YAAtCF,uCAA0C,IAAIG,IAAI,GAAGyC,OAAO,CAAC,QAAQ;AACrF;AAEA,kEAAkE,GAClE,OAAO,SAAS7B,0BAA0BD,IAA+B;IACvE,MAAMhF,QAAQmE,OAAOa,eAAAA,OAAQ,IAAIX,IAAI;IACrC,IAAI,CAACrE,OAAO,OAAO;IACnB,IAAI,gBAAgBsE,IAAI,CAACtE,QAAQ,OAAOA;IACxC,MAAM2E,SAASC;IACf,OAAOD,UAAU3E,MAAM+G,UAAU,CAAC,OAAO,GAAGpC,SAAS3E,OAAO,GAAG;AACjE;AAEA;;;CAGC,GACD,SAASgH,WAAWC,QAA4B,EAAEC,OAAyC;IACzF,IAAI,CAACD,UAAU,OAAO;IACtB,MAAME,UAAU;WAAIF,SAASG,QAAQ,CAAC;KAA2B,CAACC,IAAI,CAAC,CAAC,GAAGC,KAAK;QAC9E,MAAMtH,QAAQkH,2BAAAA,OAAS,CAACI,KAAe;QACvC,OAAOtH,UAAU,QAAQA,UAAUP,aAAaO,UAAU;IAC5D;IACA,OAAOmH,UAAU,KAAK1I,4BAA4BwI,UAAUC;AAC9D;AAEA,+EAA+E;AAE/E,iEAAiE,GACjE,SAASK,uBAAuBxH,IAAY;IAC1C,OAAO;QACLA;QACAyH,OAAOzH;QACP0H,aAAa;QACbvC,MAAM;QACNS,UAAU;QACVR,OAAO;QACPC,MAAM;QACN9E,UAAU;QACVkC,qBAAqB;QACrBkF,gBAAgB;IAClB;AACF;AAEA,sEAAsE,GACtE,OAAO,SAASC,+BACd5H,IAAsC;IAEtC,IAAI,OAAOA,SAAS,UAAU;QAC5B,MAAM6H,QAAQlJ,iBAAiBqB,KAAKA,IAAI;QACxC,IAAI6H,OAAO,OAAOA;QAClB,IAAI7H,KAAK8H,QAAQ,EAAE;YACjB,IAAI;gBACFlJ,uBAAuB;oBAACoB;iBAAK,EAAE;oBAAE8H,UAAU9H,KAAK8H,QAAQ;gBAAC;YAC3D,EAAE,OAAOpG,OAAO;gBACdC,QAAQD,KAAK,CAAC,CAAC,kBAAkB,EAAE1B,KAAKA,IAAI,CAAC,wBAAwB,CAAC,EAAE0B;YAC1E;QACF;QACA,OAAO1B;IACT;IACA,MAAM6H,QAAQlJ,iBAAiBqB;IAC/B,IAAI6H,OAAO,OAAOA;IAClBlG,QAAQD,KAAK,CAAC,CAAC,mBAAmB,EAAE1B,KAAK,mDAAmD,CAAC;IAC7F,OAAOwH,uBAAuBxH;AAChC;AAEA,OAAO,SAAS+H,oBACd1F,UAAmC,EACnCqC,UAAqC,CAAC,CAAC;QAKjCA,kBAGGA,kBAIAA,eAQMA,cACKrC;IAnBpB,MAAM8E,UAAgC,aAChCzC,QAAQsD,OAAO,GAAG;QAAE5C,OAAOV,QAAQsD,OAAO;IAAC,IAAI,CAAC,GAChDtD,QAAQW,IAAI,GAAG;QAAEA,MAAMX,QAAQW,IAAI;IAAC,IAAI,CAAC,IACzCX,mBAAAA,QAAQyC,OAAO,YAAfzC,mBAAmB,CAAC;IAE1B,MAAMU,QACJhB,QAAOM,mBAAAA,QAAQsD,OAAO,YAAftD,mBAAmB,IAAIJ,IAAI,MAClC5F,4BAA4B2D,WAAW+C,KAAK,EAAE+B,YAC9C9E,WAAWoF,KAAK;IAClB,MAAMpC,OACJjB,QAAOM,gBAAAA,QAAQW,IAAI,YAAZX,gBAAgB,IAAIJ,IAAI,MAAM5F,4BAA4B2D,WAAWgD,IAAI,EAAE8B;IACpF,OAAO;QACLnH,MAAMqC,WAAWrC,IAAI;QACrByH,OAAOpF,WAAWoF,KAAK;QACvBtC,MAAM9C,WAAW8C,IAAI;QACrBS,UAAUvD,WAAWuD,QAAQ;QAC7BR;QACAC;QACAJ,MAAMb,QAAOM,eAAAA,QAAQmB,GAAG,YAAXnB,eAAe,IAAIJ,IAAI,MAAM2C,WAAW5E,WAAW4C,IAAI,EAAEkC;QACtEc,gBAAgB,GAAE5F,+BAAAA,WAAW4F,gBAAgB,YAA3B5F,+BAA+B9D;OAC7CmG,QAAQwD,KAAK,GAAG;QAAEA,OAAOxD,QAAQwD,KAAK;IAAC,IAAI,CAAC,GAC5CxD,QAAQyD,MAAM,GAAG;QAAEA,QAAQzD,QAAQyD,MAAM;IAAC,IAAI,CAAC;AAEvD;AAEA,+EAA+E;AAE/E,oEAAoE,GACpE,eAAeC;IACb,IAAI,CAACtJ,qBAAqB,OAAO;IACjC,MAAM,EAAEuJ,2BAA2B,EAAE,GAAG,MAAM,MAAM,CAAC;IACrD,OAAO,AAAC,CAAA,MAAMA,6BAA4B,EAAG5H,MAAM,GAAG;AACxD;AAEA,0CAA0C,GAC1C,OAAO,eAAe6H,mBACpBtI,IAAsC,EACtC0E,UAAqC,CAAC,CAAC;IAEvC,MAAM6D,SAAS,OAAOvI,SAAS,WAAWA,OAAOA,KAAKA,IAAI;IAC1D,IAAI;YAEuB0E,oBAuBdA;QAxBX,MAAMrC,aAAauF,+BAA+B5H;QAClD,MAAMgC,YAAYoC,QAAOM,qBAAAA,QAAQ1C,SAAS,YAAjB0C,qBAAqB,IAAIJ,IAAI;QACtD,IAAIzB,UAAU;QACd,IAAIb,WAAW;YACb,MAAMkB,QAAQ,MAAMd,mBAAmBC,YAAYL;YACnD,IAAIkB,MAAMN,IAAI,KAAK,OAAO,OAAO;gBAAE4F,SAAStF,MAAMF,MAAM;gBAAEhD,MAAMuI;YAAO;YACvE1F,UAAUK,MAAML,OAAO;QACzB;QACA,MAAMmC,QAAQ+C,oBAAoB1F,YAAYqC;QAC9C,IAAI7B,UAAU,GAAG;YACfmC,MAAMK,IAAI,GAAG;gBAACL,MAAMK,IAAI;gBAAE,CAAC,OAAO,EAAExC,QAAQ,UAAU,EAAEA,YAAY,IAAI,KAAK,IAAI,wBAAwB,CAAC;aAAC,CACxGyC,MAAM,CAACC,SACPC,IAAI,CAAC;QACV;QACA,MAAMiD,WAAW,MAAMzH;QACvB,MAAM,EAAEV,OAAO,EAAEC,QAAQ,EAAE,GAAGjC,8BAA8B+D,YAAYoG;QACxE,MAAMC,WAAWpI,WAAWC,aAAa,eAAgB,MAAM6H;QAE/D,MAAM,EAAEO,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC;QAC5C,MAAMA,mBACJ;YACE3I,MAAMgF,MAAMiD,gBAAgB;YAC5B7C,OAAOJ,MAAMI,KAAK;WACdJ,MAAMK,IAAI,GAAG;YAAEA,MAAML,MAAMK,IAAI;QAAC,IAAI,CAAC;YACzCuD,KAAK,GAAElE,iBAAAA,QAAQkE,KAAK,YAAblE,iBAAiBjG,mBAAmB4D;WACvC2C,MAAMC,IAAI,IAAID,MAAMC,IAAI,CAAC+B,UAAU,CAAC,OAAO;YAAE/B,MAAMD,MAAMC,IAAI;QAAC,IAAI,CAAC,GACnED,MAAMkD,KAAK,GAAG;YAAEA,OAAOlD,MAAMkD,KAAK;QAAC,IAAI,CAAC,GACxClD,MAAMmD,MAAM,GAAG;YAAEA,QAAQnD,MAAMmD,MAAM;QAAC,IAAI,CAAC,IAEjD,qEAAqE;QACrE,gEAAgE;QAChEO,WAAW;YAAEG,WAAW;QAAK,IAAI,CAAC;QAEpC,IAAI,CAACvI,SAAS,OAAO;YAAEkI,SAAS;YAAgBxI,MAAMuI;QAAO;QAC7D,0EAA0E;QAC1E,MAAM,EAAEO,qBAAqB,EAAE,GAAG,MAAM,MAAM,CAAC;QAC/C,MAAMA,sBAAsB9D;QAC5B,IAAIzE,aAAa,UAAU;YACzB,MAAMwI,4BAA4B/D;YAClC,OAAO;gBAAEwD,SAAS;gBAAUxI,MAAMuI;YAAO;QAC3C;QACA,OAAO,MAAMS,qBAAqBhE,OAAO;YAAE0D;QAAS;IACtD,EAAE,OAAOhH,OAAO;QACdC,QAAQD,KAAK,CAAC,CAAC,kBAAkB,EAAE6G,OAAO,oBAAoB,CAAC,EAAE7G;QACjE,OAAO;YAAE8G,SAAS;YAAexI,MAAMuI;QAAO;IAChD;AACF;AAEA,oCAAoC,GACpC,eAAeS,qBACbhE,KAA4B,EAC5BN,OAA8B;IAE9B,MAAM,EAAEuE,sBAAsB,EAAE,GAAG,MAAM,MAAM,CAAC;IAChD,MAAM,CAACC,OAAOC,QAAQ,GAAG,MAAMC,QAAQC,GAAG,CAAC;QACzC3E,QAAQgE,QAAQ,GACZO,uBAAuB;YACrB7D,OAAOJ,MAAMI,KAAK;YAClBC,MAAML,MAAMK,IAAI;YAChBQ,KAAKX,0BAA0BF,MAAMC,IAAI;YACzCkC,SAAS,CAAC,eAAe,EAAEnC,MAAMhF,IAAI,EAAE;QACzC,KACAoJ,QAAQE,OAAO,CAAkB;YAAEC,MAAM;YAAOvG,QAAQ;QAAe;QAC3E+C,yBAAyB;YAACf;SAAM;KACjC;IACD,OAAO;QACLwD,SAASU,MAAMK,IAAI,IAAIJ,QAAQnD,MAAM,GAAG,cAAc;QACtDhG,MAAMgF,MAAMhF,IAAI;QAChBkJ;QACAC;IACF;AACF;AAEA,+EAA+E;AAE/E,eAAeJ,4BAA4B/D,KAA4B;IACrE,IAAI;QACF,MAAM3F,YACHiC,UAAU,CAACnC,kCACXiC,GAAG,CAACvC,qBACJ2E,GAAG,CAAC,aAAKwB;YAAOwE,MAAMtI,KAAKD,GAAG;;IACnC,EAAE,OAAOS,OAAO;QACd,uEAAuE;QACvE,oCAAoC;QACpCC,QAAQD,KAAK,CAAC,CAAC,oDAAoD,EAAEsD,MAAMhF,IAAI,CAAC,IAAI,CAAC,EAAE0B;QACvF,MAAMsH,qBAAqBhE,OAAO;YAAE0D,UAAU,MAAMN;QAAyB;IAC/E;AACF;AAEA,6EAA6E,GAC7E,OAAO,MAAMqB,6BAA6B,GAAE;AAC5C,MAAMC,oBAAoB;AAW1B,SAASC,OAAO1I,GAAW;IACzB,OAAO,IAAIC,KAAKD,KAAK0E,WAAW,GAAGxD,KAAK,CAAC,GAAG;AAC9C;AAEA,yDAAyD,GACzD,OAAO,SAASyH,uBAAuB3I,GAAW;IAChD,OAAO,CAAC,OAAO,EAAE0I,OAAO1I,MAAM;AAChC;AAEA,6DAA6D,GAC7D,OAAO,SAAS4I,0BACdpF,MAAwC,EACxCqF,QAAQrF,OAAOhE,MAAM;IAErB,MAAMsJ,SAAStF,OAAOtC,KAAK,CAAC,GAAGsH,4BAA4B1E,GAAG,CAAC,CAACC;QAC9D,MAAMC,OAAOC,0BAA0BF,MAAMC,IAAI;QACjD,OAAO;YAAC,CAAC,EAAE,EAAED,MAAMI,KAAK,EAAE;YAAEJ,MAAMK,IAAI;YAAEJ;SAAK,CAACK,MAAM,CAACC,SAASC,IAAI,CAAC;IACrE;IACA,MAAMwE,OAAOF,QAAQC,OAAOtJ,MAAM;IAClC,IAAIuJ,OAAO,GAAGD,OAAOE,IAAI,CAAC,CAAC,KAAK,EAAED,KAAK,2BAA2B,CAAC;IACnE,OAAOD,OAAOvE,IAAI,CAAC;AACrB;AAEA;;;;;CAKC,GACD,OAAO,eAAe0E,wBACpBxF,UAA+D,CAAC,CAAC;QAErDA;IAAZ,MAAMzD,OAAMyD,eAAAA,QAAQzD,GAAG,YAAXyD,eAAexD,KAAKD,GAAG;IACnC,IAAI;YAEWwH;QADb,MAAMA,WAAW,MAAMzH;QACvB,MAAMN,QAAO+H,0BAAAA,SAAS7H,aAAa,YAAtB6H,0BAA0BrJ;QACvC,IAAI,CAACsF,QAAQyF,KAAK,IAAI,IAAIjJ,KAAKD,KAAKmJ,WAAW,KAAK1J,MAAM;YACxD,OAAO;gBAAE6I,MAAM;gBAAOvG,QAAQ;gBAAWqH,OAAO;YAAE;QACpD;QACA,MAAM/I,aAAajC,YAAYiC,UAAU,CAACnC;QAC1C,MAAMmL,SAASjL,YACZiC,UAAU,CAACpC,iCACXkC,GAAG,CAACwI,uBAAuB3I;QAC9B,IAAI,CAACyD,QAAQyF,KAAK,IAAI,AAAC,CAAA,MAAMG,OAAO/I,GAAG,EAAC,EAAGC,MAAM,EAAE;YACjD,OAAO;gBAAE+H,MAAM;gBAAOvG,QAAQ;gBAAgBqH,OAAO;YAAE;QACzD;QACA,MAAMhJ,WAAW,MAAMC,WAAWiJ,OAAO,CAAC,QAAQ,OAAOC,KAAK,CAACd,mBAAmBnI,GAAG;QACrF,MAAMkJ,SAASpJ,SAASqJ,IAAI;QAC5B,IAAI,CAACD,OAAOhK,MAAM,EAAE,OAAO;YAAE8I,MAAM;YAAOvG,QAAQ;YAASqH,OAAO;QAAE;QACpE,IAAI3F,QAAQiG,MAAM,EAAE,OAAO;YAAEpB,MAAM;YAAOvG,QAAQ;YAAWqH,OAAOI,OAAOhK,MAAM;QAAC;QAClF,IAAI,CAACiE,QAAQyF,KAAK,EAAE;YAClB,IAAI;gBACF,MAAMG,OAAOM,MAAM,CAAC;oBAAEpB,MAAMvI;oBAAKoJ,OAAOI,OAAOhK,MAAM;gBAAC;YACxD,EAAE,eAAM;gBACN,OAAO;oBAAE8I,MAAM;oBAAOvG,QAAQ;oBAAgBqH,OAAO;gBAAE;YACzD;QACF;QACA,MAAM5F,SAASgG,OAAO1F,GAAG,CAAC,CAAC3D,MAAaA,IAAIK,IAAI;QAChD,MAAM,EAAEoJ,4BAA4B,EAAE,GAAG,MAAM,MAAM,CAAC;QACtD,MAAM,CAAC3B,OAAOC,QAAQ,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACzCwB,6BAA6B;gBAC3BC,MAAMnB,OAAO1I;gBACboJ,OAAO5F,OAAOhE,MAAM;gBACpB4E,MAAMwE,0BAA0BpF;gBAChCoB,KAAKX,0BAA0B;YACjC;YACAa,yBAAyBtB,QAAQ;gBAAEvC,QAAQ;gBAAMjB;YAAI;SACtD;QACD,IAAIiI,MAAMK,IAAI,IAAIJ,QAAQnD,MAAM,EAAE;YAChC,MAAM+E,QAAQ1L,YAAY0L,KAAK;YAC/B,KAAK,MAAM3J,OAAOqJ,OAAQM,MAAMC,MAAM,CAAC5J,IAAI6B,GAAG;YAC9C,MAAM8H,MAAME,MAAM;QACpB,OAAO,IAAI,CAACvG,QAAQyF,KAAK,EAAE;YACzB,qEAAqE;YACrE,MAAMG,OAAOU,MAAM,GAAGE,KAAK,CAAC,IAAMxL;QACpC;QACA,OAAO;YAAE6J,MAAML,MAAMK,IAAI,IAAIJ,QAAQnD,MAAM;YAAEqE,OAAO5F,OAAOhE,MAAM;YAAEyI;YAAOC;QAAQ;IACpF,EAAE,OAAOzH,OAAO;QACdC,QAAQD,KAAK,CAAC,mCAAmCA;QACjD,OAAO;YAAE6H,MAAM;YAAOvG,QAAQ;YAAeqH,OAAO;QAAE;IACxD;AACF;AAmBA,sEAAsE,GACtE,OAAO,SAASc,yBACdC,WAA+C,EAC/C3C,QAAkC;IAElC,OAAO2C,YAAYrG,GAAG,CAAC,CAAC1C;YAMZA;YAGUoG;eATkB;YACtCzI,MAAMqC,WAAWrC,IAAI;YACrByH,OAAOpF,WAAWoF,KAAK;YACvBC,aAAarF,WAAWqF,WAAW;YACnCvC,MAAM9C,WAAW8C,IAAI;YACrBS,UAAUvD,WAAWuD,QAAQ;YAC7BkC,QAAQ,GAAEzF,uBAAAA,WAAWyF,QAAQ,YAAnBzF,uBAAuB;YACjCgJ,UAAU;gBAAE/K,SAAS+B,WAAWsF,cAAc;gBAAEpH,UAAU8B,WAAW9B,QAAQ;YAAC;YAC9E+K,WAAWhN,8BAA8B+D,YAAYoG;YACrD8C,YAAYhG,SAAQkD,kBAAAA,SAAS3I,KAAK,qBAAd2I,eAAgB,CAACpG,WAAWrC,IAAI,CAAC;QACvD;;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAewL,sBACpBnJ,UAAmC;QAG8BA;IADjE,MAAMoJ,SAAiC,CAAC;IACxC,KAAK,MAAM,GAAGlE,KAAK,IAAI,GAAGlF,WAAW+C,KAAK,CAAC,CAAC,EAAE/C,WAAWgD,IAAI,CAAC,CAAC,GAAEhD,mBAAAA,WAAW4C,IAAI,YAAf5C,mBAAmB,IAAI,CAACgF,QAAQ,CAC/F,2BACC;QACDoE,MAAM,CAAClE,KAAe,GAAG,CAAC,OAAO,EAAEA,MAAM;IAC3C;IACA,MAAMvC,QAAQ+C,oBAAoB1F,YAAY;QAAE8E,SAASsE;IAAO;IAChEzG,MAAMI,KAAK,GAAG,CAAC,OAAO,EAAEJ,MAAMI,KAAK,EAAE;IACrCJ,MAAMK,IAAI,GAAG;QACXL,MAAMK,IAAI;QACV;KACD,CAACG,IAAI,CAAC;IACP,MAAM,EAAEyD,sBAAsB,EAAE,GAAG,MAAM,MAAM,CAAC;IAChD,MAAM,CAACC,OAAOC,QAAQ,GAAG,MAAMC,QAAQC,GAAG,CAAC;QACzCvK,sBACImK,uBAAuB;YACrB7D,OAAOJ,MAAMI,KAAK;YAClBC,MAAML,MAAMK,IAAI;YAChBQ,KAAKX,0BAA0BF,MAAMC,IAAI,KAAKC,0BAA0B;YACxEiC,SAAS,CAAC,oBAAoB,EAAE9E,WAAWrC,IAAI,EAAE;QACnD,KACAoJ,QAAQE,OAAO,CAAkB;YAAEC,MAAM;YAAOvG,QAAQ;QAAe;QAC3E+C,yBAAyB;YAACf;SAAM;KACjC;IACD,OAAO;QAAEkE;QAAOC;IAAQ;AAC1B;AAEA,0EAA0E,GAC1E,OAAO,eAAeuC;QAOMvH,gCAIHA;IAPvB,MAAM,EAAEkE,2BAA2B,EAAE,GAAG,MAAM,MAAM,CAAC;IACrD,MAAMsD,aAAa,MAAMtD;IACzB,IAAI,CAACsD,WAAWlL,MAAM,EAAE,OAAO;QAAEZ,QAAQ;QAAQwK,OAAO;IAAE;IAC1D,MAAMuB,aAAaxH,QAAOD,iCAAAA,QAAQE,GAAG,CAAC,oBAAoB,YAAhCF,iCAAoC,IAAIG,IAAI,GAAGuH,WAAW;IACpF,IAAID,WAAWE,QAAQ,CAAC,QAAQH,UAAU,CAAC,EAAE,KAAKC,YAAY;QAC5D,OAAO;YAAE/L,QAAQ;YAAqBwK,OAAO;QAAE;IACjD;IACA,MAAM0B,UAAU3H,QAAOD,kDAAAA,QAAQE,GAAG,CAAC,qCAAqC,YAAjDF,kDAAqD,IAAIG,IAAI,GAAGuH,WAAW;IAClG,IAAIE,QAAQD,QAAQ,CAAC,QAAQH,UAAU,CAAC,EAAE,KAAKI,SAAS;QACtD,OAAO;YAAElM,QAAQ;YAAsCwK,OAAO;QAAE;IAClE;IACA,OAAO;QAAExK,QAAQ;QAAkBwK,OAAOsB,WAAWlL,MAAM;IAAC;AAC9D"}
@@ -256,6 +256,114 @@ const firestoreApp = ()=>firebaseAdmin.app().firestore();
256
256
  exported: true,
257
257
  note: 'Each paid order’s hand-off to a fulfillment network (AGL-3634): the order number, the lines sent with SKU and quantity, the network’s reference, the parcels read back with carrier, tracking number and status, and why anything was not sent. The merchant’s own fulfillment record of a SHOPPER’s order, so disclosed to the ORG only.'
258
258
  },
259
+ {
260
+ collection: 'marketplaceConnections',
261
+ keyedBy: 'field',
262
+ subjects: [
263
+ 'org'
264
+ ],
265
+ exported: true,
266
+ note: 'EXISTENCE ONLY for the grant — each site’s connection to the merchant’s own Amazon, eBay, Etsy, TikTok Shop, Walmart or Faire seller account (AGL-3638): marketplace, status, account name, the marketplace sites the account sells on, the listing and order settings, the order cursor and the last sync’s counts. The sealed access and refresh tokens and the id of the key that sealed them carry `token` in their names and are redacted (see redactSecrets); the account’s marketplace ids are disclosed as stored. The document id is the site id and the marketplace. Its `log` and `listingState` subcollections (what each listing was last sent) are not read by this export.'
267
+ },
268
+ {
269
+ collection: 'marketplaceOrders',
270
+ keyedBy: 'field',
271
+ subjects: [
272
+ 'org'
273
+ ],
274
+ exported: true,
275
+ note: 'Each marketplace order the store imported (AGL-3638): the marketplace and its order id, the store order it became, which order line each marketplace line became, the marketplace’s fees, and each shipment confirmed back with carrier and tracking number. The merchant’s own record of a marketplace BUYER’s order, so disclosed to the ORG only; the buyer’s name and address are on the store’s order, not here.'
276
+ },
277
+ {
278
+ collection: 'inventorySyncConnections',
279
+ keyedBy: 'field',
280
+ subjects: [
281
+ 'org'
282
+ ],
283
+ exported: true,
284
+ note: 'EXISTENCE ONLY for the keys — each site’s connection to the merchant’s own Cin7 Core, inFlow or Brightpearl account (AGL-3642): system, status, account name, the stock, product and order settings (location, the customer orders are recorded under, the tax rule), the last count by SKU, the last syncs’ counts, cursors and totals. The sealed API keys or grant and the id of the key that sealed them carry `credential` in their names and are redacted (see redactSecrets); Brightpearl’s account code and datacenter are disclosed as stored. The document id is the site id. Its `log` subcollection is not read by this export.'
285
+ },
286
+ {
287
+ collection: 'inventorySyncOrders',
288
+ keyedBy: 'field',
289
+ subjects: [
290
+ 'org'
291
+ ],
292
+ exported: true,
293
+ note: 'Each paid order’s hand-off to the connected inventory system (AGL-3642): the order number and our reference, the lines sent with SKU, quantity and price, the buyer’s name, email address and shipping address as sent, the totals, the system’s id and number for it, attempts and why it was not sent. The merchant’s own record of a SHOPPER’s order, so disclosed to the ORG only.'
294
+ },
295
+ {
296
+ collection: 'inventorySyncProducts',
297
+ keyedBy: 'field',
298
+ subjects: [
299
+ 'org'
300
+ ],
301
+ exported: true,
302
+ note: 'Each product linked between the store and the connected inventory system (AGL-3642): the system’s product id, the SKU, the version last synced, the store’s product and variant ids, and why it was not synced. Catalog data, no person’s.'
303
+ },
304
+ {
305
+ collection: 'podConnections',
306
+ keyedBy: 'field',
307
+ subjects: [
308
+ 'org'
309
+ ],
310
+ exported: true,
311
+ note: 'EXISTENCE ONLY for the tokens — each site’s connection to the merchant’s own Printful store or Printify shop (AGL-3641): service, store id and name, currency, whether orders are sent automatically or held for review, whether prices sync, how shipments are heard about, and the last error. The sealed API token and webhook token and the id of the key that sealed them carry `token` in their names and are redacted (see redactSecrets).'
312
+ },
313
+ {
314
+ collection: 'podProductLinks',
315
+ keyedBy: 'field',
316
+ subjects: [
317
+ 'org'
318
+ ],
319
+ exported: true,
320
+ note: 'Each product imported from a print-on-demand service (AGL-3641): the service’s product and variant ids, the store product and variants it became, names, SKUs, what the service charges for each variant and the store’s price. Catalog data, no person’s.'
321
+ },
322
+ {
323
+ collection: 'podOrders',
324
+ keyedBy: 'field',
325
+ subjects: [
326
+ 'org'
327
+ ],
328
+ exported: true,
329
+ note: 'The part of each paid order a print-on-demand service makes and ships (AGL-3641): the store order and its number, the service’s order id, the lines sent with quantity and cost, its status, costs, attempts and errors, and each parcel’s carrier and tracking number. No buyer name, address or email is kept here; they are on the store’s order. The merchant’s own record of a SHOPPER’s order, so disclosed to the ORG only.'
330
+ },
331
+ {
332
+ collection: 'deliveryAppStores',
333
+ keyedBy: 'field',
334
+ subjects: [
335
+ 'org'
336
+ ],
337
+ exported: true,
338
+ note: 'Each site’s link to the merchant’s own DoorDash, Uber Eats or Grubhub store (AGL-3644): the service, the store id it shows the merchant, the settings (accept automatically, prep minutes), the items matched to products and those waiting, and the menu’s last send. No credential is stored: every call is signed with the deployment’s partner credentials.'
339
+ },
340
+ {
341
+ collection: 'deliveryAppOrders',
342
+ keyedBy: 'field',
343
+ subjects: [
344
+ 'org'
345
+ ],
346
+ exported: true,
347
+ note: 'Each order a delivery service sent a site’s register (AGL-3644): the service and its order id and short code, the items with their options and instructions, the totals and refunds, where it stood at the counter, and the store order it became. The buyer appears by first name and last initial only. The merchant’s own record of a delivery BUYER’s order, so disclosed to the ORG only.'
348
+ },
349
+ {
350
+ collection: 'zapierHooks',
351
+ keyedBy: 'field',
352
+ subjects: [
353
+ 'org'
354
+ ],
355
+ exported: true,
356
+ note: 'Each Zapier REST hook a site’s API key subscribed (AGL-3643): the site, the events, the API key’s public id and name, delivery counts and dates. EXISTENCE ONLY for the URL — `targetUrl` is the capability Zapier minted for the Zap, a bearer URL whose secret is in its path, so it is redacted by name (see redactSecrets).'
357
+ },
358
+ {
359
+ collection: 'zapierHookDeliveries',
360
+ keyedBy: 'field',
361
+ subjects: [
362
+ 'org'
363
+ ],
364
+ exported: true,
365
+ note: 'Markers that one event reached one Zapier hook, so a retry does not post it twice (AGL-3643): the hook, the status and the time. The document id is a SHA-256 of the event and hook, an identifier rather than a verifier of any credential; no customer data, and a TTL policy removes each row.'
366
+ },
259
367
  {
260
368
  collection: 'ssoDomains',
261
369
  keyedBy: 'field',
@@ -496,6 +604,10 @@ const firestoreApp = ()=>firebaseAdmin.app().firestore();
496
604
  // A Stripe payment link is a live bearer URL: whoever holds it can pay.
497
605
  // Named in the AGL-1443 inventory of what the old dump actually carried.
498
606
  'payment link',
607
+ // A Zapier REST hook's `targetUrl` (AGL-3643) is the same kind of bearer
608
+ // URL: whoever holds it can post into the Zap, and its secret is in the
609
+ // path, which the URL scrub does not read.
610
+ 'target url',
499
611
  'auth code',
500
612
  'recovery code',
501
613
  'backup code'
@@ -939,7 +1051,17 @@ function sourcesFor(subject) {
939
1051
  'marketingPlatformConnections',
940
1052
  'marketingPlatformEvents',
941
1053
  'fulfillmentNetworkConnections',
942
- 'fulfillmentNetworkOrders'
1054
+ 'fulfillmentNetworkOrders',
1055
+ 'marketplaceConnections',
1056
+ 'marketplaceOrders',
1057
+ 'inventorySyncConnections',
1058
+ 'inventorySyncOrders',
1059
+ 'inventorySyncProducts',
1060
+ 'podConnections',
1061
+ 'podProductLinks',
1062
+ 'podOrders',
1063
+ 'deliveryAppStores',
1064
+ 'deliveryAppOrders'
943
1065
  ]){
944
1066
  data[collection] = await readByField(db, collection, 'orgId', orgId);
945
1067
  }