@aglyn/tenant-data-admin 1.0.0-beta.219 → 1.0.0-beta.220

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.
Files changed (85) hide show
  1. package/package.json +8 -8
  2. package/src/lib/server/api-http.js +5 -1
  3. package/src/lib/server/api-http.js.map +1 -1
  4. package/src/lib/server/api-v1-kit.d.ts +7 -0
  5. package/src/lib/server/api-v1-kit.js.map +1 -1
  6. package/src/lib/server/api-v1-resources.d.ts +1 -2
  7. package/src/lib/server/api-v1-resources.js +0 -1
  8. package/src/lib/server/api-v1-resources.js.map +1 -1
  9. package/src/lib/server/campaign-conversion-attribution.d.ts +1 -1
  10. package/src/lib/server/campaign-conversion-attribution.js +3 -3
  11. package/src/lib/server/campaign-conversion-attribution.js.map +1 -1
  12. package/src/lib/server/card-payment-velocity.d.ts +9 -0
  13. package/src/lib/server/card-payment-velocity.js +28 -2
  14. package/src/lib/server/card-payment-velocity.js.map +1 -1
  15. package/src/lib/server/consent-group-carry.js +1 -1
  16. package/src/lib/server/consent-group-carry.js.map +1 -1
  17. package/src/lib/server/consent-group-change.js +2 -2
  18. package/src/lib/server/consent-group-change.js.map +1 -1
  19. package/src/lib/server/dynamic-list-materialize.js +3 -3
  20. package/src/lib/server/dynamic-list-materialize.js.map +1 -1
  21. package/src/lib/server/email-flow-gate.js +2 -2
  22. package/src/lib/server/email-flow-gate.js.map +1 -1
  23. package/src/lib/server/email-suppression.js +1 -1
  24. package/src/lib/server/email-suppression.js.map +1 -1
  25. package/src/lib/server/host-visitor-records.d.ts +32 -4
  26. package/src/lib/server/host-visitor-records.js +73 -14
  27. package/src/lib/server/host-visitor-records.js.map +1 -1
  28. package/src/lib/server/hosted-page-review.d.ts +27 -0
  29. package/src/lib/server/hosted-page-review.js +149 -12
  30. package/src/lib/server/hosted-page-review.js.map +1 -1
  31. package/src/lib/server/media-cdn-rate-limit.d.ts +6 -3
  32. package/src/lib/server/media-cdn-rate-limit.js +6 -3
  33. package/src/lib/server/media-cdn-rate-limit.js.map +1 -1
  34. package/src/lib/server/member-email-aliases.d.ts +9 -1
  35. package/src/lib/server/member-email-aliases.js +25 -1
  36. package/src/lib/server/member-email-aliases.js.map +1 -1
  37. package/src/lib/server/notifications.d.ts +14 -0
  38. package/src/lib/server/notifications.js +29 -0
  39. package/src/lib/server/notifications.js.map +1 -1
  40. package/src/lib/server/operator-alerts.d.ts +7 -1
  41. package/src/lib/server/operator-alerts.js +5 -3
  42. package/src/lib/server/operator-alerts.js.map +1 -1
  43. package/src/lib/server/org-containers.d.ts +66 -0
  44. package/src/lib/server/org-containers.js +95 -0
  45. package/src/lib/server/org-containers.js.map +1 -0
  46. package/src/lib/server/org-member-notice.js +3 -2
  47. package/src/lib/server/org-member-notice.js.map +1 -1
  48. package/src/lib/server/payment-fraud-signal.js +5 -1
  49. package/src/lib/server/payment-fraud-signal.js.map +1 -1
  50. package/src/lib/server/payment-provider-stripe-connect-payouts.js +35 -1
  51. package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -1
  52. package/src/lib/server/payment-risk-record.d.ts +1 -1
  53. package/src/lib/server/payment-risk-record.js +3 -1
  54. package/src/lib/server/payment-risk-record.js.map +1 -1
  55. package/src/lib/server/platform-marketing-consent.d.ts +1 -1
  56. package/src/lib/server/platform-marketing-consent.js +1 -1
  57. package/src/lib/server/platform-marketing-consent.js.map +1 -1
  58. package/src/lib/server/risk-notice.d.ts +32 -1
  59. package/src/lib/server/risk-notice.js +90 -28
  60. package/src/lib/server/risk-notice.js.map +1 -1
  61. package/src/lib/server/sending-domains.d.ts +17 -0
  62. package/src/lib/server/sending-domains.js +28 -4
  63. package/src/lib/server/sending-domains.js.map +1 -1
  64. package/src/lib/server/sso-enforcement.js +7 -2
  65. package/src/lib/server/sso-enforcement.js.map +1 -1
  66. package/src/lib/server/staff-alert-email.js +4 -2
  67. package/src/lib/server/staff-alert-email.js.map +1 -1
  68. package/src/lib/server/{email-topic-confirmation.js → topic-subscriptions.js} +15 -11
  69. package/src/lib/server/topic-subscriptions.js.map +1 -0
  70. package/src/lib/server/upsert-contact.js +2 -2
  71. package/src/lib/server/upsert-contact.js.map +1 -1
  72. package/src/tenancy.d.ts +1 -3
  73. package/src/tenancy.js +1 -7
  74. package/src/tenancy.js.map +1 -1
  75. package/src/lib/server/contact-email-engagement.d.ts +0 -32
  76. package/src/lib/server/contact-email-engagement.js +0 -145
  77. package/src/lib/server/contact-email-engagement.js.map +0 -1
  78. package/src/lib/server/crm-booking-activity.d.ts +0 -86
  79. package/src/lib/server/crm-booking-activity.js +0 -257
  80. package/src/lib/server/crm-booking-activity.js.map +0 -1
  81. package/src/lib/server/crm-inbound-email.d.ts +0 -168
  82. package/src/lib/server/crm-inbound-email.js +0 -282
  83. package/src/lib/server/crm-inbound-email.js.map +0 -1
  84. package/src/lib/server/email-topic-confirmation.js.map +0 -1
  85. /package/src/lib/server/{email-topic-confirmation.d.ts → topic-subscriptions.d.ts} +0 -0
@@ -282,6 +282,8 @@ async function listStaffUids() {
282
282
  subject: payload.title
283
283
  }, payload.body ? {
284
284
  body: payload.body
285
+ } : {}, payload.level ? {
286
+ level: payload.level
285
287
  } : {}, payload.link ? {
286
288
  url: payload.link
287
289
  } : {}, payload.orgId ? {
@@ -338,6 +340,28 @@ async function listStaffUids() {
338
340
  console.error('org admin notification failed', error);
339
341
  }
340
342
  }
343
+ /**
344
+ * The name a site's managers know it by: the name they gave it, then its
345
+ * address, then — for a doc with neither — its id.
346
+ */ export function hostDisplayName(host, hostId) {
347
+ for (const value of [
348
+ host == null ? void 0 : host['displayName'],
349
+ host == null ? void 0 : host['subdomain']
350
+ ]){
351
+ if (typeof value === 'string' && value.trim()) return value.trim();
352
+ }
353
+ return hostId;
354
+ }
355
+ /**
356
+ * Fills `{site}` in a host notification's title or body (AGL-3432).
357
+ *
358
+ * A manager of ten storefronts reads "New order — $42.00" and cannot tell
359
+ * which store it came from, and the email shows only the body to someone who
360
+ * skips the subject. `notifyHostManagers` already reads the host doc, so a
361
+ * caller writes "New order on {site}" and pays no read of its own.
362
+ */ export function withSiteName(text, site) {
363
+ return text.includes('{site}') ? text.split('{site}').join(site) : text;
364
+ }
341
365
  /**
342
366
  * Notifies everyone who manages a host (admin/editor in the host doc's
343
367
  * `memberRoles` projection) — the audience for form submissions and
@@ -364,7 +388,12 @@ async function listStaffUids() {
364
388
  const memberRoles = (_host_get = host.get('memberRoles')) != null ? _host_get : {};
365
389
  const managers = Object.entries(memberRoles).filter(([, role])=>role === 'admin' || role === 'editor').map(([uid])=>uid);
366
390
  const orgId = (_payload_orgId = payload.orgId) != null ? _payload_orgId : host.get('orgId');
391
+ const site = hostDisplayName(host.data(), hostId);
367
392
  await notifyUsers(managers, _extends({}, payload, {
393
+ title: withSiteName(payload.title, site)
394
+ }, payload.body !== undefined ? {
395
+ body: withSiteName(payload.body, site)
396
+ } : {}, {
368
397
  hostId
369
398
  }, orgId ? {
370
399
  orgId
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/notifications.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 {\n buildRoute,\n NOTIFICATION_SELF_SENT_EMAIL_TYPES,\n NOTIFICATION_SETTINGS_FIELD,\n normalizeNotificationLink,\n notificationChannelEnabled,\n type AglynNotification,\n Route,\n type NotificationSettings,\n} from '@aglyn/aglyn/server'\nimport { operatorAlertForNotificationType } from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { withoutMailWithheldAccounts } from './account-mail'\nimport { findUserByUidAcrossPools, listStaffUidsAcrossPools } from './auth-pools'\nimport { filterSuppressedEmails } from './email-suppression'\nimport { meterOrgEmail, meterPlatformEmail } from './email-metering'\nimport firebaseAdmin from './firebase-admin'\nimport { listOrgMembers } from './organizations'\nimport {\n loadSystemEmail,\n renderLoadedSystemEmail,\n systemEmailBrand,\n systemEmailContent,\n type SystemEmailBrand,\n} from './render-system-email'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\nexport type NotificationPayload = Omit<\n AglynNotification,\n '$id' | 'createdAt' | 'readAt'\n>\n\nexport interface NotifyUsersOptions {\n /**\n * Addresses the caller already holds, by uid (AGL-3224).\n *\n * Purely an optimization, and one worth taking where it is free: a caller\n * that read the roster — `notifyOrgAdmins` does — has every address in hand\n * already, and without this the email half would look each one up again in\n * the directory. Absent uids fall back to that lookup, so passing a partial\n * map is fine and passing none is correct.\n */\n emails?: Readonly<Record<string, string | null | undefined>>\n /**\n * Who the fan-out is for (AGL-3367). `staff` is the platform talking to its\n * own people, so the email wears the platform's brand and is the platform's\n * cost even when the notification is ABOUT an org, which a canceled\n * subscription or a new ticket always is. Anything else speaks in the org's\n * brand when the notification names one.\n */\n audience?: 'staff'\n /**\n * Write the console notification only, and mail nobody through the\n * per-person email channel (AGL-3375): the caller has emailed the same\n * alert to the operator's inbox, and a staff member who also switched\n * email on would otherwise get it twice.\n */\n skipEmail?: boolean\n}\n\n/** The console's absolute origin for an email link, or `''` when unset. */\nfunction consoleOrigin(): string {\n return (process.env.NEXT_PUBLIC_CONSOLE_URL ?? '').trim().replace(/\\/+$/, '')\n}\n\n/**\n * At most this many directory lookups per fan-out, and at most this many\n * notification emails.\n *\n * Both ceilings only ever bite on a fan-out where many recipients have\n * OPTED IN — the email channel defaults off, so the ordinary notification\n * costs exactly what it cost before this existed. They are here because a\n * fan-out takes up to 400 uids and `notifyUsers` runs inside the mutation\n * that emitted it: a notification must not be able to turn one write into\n * four hundred directory reads and four hundred sends.\n */\nconst NOTIFY_EMAIL_MAX_LOOKUPS = 25\nconst NOTIFY_EMAIL_MAX_SENDS = 50\n\n/**\n * The email beside the console notification (AGL-3224).\n *\n * One send per recipient, never a shared `to:` list — two people who manage\n * the same site have not agreed to be shown each other's addresses, and the\n * settings link in the footer belongs to one person.\n *\n * `List-Unsubscribe` points at the settings page rather than at a one-click\n * endpoint: this is not marketing mail and RFC 8058's POST form would be\n * claiming a capability that does not exist. What it does do is put the way\n * out in the headers as well as the body, so a client that surfaces it shows\n * the person the page where the switch they want actually is.\n *\n * Rendered through the `notification` system email (AGL-3367), which is what\n * gives it the header and footer every other system email wears, and makes\n * it designable on the staff System emails page. It used to hand `sendEmail`\n * bare text, which is the unbranded card the fallback HTML part draws. The\n * template is read once per fan-out and rendered per recipient.\n *\n * Never throws, like everything else on this path.\n */\nasync function emailNotification(\n uids: string[],\n payload: NotificationPayload,\n known: Readonly<Record<string, string | null | undefined>>,\n audience: NotifyUsersOptions['audience'],\n): Promise<void> {\n const origin = consoleOrigin()\n const settingsUrl = `${origin}${buildRoute(Route.MANAGE_NOTIFICATION_SETTINGS)}`\n const context = await notificationEmailContext(payload, audience)\n const url = notificationEmailUrl(payload, context, origin)\n const loaded = await loadSystemEmail('notification').catch(() => null)\n const fallbackText = [\n payload.title,\n payload.body ?? '',\n url,\n origin ? `Change what you are emailed about: ${settingsUrl}` : '',\n ]\n .filter(Boolean)\n .join('\\n\\n')\n let lookups = 0\n let sent = 0\n for (const uid of uids) {\n if (sent >= NOTIFY_EMAIL_MAX_SENDS) break\n let address = String(known[uid] ?? '').trim().toLowerCase()\n if (!address.includes('@')) {\n if (lookups >= NOTIFY_EMAIL_MAX_LOOKUPS) continue\n lookups += 1\n const pooled = await findUserByUidAcrossPools(uid).catch(() => null)\n address = String(pooled?.record?.email ?? '').trim().toLowerCase()\n }\n if (!address.includes('@')) continue\n const recipients = await filterSuppressedEmails([address])\n if (!recipients.length) continue\n const rendered = loaded\n ? renderLoadedSystemEmail(\n loaded,\n {\n ...context.brand.merge,\n 'notification.title': payload.title,\n 'notification.body': payload.body ?? '',\n 'notification.url': url,\n settingsUrl: origin ? settingsUrl : '',\n },\n context.brand.options,\n )\n : null\n const result = await sendEmail({\n to: recipients,\n ...systemEmailContent(rendered, {\n subject: payload.title,\n text: fallbackText,\n }),\n ...(context.brand.fromName ? { fromName: context.brand.fromName } : {}),\n context: 'notification',\n ...(origin\n ? { headers: { 'List-Unsubscribe': `<${settingsUrl}>` } }\n : {}),\n })\n if (!result.sent) continue\n sent += 1\n // Whose cost it is: a notification to a workspace's people is that\n // workspace's mail, and a staff alert or an account-level notice is the\n // platform's, whichever org it is about.\n await (payload.orgId && audience !== 'staff'\n ? meterOrgEmail(payload.orgId)\n : meterPlatformEmail()\n ).catch(() => undefined)\n }\n}\n\n/** What a notification email needs from the org and site it is about. */\ninterface NotificationEmailContext {\n brand: SystemEmailBrand\n orgSlug?: string\n hostSubdomain?: string\n}\n\n/**\n * The org's brand, and the slug and subdomain its stored link is rewritten\n * with (AGL-3367): at most two reads per fan-out, made only when someone is\n * being emailed.\n *\n * A staff fan-out reads nothing. Its brand is the platform's, and its links\n * are staff routes that need no rewriting.\n */\nasync function notificationEmailContext(\n payload: NotificationPayload,\n audience: NotifyUsersOptions['audience'],\n): Promise<NotificationEmailContext> {\n if (audience === 'staff' || !payload.orgId) {\n return { brand: systemEmailBrand(null) }\n }\n try {\n const db = firestore()\n const [org, host] = await Promise.all([\n db.collection('orgs').doc(payload.orgId).get(),\n payload.hostId\n ? db.collection('hosts').doc(payload.hostId).get()\n : Promise.resolve(null),\n ])\n const slug = org.get('slug')\n const subdomain = host?.get('subdomain')\n return {\n brand: systemEmailBrand(org.exists ? (org.data() ?? null) : null),\n ...(typeof slug === 'string' && slug ? { orgSlug: slug } : {}),\n ...(typeof subdomain === 'string' && subdomain\n ? { hostSubdomain: subdomain }\n : {}),\n }\n } catch (error) {\n console.error('notification email context failed', error)\n return { brand: systemEmailBrand(null) }\n }\n}\n\n/**\n * Where the email's button goes: the notification's own link, rewritten onto\n * today's routes the way the console rewrites it when it is clicked, else\n * the notifications page. A stored host link is `/{hostDocId}/…`, which only\n * that rewrite turns into a page that exists. Empty when the deployment has\n * no console origin to make a link absolute with.\n */\nfunction notificationEmailUrl(\n payload: NotificationPayload,\n context: NotificationEmailContext,\n origin: string,\n): string {\n const link = normalizeNotificationLink(payload.link, {\n orgSlug: context.orgSlug,\n hostId: payload.hostId,\n hostSubdomain: context.hostSubdomain,\n })\n if (link && /^https?:\\/\\//i.test(link)) return link\n if (!origin) return ''\n return `${origin}${link?.startsWith('/') ? link : buildRoute(Route.MANAGE_NOTIFICATIONS)}`\n}\n\n/**\n * Notification fan-out (AGL-259): batch-writes one doc per recipient at\n * `users/{uid}/notifications`, and — for recipients who asked for it\n * (AGL-3224) — sends one email beside it. Never throws: a notification miss,\n * or a send that failed, must not break the mutation that emitted it.\n */\nexport async function notifyUsers(\n uids: Iterable<string>,\n payload: NotificationPayload,\n options: NotifyUsersOptions = {},\n): Promise<void> {\n try {\n const db = firestore()\n const targets = [...new Set(uids)].filter(Boolean).slice(0, 400)\n if (!targets.length) return\n // Per-user preferences (AGL-267, AGL-3223): one getAll over the user\n // docs, which now answers for both channels and for all three scopes —\n // the settings live on this document precisely so that stays one read.\n const userDocs = await db.getAll(\n ...targets.map((uid) => db.collection('users').doc(uid)),\n )\n const scope = { orgId: payload.orgId, hostId: payload.hostId }\n const batch = db.batch()\n let count = 0\n const mailTo: string[] = []\n for (const userDoc of userDocs) {\n const settings = userDoc.get(NOTIFICATION_SETTINGS_FIELD) as\n | NotificationSettings\n | undefined\n const legacy = userDoc.get('notificationPrefs') as\n | Record<string, boolean>\n | undefined\n if (\n notificationChannelEnabled(\n settings,\n 'console',\n payload.type,\n scope,\n legacy,\n )\n ) {\n batch.set(\n db\n .collection('users')\n .doc(userDoc.id)\n .collection('notifications')\n .doc(),\n // `read: false` on create, so the feed can ask for unread and read\n // alike by equality (AGL-3321; see `AglynNotification.read`).\n { ...payload, read: false, createdAt: FieldValue.serverTimestamp() },\n )\n count += 1\n }\n if (\n notificationChannelEnabled(settings, 'email', payload.type, scope, legacy)\n ) {\n mailTo.push(userDoc.id)\n }\n }\n if (count > 0) await batch.commit()\n /*\n * THE EMAIL AFTER THE COMMIT, and not inside its `try`.\n *\n * The console notification is the durable record and the send is a\n * courtesy on top of it, so the order is the one where a failing network\n * call cannot lose the record. The two channels are independent by\n * design — a person may have muted the feed and asked for mail, or the\n * reverse — so neither is gated on the other having happened.\n *\n * Digests are excluded: they compose and send their own mail, under\n * their own switches, and the generic channel would send a second\n * message announcing that the first one had been sent.\n *\n * A locked or disabled account is not mailed (AGL-3418); its feed entry\n * above still records the event.\n */\n if (\n mailTo.length &&\n !options.skipEmail &&\n isEmailConfigured() &&\n !NOTIFICATION_SELF_SENT_EMAIL_TYPES.has(payload.type)\n ) {\n const reachable = await withoutMailWithheldAccounts(mailTo)\n if (!reachable.length) return\n await emailNotification(\n reachable,\n payload,\n options.emails ?? {},\n options.audience,\n )\n }\n } catch (error) {\n console.error('notification fan-out failed', error)\n }\n}\n\n// Staff are identified by a Firebase Auth custom claim (`staff`), which is not\n// a Firestore query — so the roster comes from paginating the auth users. A\n// burst of ticket activity shouldn't rescan every time, so the (small) result\n// is cached briefly. Fails soft to an empty list.\nlet staffUidCache: { uids: string[]; at: number } | null = null\nconst STAFF_CACHE_MS = 60_000\n\nasync function listStaffUids(): Promise<string[]> {\n const now = Date.now()\n if (staffUidCache && now - staffUidCache.at < STAFF_CACHE_MS) {\n return staffUidCache.uids\n }\n // Across ALL auth pools (AGL-1122). This scanned only the project pool, so\n // a staff member who signs in through enterprise SSO — whose account lives\n // in a GCIP tenant pool — was never in the fan-out. Nothing errored; the\n // notification simply never arrived, which is the worst shape a miss can\n // take on an alerting path.\n const uids = await listStaffUidsAcrossPools()\n staffUidCache = { uids, at: now }\n return uids\n}\n\n/**\n * Notifies every staff-claim holder (AGL-850) — the support-desk audience.\n * Staff are not org members, so `notifyOrgAdmins`/`notifyHostManagers` never\n * reach them; this enumerates the `staff` custom claim instead. The docs land\n * at `users/{uid}/notifications`, which the console notifications menu already\n * renders, so no separate staff inbox is needed. Never throws.\n *\n * A type the operator alert registry names (AGL-3377) — a fraud or risk\n * alert, a dispute nobody owns, a support ticket — is raised as that alert\n * instead: the same console notification, plus staff's switches, the\n * operator email or digest, and the webhook. See `raiseOperatorAlert`.\n */\nexport async function notifyStaff(payload: NotificationPayload): Promise<void> {\n try {\n const alert = operatorAlertForNotificationType(payload.type)\n if (alert) {\n const { raiseOperatorAlert } = await import('./operator-alerts')\n await raiseOperatorAlert(alert, {\n subject: payload.title,\n ...(payload.body ? { body: payload.body } : {}),\n ...(payload.link ? { url: payload.link } : {}),\n ...(payload.orgId ? { orgId: payload.orgId } : {}),\n ...(payload.hostId ? { hostId: payload.hostId } : {}),\n })\n return\n }\n await notifyStaffConsole(payload)\n } catch (error) {\n console.error('staff notification failed', error)\n }\n}\n\n/**\n * The staff console notification alone, with the per-person email channel\n * it carries: what `notifyStaff` did before the registry, and what\n * `raiseOperatorAlert` writes as its first step. Never throws.\n */\nexport async function notifyStaffConsole(\n payload: NotificationPayload,\n options: { skipEmail?: boolean } = {},\n): Promise<void> {\n try {\n await notifyUsers(await listStaffUids(), payload, {\n audience: 'staff',\n ...(options.skipEmail ? { skipEmail: true } : {}),\n })\n } catch (error) {\n console.error('staff notification failed', error)\n }\n}\n\n/**\n * Notifies the org's owner + admins (billing, membership, org events).\n *\n * `skipEmail` is for a caller that emails the same notice to the same people\n * itself — the usage alerts do (AGL-3431) — so an admin who switched the\n * email channel on is not sent it twice.\n */\nexport async function notifyOrgAdmins(\n orgId: string,\n payload: NotificationPayload,\n options: Pick<NotifyUsersOptions, 'skipEmail'> = {},\n): Promise<void> {\n try {\n const members = await listOrgMembers(orgId)\n const admins = members.filter(\n (member) => member.role === 'owner' || member.role === 'admin',\n )\n // The roster rows carry addresses, so the email channel (AGL-3224) needs\n // no directory lookup for anyone reached this way.\n const emails: Record<string, string | undefined> = {}\n for (const member of admins) {\n if (member.email) emails[member.$id] = member.email\n }\n await notifyUsers(\n admins.map((member) => member.$id),\n { ...payload, orgId },\n { emails, ...(options.skipEmail ? { skipEmail: true } : {}) },\n )\n } catch (error) {\n console.error('org admin notification failed', error)\n }\n}\n\n/**\n * Notifies everyone who manages a host (admin/editor in the host doc's\n * `memberRoles` projection) — the audience for form submissions and\n * bookings on that site.\n *\n * The OWNING ORG travels with the notification (AGL-1773). Host links are\n * stored in the legacy `/{hostDocId}/rest` shape and rewritten to\n * `/{orgSlug}/hosts/{subdomain}/rest` when they are FOLLOWED\n * (`normalizeNotificationLink`, AGL-644) — a rewrite that needs an org slug.\n * With no `orgId` on the doc the console had nothing to key on and fell back\n * to whichever org the reader happens to have OPEN, so a manager who belongs\n * to more than one workspace was routed to `/{other-org}/hosts/{subdomain}/…`\n * and got a designed 404: `HostGuard` only resolves subdomains belonging to\n * the current org. `notifyOrgAdmins` has always stamped it; the host fan-out\n * — every form submission, booking, order and stock alert — never did.\n *\n * The host doc already carries `orgId` (AGL-233), so this costs no extra\n * read. Spread conditionally: the field is optional on the host model and\n * Firestore rejects `undefined`.\n */\nexport async function notifyHostManagers(\n hostId: string,\n payload: NotificationPayload,\n): Promise<void> {\n try {\n const host = await firestore().collection('hosts').doc(hostId).get()\n const memberRoles =\n (host.get('memberRoles') as Record<string, string> | undefined) ?? {}\n const managers = Object.entries(memberRoles)\n .filter(([, role]) => role === 'admin' || role === 'editor')\n .map(([uid]) => uid)\n const orgId = payload.orgId ?? (host.get('orgId') as string | undefined)\n await notifyUsers(managers, {\n ...payload,\n hostId,\n ...(orgId ? { orgId } : {}),\n })\n } catch (error) {\n console.error('host manager notification failed', error)\n }\n}\n"],"names":["buildRoute","NOTIFICATION_SELF_SENT_EMAIL_TYPES","NOTIFICATION_SETTINGS_FIELD","normalizeNotificationLink","notificationChannelEnabled","Route","operatorAlertForNotificationType","isEmailConfigured","sendEmail","FieldValue","withoutMailWithheldAccounts","findUserByUidAcrossPools","listStaffUidsAcrossPools","filterSuppressedEmails","meterOrgEmail","meterPlatformEmail","firebaseAdmin","listOrgMembers","loadSystemEmail","renderLoadedSystemEmail","systemEmailBrand","systemEmailContent","firestore","app","consoleOrigin","process","env","NEXT_PUBLIC_CONSOLE_URL","trim","replace","NOTIFY_EMAIL_MAX_LOOKUPS","NOTIFY_EMAIL_MAX_SENDS","emailNotification","uids","payload","known","audience","origin","settingsUrl","MANAGE_NOTIFICATION_SETTINGS","context","notificationEmailContext","url","notificationEmailUrl","loaded","catch","fallbackText","title","body","filter","Boolean","join","lookups","sent","uid","address","String","toLowerCase","includes","pooled","record","email","recipients","length","rendered","brand","merge","options","result","to","subject","text","fromName","headers","orgId","undefined","org","db","host","Promise","all","collection","doc","get","hostId","resolve","slug","subdomain","exists","data","orgSlug","hostSubdomain","error","console","link","test","startsWith","MANAGE_NOTIFICATIONS","notifyUsers","targets","Set","slice","userDocs","getAll","map","scope","batch","count","mailTo","userDoc","settings","legacy","type","set","id","read","createdAt","serverTimestamp","push","commit","skipEmail","has","reachable","emails","staffUidCache","STAFF_CACHE_MS","listStaffUids","now","Date","at","notifyStaff","alert","raiseOperatorAlert","notifyStaffConsole","notifyOrgAdmins","members","admins","member","role","$id","notifyHostManagers","memberRoles","managers","Object","entries"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/notifications.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 {\n buildRoute,\n NOTIFICATION_SELF_SENT_EMAIL_TYPES,\n NOTIFICATION_SETTINGS_FIELD,\n normalizeNotificationLink,\n notificationChannelEnabled,\n type AglynNotification,\n Route,\n type NotificationSettings,\n} from '@aglyn/aglyn/server'\nimport { operatorAlertForNotificationType } from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { withoutMailWithheldAccounts } from './account-mail'\nimport { findUserByUidAcrossPools, listStaffUidsAcrossPools } from './auth-pools'\nimport { filterSuppressedEmails } from './email-suppression'\nimport { meterOrgEmail, meterPlatformEmail } from './email-metering'\nimport firebaseAdmin from './firebase-admin'\nimport { listOrgMembers } from './organizations'\nimport {\n loadSystemEmail,\n renderLoadedSystemEmail,\n systemEmailBrand,\n systemEmailContent,\n type SystemEmailBrand,\n} from './render-system-email'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\nexport type NotificationPayload = Omit<\n AglynNotification,\n '$id' | 'createdAt' | 'readAt'\n>\n\nexport interface NotifyUsersOptions {\n /**\n * Addresses the caller already holds, by uid (AGL-3224).\n *\n * Purely an optimization, and one worth taking where it is free: a caller\n * that read the roster — `notifyOrgAdmins` does — has every address in hand\n * already, and without this the email half would look each one up again in\n * the directory. Absent uids fall back to that lookup, so passing a partial\n * map is fine and passing none is correct.\n */\n emails?: Readonly<Record<string, string | null | undefined>>\n /**\n * Who the fan-out is for (AGL-3367). `staff` is the platform talking to its\n * own people, so the email wears the platform's brand and is the platform's\n * cost even when the notification is ABOUT an org, which a canceled\n * subscription or a new ticket always is. Anything else speaks in the org's\n * brand when the notification names one.\n */\n audience?: 'staff'\n /**\n * Write the console notification only, and mail nobody through the\n * per-person email channel (AGL-3375): the caller has emailed the same\n * alert to the operator's inbox, and a staff member who also switched\n * email on would otherwise get it twice.\n */\n skipEmail?: boolean\n}\n\n/** The console's absolute origin for an email link, or `''` when unset. */\nfunction consoleOrigin(): string {\n return (process.env.NEXT_PUBLIC_CONSOLE_URL ?? '').trim().replace(/\\/+$/, '')\n}\n\n/**\n * At most this many directory lookups per fan-out, and at most this many\n * notification emails.\n *\n * Both ceilings only ever bite on a fan-out where many recipients have\n * OPTED IN — the email channel defaults off, so the ordinary notification\n * costs exactly what it cost before this existed. They are here because a\n * fan-out takes up to 400 uids and `notifyUsers` runs inside the mutation\n * that emitted it: a notification must not be able to turn one write into\n * four hundred directory reads and four hundred sends.\n */\nconst NOTIFY_EMAIL_MAX_LOOKUPS = 25\nconst NOTIFY_EMAIL_MAX_SENDS = 50\n\n/**\n * The email beside the console notification (AGL-3224).\n *\n * One send per recipient, never a shared `to:` list — two people who manage\n * the same site have not agreed to be shown each other's addresses, and the\n * settings link in the footer belongs to one person.\n *\n * `List-Unsubscribe` points at the settings page rather than at a one-click\n * endpoint: this is not marketing mail and RFC 8058's POST form would be\n * claiming a capability that does not exist. What it does do is put the way\n * out in the headers as well as the body, so a client that surfaces it shows\n * the person the page where the switch they want actually is.\n *\n * Rendered through the `notification` system email (AGL-3367), which is what\n * gives it the header and footer every other system email wears, and makes\n * it designable on the staff System emails page. It used to hand `sendEmail`\n * bare text, which is the unbranded card the fallback HTML part draws. The\n * template is read once per fan-out and rendered per recipient.\n *\n * Never throws, like everything else on this path.\n */\nasync function emailNotification(\n uids: string[],\n payload: NotificationPayload,\n known: Readonly<Record<string, string | null | undefined>>,\n audience: NotifyUsersOptions['audience'],\n): Promise<void> {\n const origin = consoleOrigin()\n const settingsUrl = `${origin}${buildRoute(Route.MANAGE_NOTIFICATION_SETTINGS)}`\n const context = await notificationEmailContext(payload, audience)\n const url = notificationEmailUrl(payload, context, origin)\n const loaded = await loadSystemEmail('notification').catch(() => null)\n const fallbackText = [\n payload.title,\n payload.body ?? '',\n url,\n origin ? `Change what you are emailed about: ${settingsUrl}` : '',\n ]\n .filter(Boolean)\n .join('\\n\\n')\n let lookups = 0\n let sent = 0\n for (const uid of uids) {\n if (sent >= NOTIFY_EMAIL_MAX_SENDS) break\n let address = String(known[uid] ?? '').trim().toLowerCase()\n if (!address.includes('@')) {\n if (lookups >= NOTIFY_EMAIL_MAX_LOOKUPS) continue\n lookups += 1\n const pooled = await findUserByUidAcrossPools(uid).catch(() => null)\n address = String(pooled?.record?.email ?? '').trim().toLowerCase()\n }\n if (!address.includes('@')) continue\n const recipients = await filterSuppressedEmails([address])\n if (!recipients.length) continue\n const rendered = loaded\n ? renderLoadedSystemEmail(\n loaded,\n {\n ...context.brand.merge,\n 'notification.title': payload.title,\n 'notification.body': payload.body ?? '',\n 'notification.url': url,\n settingsUrl: origin ? settingsUrl : '',\n },\n context.brand.options,\n )\n : null\n const result = await sendEmail({\n to: recipients,\n ...systemEmailContent(rendered, {\n subject: payload.title,\n text: fallbackText,\n }),\n ...(context.brand.fromName ? { fromName: context.brand.fromName } : {}),\n context: 'notification',\n ...(origin\n ? { headers: { 'List-Unsubscribe': `<${settingsUrl}>` } }\n : {}),\n })\n if (!result.sent) continue\n sent += 1\n // Whose cost it is: a notification to a workspace's people is that\n // workspace's mail, and a staff alert or an account-level notice is the\n // platform's, whichever org it is about.\n await (payload.orgId && audience !== 'staff'\n ? meterOrgEmail(payload.orgId)\n : meterPlatformEmail()\n ).catch(() => undefined)\n }\n}\n\n/** What a notification email needs from the org and site it is about. */\ninterface NotificationEmailContext {\n brand: SystemEmailBrand\n orgSlug?: string\n hostSubdomain?: string\n}\n\n/**\n * The org's brand, and the slug and subdomain its stored link is rewritten\n * with (AGL-3367): at most two reads per fan-out, made only when someone is\n * being emailed.\n *\n * A staff fan-out reads nothing. Its brand is the platform's, and its links\n * are staff routes that need no rewriting.\n */\nasync function notificationEmailContext(\n payload: NotificationPayload,\n audience: NotifyUsersOptions['audience'],\n): Promise<NotificationEmailContext> {\n if (audience === 'staff' || !payload.orgId) {\n return { brand: systemEmailBrand(null) }\n }\n try {\n const db = firestore()\n const [org, host] = await Promise.all([\n db.collection('orgs').doc(payload.orgId).get(),\n payload.hostId\n ? db.collection('hosts').doc(payload.hostId).get()\n : Promise.resolve(null),\n ])\n const slug = org.get('slug')\n const subdomain = host?.get('subdomain')\n return {\n brand: systemEmailBrand(org.exists ? (org.data() ?? null) : null),\n ...(typeof slug === 'string' && slug ? { orgSlug: slug } : {}),\n ...(typeof subdomain === 'string' && subdomain\n ? { hostSubdomain: subdomain }\n : {}),\n }\n } catch (error) {\n console.error('notification email context failed', error)\n return { brand: systemEmailBrand(null) }\n }\n}\n\n/**\n * Where the email's button goes: the notification's own link, rewritten onto\n * today's routes the way the console rewrites it when it is clicked, else\n * the notifications page. A stored host link is `/{hostDocId}/…`, which only\n * that rewrite turns into a page that exists. Empty when the deployment has\n * no console origin to make a link absolute with.\n */\nfunction notificationEmailUrl(\n payload: NotificationPayload,\n context: NotificationEmailContext,\n origin: string,\n): string {\n const link = normalizeNotificationLink(payload.link, {\n orgSlug: context.orgSlug,\n hostId: payload.hostId,\n hostSubdomain: context.hostSubdomain,\n })\n if (link && /^https?:\\/\\//i.test(link)) return link\n if (!origin) return ''\n return `${origin}${link?.startsWith('/') ? link : buildRoute(Route.MANAGE_NOTIFICATIONS)}`\n}\n\n/**\n * Notification fan-out (AGL-259): batch-writes one doc per recipient at\n * `users/{uid}/notifications`, and — for recipients who asked for it\n * (AGL-3224) — sends one email beside it. Never throws: a notification miss,\n * or a send that failed, must not break the mutation that emitted it.\n */\nexport async function notifyUsers(\n uids: Iterable<string>,\n payload: NotificationPayload,\n options: NotifyUsersOptions = {},\n): Promise<void> {\n try {\n const db = firestore()\n const targets = [...new Set(uids)].filter(Boolean).slice(0, 400)\n if (!targets.length) return\n // Per-user preferences (AGL-267, AGL-3223): one getAll over the user\n // docs, which now answers for both channels and for all three scopes —\n // the settings live on this document precisely so that stays one read.\n const userDocs = await db.getAll(\n ...targets.map((uid) => db.collection('users').doc(uid)),\n )\n const scope = { orgId: payload.orgId, hostId: payload.hostId }\n const batch = db.batch()\n let count = 0\n const mailTo: string[] = []\n for (const userDoc of userDocs) {\n const settings = userDoc.get(NOTIFICATION_SETTINGS_FIELD) as\n | NotificationSettings\n | undefined\n const legacy = userDoc.get('notificationPrefs') as\n | Record<string, boolean>\n | undefined\n if (\n notificationChannelEnabled(\n settings,\n 'console',\n payload.type,\n scope,\n legacy,\n )\n ) {\n batch.set(\n db\n .collection('users')\n .doc(userDoc.id)\n .collection('notifications')\n .doc(),\n // `read: false` on create, so the feed can ask for unread and read\n // alike by equality (AGL-3321; see `AglynNotification.read`).\n { ...payload, read: false, createdAt: FieldValue.serverTimestamp() },\n )\n count += 1\n }\n if (\n notificationChannelEnabled(settings, 'email', payload.type, scope, legacy)\n ) {\n mailTo.push(userDoc.id)\n }\n }\n if (count > 0) await batch.commit()\n /*\n * THE EMAIL AFTER THE COMMIT, and not inside its `try`.\n *\n * The console notification is the durable record and the send is a\n * courtesy on top of it, so the order is the one where a failing network\n * call cannot lose the record. The two channels are independent by\n * design — a person may have muted the feed and asked for mail, or the\n * reverse — so neither is gated on the other having happened.\n *\n * Digests are excluded: they compose and send their own mail, under\n * their own switches, and the generic channel would send a second\n * message announcing that the first one had been sent.\n *\n * A locked or disabled account is not mailed (AGL-3418); its feed entry\n * above still records the event.\n */\n if (\n mailTo.length &&\n !options.skipEmail &&\n isEmailConfigured() &&\n !NOTIFICATION_SELF_SENT_EMAIL_TYPES.has(payload.type)\n ) {\n const reachable = await withoutMailWithheldAccounts(mailTo)\n if (!reachable.length) return\n await emailNotification(\n reachable,\n payload,\n options.emails ?? {},\n options.audience,\n )\n }\n } catch (error) {\n console.error('notification fan-out failed', error)\n }\n}\n\n// Staff are identified by a Firebase Auth custom claim (`staff`), which is not\n// a Firestore query — so the roster comes from paginating the auth users. A\n// burst of ticket activity shouldn't rescan every time, so the (small) result\n// is cached briefly. Fails soft to an empty list.\nlet staffUidCache: { uids: string[]; at: number } | null = null\nconst STAFF_CACHE_MS = 60_000\n\nasync function listStaffUids(): Promise<string[]> {\n const now = Date.now()\n if (staffUidCache && now - staffUidCache.at < STAFF_CACHE_MS) {\n return staffUidCache.uids\n }\n // Across ALL auth pools (AGL-1122). This scanned only the project pool, so\n // a staff member who signs in through enterprise SSO — whose account lives\n // in a GCIP tenant pool — was never in the fan-out. Nothing errored; the\n // notification simply never arrived, which is the worst shape a miss can\n // take on an alerting path.\n const uids = await listStaffUidsAcrossPools()\n staffUidCache = { uids, at: now }\n return uids\n}\n\n/**\n * Notifies every staff-claim holder (AGL-850) — the support-desk audience.\n * Staff are not org members, so `notifyOrgAdmins`/`notifyHostManagers` never\n * reach them; this enumerates the `staff` custom claim instead. The docs land\n * at `users/{uid}/notifications`, which the console notifications menu already\n * renders, so no separate staff inbox is needed. Never throws.\n *\n * A type the operator alert registry names (AGL-3377) — a fraud or risk\n * alert, a dispute nobody owns, a support ticket — is raised as that alert\n * instead: the same console notification, plus staff's switches, the\n * operator email or digest, and the webhook. See `raiseOperatorAlert`.\n */\nexport async function notifyStaff(payload: NotificationPayload): Promise<void> {\n try {\n const alert = operatorAlertForNotificationType(payload.type)\n if (alert) {\n const { raiseOperatorAlert } = await import('./operator-alerts')\n await raiseOperatorAlert(alert, {\n subject: payload.title,\n ...(payload.body ? { body: payload.body } : {}),\n ...(payload.level ? { level: payload.level } : {}),\n ...(payload.link ? { url: payload.link } : {}),\n ...(payload.orgId ? { orgId: payload.orgId } : {}),\n ...(payload.hostId ? { hostId: payload.hostId } : {}),\n })\n return\n }\n await notifyStaffConsole(payload)\n } catch (error) {\n console.error('staff notification failed', error)\n }\n}\n\n/**\n * The staff console notification alone, with the per-person email channel\n * it carries: what `notifyStaff` did before the registry, and what\n * `raiseOperatorAlert` writes as its first step. Never throws.\n */\nexport async function notifyStaffConsole(\n payload: NotificationPayload,\n options: { skipEmail?: boolean } = {},\n): Promise<void> {\n try {\n await notifyUsers(await listStaffUids(), payload, {\n audience: 'staff',\n ...(options.skipEmail ? { skipEmail: true } : {}),\n })\n } catch (error) {\n console.error('staff notification failed', error)\n }\n}\n\n/**\n * Notifies the org's owner + admins (billing, membership, org events).\n *\n * `skipEmail` is for a caller that emails the same notice to the same people\n * itself — the usage alerts do (AGL-3431) — so an admin who switched the\n * email channel on is not sent it twice.\n */\nexport async function notifyOrgAdmins(\n orgId: string,\n payload: NotificationPayload,\n options: Pick<NotifyUsersOptions, 'skipEmail'> = {},\n): Promise<void> {\n try {\n const members = await listOrgMembers(orgId)\n const admins = members.filter(\n (member) => member.role === 'owner' || member.role === 'admin',\n )\n // The roster rows carry addresses, so the email channel (AGL-3224) needs\n // no directory lookup for anyone reached this way.\n const emails: Record<string, string | undefined> = {}\n for (const member of admins) {\n if (member.email) emails[member.$id] = member.email\n }\n await notifyUsers(\n admins.map((member) => member.$id),\n { ...payload, orgId },\n { emails, ...(options.skipEmail ? { skipEmail: true } : {}) },\n )\n } catch (error) {\n console.error('org admin notification failed', error)\n }\n}\n\n/**\n * The name a site's managers know it by: the name they gave it, then its\n * address, then — for a doc with neither — its id.\n */\nexport function hostDisplayName(\n host: Record<string, unknown> | undefined,\n hostId: string,\n): string {\n for (const value of [host?.['displayName'], host?.['subdomain']]) {\n if (typeof value === 'string' && value.trim()) return value.trim()\n }\n return hostId\n}\n\n/**\n * Fills `{site}` in a host notification's title or body (AGL-3432).\n *\n * A manager of ten storefronts reads \"New order — $42.00\" and cannot tell\n * which store it came from, and the email shows only the body to someone who\n * skips the subject. `notifyHostManagers` already reads the host doc, so a\n * caller writes \"New order on {site}\" and pays no read of its own.\n */\nexport function withSiteName(text: string, site: string): string {\n return text.includes('{site}') ? text.split('{site}').join(site) : text\n}\n\n/**\n * Notifies everyone who manages a host (admin/editor in the host doc's\n * `memberRoles` projection) — the audience for form submissions and\n * bookings on that site.\n *\n * The OWNING ORG travels with the notification (AGL-1773). Host links are\n * stored in the legacy `/{hostDocId}/rest` shape and rewritten to\n * `/{orgSlug}/hosts/{subdomain}/rest` when they are FOLLOWED\n * (`normalizeNotificationLink`, AGL-644) — a rewrite that needs an org slug.\n * With no `orgId` on the doc the console had nothing to key on and fell back\n * to whichever org the reader happens to have OPEN, so a manager who belongs\n * to more than one workspace was routed to `/{other-org}/hosts/{subdomain}/…`\n * and got a designed 404: `HostGuard` only resolves subdomains belonging to\n * the current org. `notifyOrgAdmins` has always stamped it; the host fan-out\n * — every form submission, booking, order and stock alert — never did.\n *\n * The host doc already carries `orgId` (AGL-233), so this costs no extra\n * read. Spread conditionally: the field is optional on the host model and\n * Firestore rejects `undefined`.\n */\nexport async function notifyHostManagers(\n hostId: string,\n payload: NotificationPayload,\n): Promise<void> {\n try {\n const host = await firestore().collection('hosts').doc(hostId).get()\n const memberRoles =\n (host.get('memberRoles') as Record<string, string> | undefined) ?? {}\n const managers = Object.entries(memberRoles)\n .filter(([, role]) => role === 'admin' || role === 'editor')\n .map(([uid]) => uid)\n const orgId = payload.orgId ?? (host.get('orgId') as string | undefined)\n const site = hostDisplayName(host.data(), hostId)\n await notifyUsers(managers, {\n ...payload,\n title: withSiteName(payload.title, site),\n ...(payload.body !== undefined\n ? { body: withSiteName(payload.body, site) }\n : {}),\n hostId,\n ...(orgId ? { orgId } : {}),\n })\n } catch (error) {\n console.error('host manager notification failed', error)\n }\n}\n"],"names":["buildRoute","NOTIFICATION_SELF_SENT_EMAIL_TYPES","NOTIFICATION_SETTINGS_FIELD","normalizeNotificationLink","notificationChannelEnabled","Route","operatorAlertForNotificationType","isEmailConfigured","sendEmail","FieldValue","withoutMailWithheldAccounts","findUserByUidAcrossPools","listStaffUidsAcrossPools","filterSuppressedEmails","meterOrgEmail","meterPlatformEmail","firebaseAdmin","listOrgMembers","loadSystemEmail","renderLoadedSystemEmail","systemEmailBrand","systemEmailContent","firestore","app","consoleOrigin","process","env","NEXT_PUBLIC_CONSOLE_URL","trim","replace","NOTIFY_EMAIL_MAX_LOOKUPS","NOTIFY_EMAIL_MAX_SENDS","emailNotification","uids","payload","known","audience","origin","settingsUrl","MANAGE_NOTIFICATION_SETTINGS","context","notificationEmailContext","url","notificationEmailUrl","loaded","catch","fallbackText","title","body","filter","Boolean","join","lookups","sent","uid","address","String","toLowerCase","includes","pooled","record","email","recipients","length","rendered","brand","merge","options","result","to","subject","text","fromName","headers","orgId","undefined","org","db","host","Promise","all","collection","doc","get","hostId","resolve","slug","subdomain","exists","data","orgSlug","hostSubdomain","error","console","link","test","startsWith","MANAGE_NOTIFICATIONS","notifyUsers","targets","Set","slice","userDocs","getAll","map","scope","batch","count","mailTo","userDoc","settings","legacy","type","set","id","read","createdAt","serverTimestamp","push","commit","skipEmail","has","reachable","emails","staffUidCache","STAFF_CACHE_MS","listStaffUids","now","Date","at","notifyStaff","alert","raiseOperatorAlert","level","notifyStaffConsole","notifyOrgAdmins","members","admins","member","role","$id","hostDisplayName","value","withSiteName","site","split","notifyHostManagers","memberRoles","managers","Object","entries"],"mappings":"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"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type AglynNotificationType, type OperatorAlertContext, type OperatorAlertDefinition, type OperatorAlertSettings } from '@aglyn/aglyn/server';
17
+ import { type AglynNotificationType, type NotificationLevel, type OperatorAlertContext, type OperatorAlertDefinition, type OperatorAlertSettings } from '@aglyn/aglyn/server';
18
18
  import { type SendEmailResult } from '@aglyn/shared-util-email';
19
19
  /**
20
20
  * RAISING AN OPERATOR ALERT (AGL-3377): the one server entry point every
@@ -70,6 +70,12 @@ export interface RaiseOperatorAlertOptions {
70
70
  subject?: string;
71
71
  /** The body as written by the caller; overrides the rendered one. */
72
72
  body?: string;
73
+ /**
74
+ * The console notification's level as the caller judged it (AGL-3437);
75
+ * overrides the definition's. For a caller whose one alert type covers
76
+ * news of more than one tone.
77
+ */
78
+ level?: NotificationLevel;
73
79
  orgId?: string;
74
80
  hostId?: string;
75
81
  }
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { createHash, createHmac } from "node:crypto";
18
- import { effectiveOperatorAlertSetting, OPERATOR_ALERT_DEFAULT_NOTIFICATION_TYPE, OPERATOR_ALERT_TIER_LABELS, renderOperatorAlertTemplate } from "@aglyn/aglyn/server";
18
+ import { effectiveOperatorAlertSetting, OPERATOR_ALERT_DEFAULT_NOTIFICATION_TYPE, OPERATOR_ALERT_TIER_LABELS, operatorAlertLevel, renderOperatorAlertTemplate } from "@aglyn/aglyn/server";
19
19
  import { getOperatorAlert, registerOperatorAlerts } from "@aglyn/aglyn/plugin-manager/operator-alerts";
20
20
  import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
21
21
  import { isEmailConfigured } from "@aglyn/shared-util-email";
@@ -383,7 +383,7 @@ export function renderOperatorAlert(definition, options = {}) {
383
383
  /** See the module comment. Never throws. */ export async function raiseOperatorAlert(type, options = {}) {
384
384
  const typeId = typeof type === 'string' ? type : type.type;
385
385
  try {
386
- var _options_dedupeKey;
386
+ var _options_dedupeKey, _options_level;
387
387
  const definition = resolveOperatorAlertDefinition(type);
388
388
  const dedupeKey = String((_options_dedupeKey = options.dedupeKey) != null ? _options_dedupeKey : '').trim();
389
389
  let repeats = 0;
@@ -411,7 +411,9 @@ export function renderOperatorAlert(definition, options = {}) {
411
411
  title: alert.title
412
412
  }, alert.body ? {
413
413
  body: alert.body
414
- } : {}, alert.link && alert.link.startsWith('/') ? {
414
+ } : {}, {
415
+ level: (_options_level = options.level) != null ? _options_level : operatorAlertLevel(definition)
416
+ }, alert.link && alert.link.startsWith('/') ? {
415
417
  link: alert.link
416
418
  } : {}, alert.orgId ? {
417
419
  orgId: alert.orgId
@@ -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 renderOperatorAlertTemplate,\n type AglynNotificationType,\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 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 ...(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","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","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 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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"}
@@ -0,0 +1,66 @@
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
+ */
17
+ /**
18
+ * AN ORGANIZATION'S CONTAINERS OF ONE KIND, read on the server by a plugin
19
+ * that files its records under them and does not keep them.
20
+ *
21
+ * A container kind is declared by the plugin that keeps it
22
+ * (`plugin-manager/plugin-containers.ts`): which org collection holds the
23
+ * containers and which field names one. That declaration is the whole of what
24
+ * another plugin may rely on — a container's id, its name, the sites its
25
+ * `visibleTo` places it on, and whether its owner retired it (`deletedAt`) —
26
+ * and this module reads exactly that, where the declaration says. A sequence
27
+ * that validates the campaigns it is filed under, an import that matches
28
+ * campaign names, a timeline entry that names the campaign a lead was filed
29
+ * under: none of them names the owner's collection, so the owner may move it.
30
+ *
31
+ * A kind no plugin keeps reads nothing: every id answers as gone, and a list
32
+ * is empty. That is the honest answer for a workspace without the owner.
33
+ */
34
+ /** One container, as much of it as a plugin that files under it may read. */
35
+ export interface OrgContainerRecord {
36
+ id: string;
37
+ /** The org holds a container under this id, retired or not. */
38
+ exists: boolean;
39
+ /** It exists and its owner has not retired it. */
40
+ live: boolean;
41
+ /** Its name, from the field its kind declares; `''` when it has none or is gone. */
42
+ name: string;
43
+ /** Its `visibleTo` scope tokens; `[]` when it has none or is gone. */
44
+ visibleTo: string[];
45
+ }
46
+ /**
47
+ * The containers named by `ids`, one record per id in the order asked: one
48
+ * keyed read each, in a single `getAll`. An id that cannot be a document id
49
+ * is answered as gone without a read.
50
+ *
51
+ * Throws what the read throws; a caller that must not fail on it catches.
52
+ */
53
+ export declare function readOrgContainers(firestore: FirebaseFirestore.Firestore, kind: string, orgId: string, ids: readonly string[]): Promise<OrgContainerRecord[]>;
54
+ /**
55
+ * Up to `ceiling` of the organization's containers of a kind, in document
56
+ * order, retired ones included and marked: a caller that matches by name
57
+ * decides what a retired one means to it. Read as a projection of the
58
+ * fields a record carries.
59
+ */
60
+ export declare function listOrgContainers(firestore: FirebaseFirestore.Firestore, kind: string, orgId: string, ceiling: number): Promise<OrgContainerRecord[]>;
61
+ /**
62
+ * Up to `limit` of the organization's containers of a kind whose declared
63
+ * name field is exactly `name`, retired ones included and marked, so a caller
64
+ * resolving a name can tell a live match from a retired one sharing it.
65
+ */
66
+ export declare function findOrgContainersByName(firestore: FirebaseFirestore.Firestore, kind: string, orgId: string, name: string, limit: number): Promise<OrgContainerRecord[]>;