@aglyn/tenant-data-admin 1.0.0-beta.203 → 1.0.0-beta.205

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 (54) hide show
  1. package/package.json +7 -7
  2. package/src/lib/server/capture-email-check.d.ts +109 -0
  3. package/src/lib/server/capture-email-check.js +362 -0
  4. package/src/lib/server/capture-email-check.js.map +1 -0
  5. package/src/lib/server/card-payment-velocity.d.ts +11 -16
  6. package/src/lib/server/card-payment-velocity.js +24 -28
  7. package/src/lib/server/card-payment-velocity.js.map +1 -1
  8. package/src/lib/server/email-deliverability.d.ts +19 -3
  9. package/src/lib/server/email-deliverability.js +34 -16
  10. package/src/lib/server/email-deliverability.js.map +1 -1
  11. package/src/lib/server/held-page-subject.d.ts +93 -0
  12. package/src/lib/server/held-page-subject.js +206 -0
  13. package/src/lib/server/held-page-subject.js.map +1 -0
  14. package/src/lib/server/host-email-tokens.d.ts +42 -1
  15. package/src/lib/server/host-email-tokens.js +100 -12
  16. package/src/lib/server/host-email-tokens.js.map +1 -1
  17. package/src/lib/server/host-visitor-records.js +21 -5
  18. package/src/lib/server/host-visitor-records.js.map +1 -1
  19. package/src/lib/server/hosted-page-review.d.ts +16 -5
  20. package/src/lib/server/hosted-page-review.js +242 -61
  21. package/src/lib/server/hosted-page-review.js.map +1 -1
  22. package/src/lib/server/list-members.js +16 -0
  23. package/src/lib/server/list-members.js.map +1 -1
  24. package/src/lib/server/notifications.d.ts +7 -0
  25. package/src/lib/server/notifications.js +27 -4
  26. package/src/lib/server/notifications.js.map +1 -1
  27. package/src/lib/server/outbound-send-review.d.ts +22 -0
  28. package/src/lib/server/outbound-send-review.js +74 -6
  29. package/src/lib/server/outbound-send-review.js.map +1 -1
  30. package/src/lib/server/payment-fraud-signal.d.ts +20 -12
  31. package/src/lib/server/payment-fraud-signal.js +41 -11
  32. package/src/lib/server/payment-fraud-signal.js.map +1 -1
  33. package/src/lib/server/payment-risk-record.d.ts +37 -13
  34. package/src/lib/server/payment-risk-record.js +23 -8
  35. package/src/lib/server/payment-risk-record.js.map +1 -1
  36. package/src/lib/server/render-system-email.d.ts +3 -0
  37. package/src/lib/server/render-system-email.js +39 -11
  38. package/src/lib/server/render-system-email.js.map +1 -1
  39. package/src/lib/server/risk-notice.d.ts +327 -0
  40. package/src/lib/server/risk-notice.js +1240 -0
  41. package/src/lib/server/risk-notice.js.map +1 -0
  42. package/src/lib/server/sender-readiness.d.ts +36 -0
  43. package/src/lib/server/sender-readiness.js +99 -0
  44. package/src/lib/server/sender-readiness.js.map +1 -0
  45. package/src/lib/server/staff-alert-email.d.ts +29 -15
  46. package/src/lib/server/staff-alert-email.js +131 -28
  47. package/src/lib/server/staff-alert-email.js.map +1 -1
  48. package/src/lib/server/test-firestore.js +1 -0
  49. package/src/lib/server/test-firestore.js.map +1 -1
  50. package/src/lib/server/upsert-contact.js +19 -3
  51. package/src/lib/server/upsert-contact.js.map +1 -1
  52. package/src/tenancy.d.ts +2 -0
  53. package/src/tenancy.js +3 -0
  54. package/src/tenancy.js.map +1 -1
@@ -20,7 +20,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
20
20
  * The server half of `@aglyn/aglyn/app-utils/payment-risk`. A plugin that
21
21
  * finds its own record behind a charge — an order, a booking — hands the
22
22
  * document here, and this writes the `paymentRisk` field once per signal and
23
- * tells the site's managers once. Any plugin that sells through Stripe uses
23
+ * tells the site's managers and the workspace's owners once, through the
24
+ * risk notice seam (AGL-3368). Any plugin that sells through Stripe uses
24
25
  * the same two calls; nothing about the record's kind is known here.
25
26
  *
26
27
  * NOTHING IS REFUNDED OR CANCELED. The merchant decides for their own
@@ -28,7 +29,12 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
28
29
  *
29
30
  * Dependencies are passed in — the Firestore handle and the notifier — so a
30
31
  * plugin's spec that substitutes the admin barrel substitutes these too.
31
- *=========================================*/ import { addPaymentRiskSignal, NOTHING_MOVED, PAYMENT_RISK_FIELD, paymentRiskAdvice, paymentRiskTitle, settlePaymentRiskDispute } from "@aglyn/aglyn/app-utils/payment-risk";
32
+ *=========================================*/ import { addPaymentRiskSignal, PAYMENT_RISK_FIELD, settlePaymentRiskDispute } from "@aglyn/aglyn/app-utils/payment-risk";
33
+ /** The risk notice each Stripe signal on a sale is told as. */ export const PAYMENT_RISK_NOTICE_KINDS = {
34
+ 'early-fraud-warning': 'sale-fraud-warning',
35
+ 'radar-review': 'sale-payment-review',
36
+ dispute: 'sale-dispute'
37
+ };
32
38
  /**
33
39
  * Stamp `signal` on `input.ref` and notify, once. Returns true when this
34
40
  * delivery wrote it; false for a redelivery or a record that is gone.
@@ -47,12 +53,21 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
47
53
  }));
48
54
  return true;
49
55
  });
50
- if (written && input.hostId) {
51
- await deps.notify(input.hostId, {
52
- type: input.notificationType,
53
- title: paymentRiskTitle(input.signal.kind),
54
- body: `${input.subjectLabel}. ${paymentRiskAdvice(input.signal.kind)}` + (input.signal.detail ? ` Stripe says: ${input.signal.detail.replace(/_/g, ' ')}.` : '') + ` ${NOTHING_MOVED}`,
55
- link: input.link
56
+ if (written && (input.hostId || input.orgId)) {
57
+ var _input_noticeKind, _input_orgId, _input_amount, _input_evidenceDueByMs;
58
+ await deps.notifyRisk({
59
+ kind: (_input_noticeKind = input.noticeKind) != null ? _input_noticeKind : PAYMENT_RISK_NOTICE_KINDS[input.signal.kind],
60
+ orgId: (_input_orgId = input.orgId) != null ? _input_orgId : null,
61
+ hostId: input.hostId || null,
62
+ item: {
63
+ label: input.subjectLabel,
64
+ path: input.link
65
+ },
66
+ occurredAtMs: input.signal.atMs,
67
+ amount: (_input_amount = input.amount) != null ? _input_amount : null,
68
+ evidenceDueByMs: (_input_evidenceDueByMs = input.evidenceDueByMs) != null ? _input_evidenceDueByMs : null,
69
+ dedupeKey: `sale:${input.signal.stripeObjectId}`,
70
+ staffEvidence: input.signal.detail ? `Stripe says: ${input.signal.detail.replace(/_/g, ' ')}.` : null
56
71
  }).catch(()=>undefined);
57
72
  }
58
73
  return written;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/payment-risk-record.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\n/*==========================================\n * STAMP A STRIPE FRAUD SIGNAL ON A MERCHANT'S RECORD (AGL-3360).\n *\n * The server half of `@aglyn/aglyn/app-utils/payment-risk`. A plugin that\n * finds its own record behind a charge — an order, a booking — hands the\n * document here, and this writes the `paymentRisk` field once per signal and\n * tells the site's managers once. Any plugin that sells through Stripe uses\n * the same two calls; nothing about the record's kind is known here.\n *\n * NOTHING IS REFUNDED OR CANCELED. The merchant decides for their own\n * customer; the notification says so.\n *\n * Dependencies are passed in — the Firestore handle and the notifier — so a\n * plugin's spec that substitutes the admin barrel substitutes these too.\n *=========================================*/\n\nimport {\n addPaymentRiskSignal,\n NOTHING_MOVED,\n PAYMENT_RISK_FIELD,\n type PaymentRisk,\n type PaymentRiskSignal,\n paymentRiskAdvice,\n paymentRiskTitle,\n settlePaymentRiskDispute,\n} from '@aglyn/aglyn/app-utils/payment-risk'\nimport type { AglynNotificationType } from '@aglyn/aglyn/app-utils/notifications'\n\n/** The notifier's shape: `notifyHostManagers` from the admin barrel. */\nexport type PaymentRiskNotifier = (\n hostId: string,\n payload: {\n type: AglynNotificationType\n title: string\n body: string\n link: string\n },\n) => Promise<void>\n\nexport interface RecordPaymentRiskInput {\n ref: FirebaseFirestore.DocumentReference\n signal: PaymentRiskSignal\n /** The site whose managers are told. Empty tells nobody. */\n hostId: string\n /** What the record is, as the merchant calls it: `Order 1042`, `Booking`. */\n subjectLabel: string\n /** The console path the notification opens. */\n link: string\n /** The plugin's own notification type: `content.order`, `content.booking`. */\n notificationType: AglynNotificationType\n /**\n * More fields to write in the same transaction, from the record as read —\n * a plugin's own activity line (an order's timeline) rides here.\n */\n extraUpdate?: (data: Record<string, unknown>) => Record<string, unknown>\n}\n\n/**\n * Stamp `signal` on `input.ref` and notify, once. Returns true when this\n * delivery wrote it; false for a redelivery or a record that is gone.\n * Throws only when the write throws, which the webhook answers with a 500 and\n * Stripe's redelivery.\n */\nexport async function recordPaymentRiskOnRecord(\n input: RecordPaymentRiskInput,\n deps: {\n firestore: FirebaseFirestore.Firestore\n notify: PaymentRiskNotifier\n },\n): Promise<boolean> {\n const written = await deps.firestore.runTransaction(async (transaction) => {\n const fresh = await transaction.get(input.ref)\n if (!fresh.exists) return false\n const data = (fresh.data() ?? {}) as Record<string, unknown>\n const next = addPaymentRiskSignal(\n data[PAYMENT_RISK_FIELD] as PaymentRisk | undefined,\n input.signal,\n )\n if (!next) return false\n transaction.update(input.ref, {\n ...(input.extraUpdate ? input.extraUpdate(data) : {}),\n [PAYMENT_RISK_FIELD]: next,\n })\n return true\n })\n if (written && input.hostId) {\n await deps\n .notify(input.hostId, {\n type: input.notificationType,\n title: paymentRiskTitle(input.signal.kind),\n body:\n `${input.subjectLabel}. ${paymentRiskAdvice(input.signal.kind)}` +\n (input.signal.detail\n ? ` Stripe says: ${input.signal.detail.replace(/_/g, ' ')}.`\n : '') +\n ` ${NOTHING_MOVED}`,\n link: input.link,\n })\n .catch(() => undefined)\n }\n return written\n}\n\n/**\n * Record a dispute's outcome on the signal already stamped for it. Silent:\n * the plugin that owns money movement on a lost dispute says so itself.\n */\nexport async function settlePaymentRiskDisputeOnRecord(\n ref: FirebaseFirestore.DocumentReference,\n disputeId: string,\n outcome: string,\n deps: { firestore: FirebaseFirestore.Firestore },\n): Promise<boolean> {\n return deps.firestore.runTransaction(async (transaction) => {\n const fresh = await transaction.get(ref)\n if (!fresh.exists) return false\n const next = settlePaymentRiskDispute(\n fresh.get(PAYMENT_RISK_FIELD) as PaymentRisk | undefined,\n disputeId,\n outcome,\n )\n if (!next) return false\n transaction.update(ref, { [PAYMENT_RISK_FIELD]: next })\n return true\n })\n}\n"],"names":["addPaymentRiskSignal","NOTHING_MOVED","PAYMENT_RISK_FIELD","paymentRiskAdvice","paymentRiskTitle","settlePaymentRiskDispute","recordPaymentRiskOnRecord","input","deps","written","firestore","runTransaction","transaction","fresh","get","ref","exists","data","next","signal","update","extraUpdate","hostId","notify","type","notificationType","title","kind","body","subjectLabel","detail","replace","link","catch","undefined","settlePaymentRiskDisputeOnRecord","disputeId","outcome"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;2CAc2C,GAE3C,SACEA,oBAAoB,EACpBC,aAAa,EACbC,kBAAkB,EAGlBC,iBAAiB,EACjBC,gBAAgB,EAChBC,wBAAwB,QACnB,sCAAqC;AAgC5C;;;;;CAKC,GACD,OAAO,eAAeC,0BACpBC,KAA6B,EAC7BC,IAGC;IAED,MAAMC,UAAU,MAAMD,KAAKE,SAAS,CAACC,cAAc,CAAC,OAAOC;YAG3CC;QAFd,MAAMA,QAAQ,MAAMD,YAAYE,GAAG,CAACP,MAAMQ,GAAG;QAC7C,IAAI,CAACF,MAAMG,MAAM,EAAE,OAAO;QAC1B,MAAMC,QAAQJ,cAAAA,MAAMI,IAAI,cAAVJ,cAAgB,CAAC;QAC/B,MAAMK,OAAOlB,qBACXiB,IAAI,CAACf,mBAAmB,EACxBK,MAAMY,MAAM;QAEd,IAAI,CAACD,MAAM,OAAO;QAClBN,YAAYQ,MAAM,CAACb,MAAMQ,GAAG,EAAE,aACxBR,MAAMc,WAAW,GAAGd,MAAMc,WAAW,CAACJ,QAAQ,CAAC;YACnD,CAACf,mBAAmB,EAAEgB;;QAExB,OAAO;IACT;IACA,IAAIT,WAAWF,MAAMe,MAAM,EAAE;QAC3B,MAAMd,KACHe,MAAM,CAAChB,MAAMe,MAAM,EAAE;YACpBE,MAAMjB,MAAMkB,gBAAgB;YAC5BC,OAAOtB,iBAAiBG,MAAMY,MAAM,CAACQ,IAAI;YACzCC,MACE,GAAGrB,MAAMsB,YAAY,CAAC,EAAE,EAAE1B,kBAAkBI,MAAMY,MAAM,CAACQ,IAAI,GAAG,GAC/DpB,CAAAA,MAAMY,MAAM,CAACW,MAAM,GAChB,CAAC,cAAc,EAAEvB,MAAMY,MAAM,CAACW,MAAM,CAACC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,GAC1D,EAAC,IACL,CAAC,CAAC,EAAE9B,eAAe;YACrB+B,MAAMzB,MAAMyB,IAAI;QAClB,GACCC,KAAK,CAAC,IAAMC;IACjB;IACA,OAAOzB;AACT;AAEA;;;CAGC,GACD,OAAO,eAAe0B,iCACpBpB,GAAwC,EACxCqB,SAAiB,EACjBC,OAAe,EACf7B,IAAgD;IAEhD,OAAOA,KAAKE,SAAS,CAACC,cAAc,CAAC,OAAOC;QAC1C,MAAMC,QAAQ,MAAMD,YAAYE,GAAG,CAACC;QACpC,IAAI,CAACF,MAAMG,MAAM,EAAE,OAAO;QAC1B,MAAME,OAAOb,yBACXQ,MAAMC,GAAG,CAACZ,qBACVkC,WACAC;QAEF,IAAI,CAACnB,MAAM,OAAO;QAClBN,YAAYQ,MAAM,CAACL,KAAK;YAAE,CAACb,mBAAmB,EAAEgB;QAAK;QACrD,OAAO;IACT;AACF"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/payment-risk-record.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\n/*==========================================\n * STAMP A STRIPE FRAUD SIGNAL ON A MERCHANT'S RECORD (AGL-3360).\n *\n * The server half of `@aglyn/aglyn/app-utils/payment-risk`. A plugin that\n * finds its own record behind a charge — an order, a booking — hands the\n * document here, and this writes the `paymentRisk` field once per signal and\n * tells the site's managers and the workspace's owners once, through the\n * risk notice seam (AGL-3368). Any plugin that sells through Stripe uses\n * the same two calls; nothing about the record's kind is known here.\n *\n * NOTHING IS REFUNDED OR CANCELED. The merchant decides for their own\n * customer; the notification says so.\n *\n * Dependencies are passed in — the Firestore handle and the notifier — so a\n * plugin's spec that substitutes the admin barrel substitutes these too.\n *=========================================*/\n\nimport {\n addPaymentRiskSignal,\n PAYMENT_RISK_FIELD,\n type PaymentRisk,\n type PaymentRiskSignal,\n type PaymentRiskSignalKind,\n settlePaymentRiskDispute,\n} from '@aglyn/aglyn/app-utils/payment-risk'\nimport type { AglynNotificationType } from '@aglyn/aglyn/app-utils/notifications'\nimport type { RiskEventKind } from '@aglyn/shared-util-email/risk-notice-catalog'\nimport type { RiskEventInput } from './risk-notice'\n\n/**\n * The notifier's shape: `notifyRiskEvent` from the admin barrel (AGL-3368),\n * which tells the site's managers and the workspace's owners and admins what\n * arrived, what it means, and how to refund, keep or answer it.\n */\nexport type PaymentRiskNotifier = (input: RiskEventInput) => Promise<unknown>\n\n/** The risk notice each Stripe signal on a sale is told as. */\nexport const PAYMENT_RISK_NOTICE_KINDS: Record<PaymentRiskSignalKind, RiskEventKind> = {\n 'early-fraud-warning': 'sale-fraud-warning',\n 'radar-review': 'sale-payment-review',\n dispute: 'sale-dispute',\n}\n\nexport interface RecordPaymentRiskInput {\n ref: FirebaseFirestore.DocumentReference\n signal: PaymentRiskSignal\n /** The site whose managers are told. Empty tells only `orgId`'s owners. */\n hostId: string\n /**\n * The workspace told when the record belongs to no site — a marketplace\n * sale's publisher. Resolved from the site otherwise.\n */\n orgId?: string | null\n /**\n * The notice to send, when the record's owner cannot act the way a shop\n * does on its own sale (a marketplace publisher cannot refund a charge the\n * platform is merchant of record for). Defaults by signal kind.\n */\n noticeKind?: RiskEventKind\n /** What the record is, as the merchant calls it: `Order 1042`, `Booking`. */\n subjectLabel: string\n /**\n * The console path the notice's actions open, in the stored-notification\n * shape (`/{hostId}/…`) — the order's own dialog, the Bookings page.\n */\n link: string\n /**\n * The plugin's own notification type. Kept for the record's callers; the\n * notice itself is always a `system.riskNotice` now.\n */\n notificationType?: AglynNotificationType\n /** `$56.00 USD`, when the plugin knows the amount. */\n amount?: string | null\n /** A dispute's evidence deadline, when the plugin knows it. */\n evidenceDueByMs?: number | null\n /**\n * More fields to write in the same transaction, from the record as read —\n * a plugin's own activity line (an order's timeline) rides here.\n */\n extraUpdate?: (data: Record<string, unknown>) => Record<string, unknown>\n}\n\n/**\n * Stamp `signal` on `input.ref` and notify, once. Returns true when this\n * delivery wrote it; false for a redelivery or a record that is gone.\n * Throws only when the write throws, which the webhook answers with a 500 and\n * Stripe's redelivery.\n */\nexport async function recordPaymentRiskOnRecord(\n input: RecordPaymentRiskInput,\n deps: {\n firestore: FirebaseFirestore.Firestore\n notifyRisk: PaymentRiskNotifier\n },\n): Promise<boolean> {\n const written = await deps.firestore.runTransaction(async (transaction) => {\n const fresh = await transaction.get(input.ref)\n if (!fresh.exists) return false\n const data = (fresh.data() ?? {}) as Record<string, unknown>\n const next = addPaymentRiskSignal(\n data[PAYMENT_RISK_FIELD] as PaymentRisk | undefined,\n input.signal,\n )\n if (!next) return false\n transaction.update(input.ref, {\n ...(input.extraUpdate ? input.extraUpdate(data) : {}),\n [PAYMENT_RISK_FIELD]: next,\n })\n return true\n })\n if (written && (input.hostId || input.orgId)) {\n await deps\n .notifyRisk({\n kind: input.noticeKind ?? PAYMENT_RISK_NOTICE_KINDS[input.signal.kind],\n orgId: input.orgId ?? null,\n hostId: input.hostId || null,\n item: { label: input.subjectLabel, path: input.link },\n occurredAtMs: input.signal.atMs,\n amount: input.amount ?? null,\n evidenceDueByMs: input.evidenceDueByMs ?? null,\n dedupeKey: `sale:${input.signal.stripeObjectId}`,\n staffEvidence: input.signal.detail\n ? `Stripe says: ${input.signal.detail.replace(/_/g, ' ')}.`\n : null,\n })\n .catch(() => undefined)\n }\n return written\n}\n\n/**\n * Record a dispute's outcome on the signal already stamped for it. Silent:\n * the plugin that owns money movement on a lost dispute says so itself.\n */\nexport async function settlePaymentRiskDisputeOnRecord(\n ref: FirebaseFirestore.DocumentReference,\n disputeId: string,\n outcome: string,\n deps: { firestore: FirebaseFirestore.Firestore },\n): Promise<boolean> {\n return deps.firestore.runTransaction(async (transaction) => {\n const fresh = await transaction.get(ref)\n if (!fresh.exists) return false\n const next = settlePaymentRiskDispute(\n fresh.get(PAYMENT_RISK_FIELD) as PaymentRisk | undefined,\n disputeId,\n outcome,\n )\n if (!next) return false\n transaction.update(ref, { [PAYMENT_RISK_FIELD]: next })\n return true\n })\n}\n"],"names":["addPaymentRiskSignal","PAYMENT_RISK_FIELD","settlePaymentRiskDispute","PAYMENT_RISK_NOTICE_KINDS","dispute","recordPaymentRiskOnRecord","input","deps","written","firestore","runTransaction","transaction","fresh","get","ref","exists","data","next","signal","update","extraUpdate","hostId","orgId","notifyRisk","kind","noticeKind","item","label","subjectLabel","path","link","occurredAtMs","atMs","amount","evidenceDueByMs","dedupeKey","stripeObjectId","staffEvidence","detail","replace","catch","undefined","settlePaymentRiskDisputeOnRecord","disputeId","outcome"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;2CAe2C,GAE3C,SACEA,oBAAoB,EACpBC,kBAAkB,EAIlBC,wBAAwB,QACnB,sCAAqC;AAY5C,6DAA6D,GAC7D,OAAO,MAAMC,4BAA0E;IACrF,uBAAuB;IACvB,gBAAgB;IAChBC,SAAS;AACX,EAAC;AAyCD;;;;;CAKC,GACD,OAAO,eAAeC,0BACpBC,KAA6B,EAC7BC,IAGC;IAED,MAAMC,UAAU,MAAMD,KAAKE,SAAS,CAACC,cAAc,CAAC,OAAOC;YAG3CC;QAFd,MAAMA,QAAQ,MAAMD,YAAYE,GAAG,CAACP,MAAMQ,GAAG;QAC7C,IAAI,CAACF,MAAMG,MAAM,EAAE,OAAO;QAC1B,MAAMC,QAAQJ,cAAAA,MAAMI,IAAI,cAAVJ,cAAgB,CAAC;QAC/B,MAAMK,OAAOjB,qBACXgB,IAAI,CAACf,mBAAmB,EACxBK,MAAMY,MAAM;QAEd,IAAI,CAACD,MAAM,OAAO;QAClBN,YAAYQ,MAAM,CAACb,MAAMQ,GAAG,EAAE,aACxBR,MAAMc,WAAW,GAAGd,MAAMc,WAAW,CAACJ,QAAQ,CAAC;YACnD,CAACf,mBAAmB,EAAEgB;;QAExB,OAAO;IACT;IACA,IAAIT,WAAYF,CAAAA,MAAMe,MAAM,IAAIf,MAAMgB,KAAK,AAAD,GAAI;YAGlChB,mBACCA,cAICA,eACSA;QARrB,MAAMC,KACHgB,UAAU,CAAC;YACVC,IAAI,GAAElB,oBAAAA,MAAMmB,UAAU,YAAhBnB,oBAAoBH,yBAAyB,CAACG,MAAMY,MAAM,CAACM,IAAI,CAAC;YACtEF,KAAK,GAAEhB,eAAAA,MAAMgB,KAAK,YAAXhB,eAAe;YACtBe,QAAQf,MAAMe,MAAM,IAAI;YACxBK,MAAM;gBAAEC,OAAOrB,MAAMsB,YAAY;gBAAEC,MAAMvB,MAAMwB,IAAI;YAAC;YACpDC,cAAczB,MAAMY,MAAM,CAACc,IAAI;YAC/BC,MAAM,GAAE3B,gBAAAA,MAAM2B,MAAM,YAAZ3B,gBAAgB;YACxB4B,eAAe,GAAE5B,yBAAAA,MAAM4B,eAAe,YAArB5B,yBAAyB;YAC1C6B,WAAW,CAAC,KAAK,EAAE7B,MAAMY,MAAM,CAACkB,cAAc,EAAE;YAChDC,eAAe/B,MAAMY,MAAM,CAACoB,MAAM,GAC9B,CAAC,aAAa,EAAEhC,MAAMY,MAAM,CAACoB,MAAM,CAACC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,GACzD;QACN,GACCC,KAAK,CAAC,IAAMC;IACjB;IACA,OAAOjC;AACT;AAEA;;;CAGC,GACD,OAAO,eAAekC,iCACpB5B,GAAwC,EACxC6B,SAAiB,EACjBC,OAAe,EACfrC,IAAgD;IAEhD,OAAOA,KAAKE,SAAS,CAACC,cAAc,CAAC,OAAOC;QAC1C,MAAMC,QAAQ,MAAMD,YAAYE,GAAG,CAACC;QACpC,IAAI,CAACF,MAAMG,MAAM,EAAE,OAAO;QAC1B,MAAME,OAAOf,yBACXU,MAAMC,GAAG,CAACZ,qBACV0C,WACAC;QAEF,IAAI,CAAC3B,MAAM,OAAO;QAClBN,YAAYQ,MAAM,CAACL,KAAK;YAAE,CAACb,mBAAmB,EAAEgB;QAAK;QACrD,OAAO;IACT;AACF"}
@@ -92,6 +92,9 @@ export declare function loadSystemEmail(templateKey: string): Promise<LoadedSyst
92
92
  * marketing site, or whose blocks could not be read. The chrome is in the
93
93
  * brand of the send — the org's email logo rides in its header, so a
94
94
  * white-label mail carries exactly one logo.
95
+ *
96
+ * Either way, a band the design draws itself is left out of the chrome
97
+ * (AGL-3372): a design that opens with its own Header gets the footer only.
95
98
  */
96
99
  export declare function renderLoadedSystemEmail(loaded: LoadedSystemEmail, merge?: Record<string, string>, options?: SystemEmailBrandOptions): RenderedSystemEmail | null;
97
100
  /**
@@ -15,7 +15,7 @@ var _process_env_NEXT_PUBLIC_CONSOLE_URL;
15
15
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
- */ import { EMAIL_NODE_ROOT_ID, SYSTEM_EMAIL_COLLECTION, buildDefaultEmailNodeMap, getSystemEmailTemplate, renderEmailHtml, substituteMergeTokens } from "@aglyn/shared-util-email";
18
+ */ import { EMAIL_NODE_ROOT_ID, PLATFORM_EMAIL_PALETTE, chromeForDesign, SYSTEM_EMAIL_COLLECTION, buildEmailPalette, buildDefaultEmailNodeMap, getSystemEmailTemplate, renderEmailHtml, substituteMergeTokens } from "@aglyn/shared-util-email";
19
19
  import { firebaseAdmin } from "./firebase-admin.js";
20
20
  import { platformMarketingHostId } from "./platform-marketing-consent.js";
21
21
  import { brandMergeTokens, collectReferencedComponentIds, composeReusableComponentNodes, decodeStoredNodes, resolveBrandingProfile, sanitizeAuthorHtml } from "@aglyn/aglyn/server";
@@ -168,6 +168,21 @@ export { isPlatformBrandedSend };
168
168
  return catalogDefault(definition);
169
169
  }
170
170
  }
171
+ /**
172
+ * The theme a system email is drawn in (AGL-3370): the console's palette,
173
+ * or, for a white-label org that set a brand color, the same palette with
174
+ * that color as its primary. Buttons use the console's corner radius.
175
+ */ function systemEmailTheme(options) {
176
+ var _options_brandPrimaryColor;
177
+ const primaryColor = String((_options_brandPrimaryColor = options.brandPrimaryColor) != null ? _options_brandPrimaryColor : '').trim();
178
+ return {
179
+ palette: primaryColor ? buildEmailPalette({
180
+ base: 'platform',
181
+ primaryColor
182
+ }) : PLATFORM_EMAIL_PALETTE,
183
+ buttonRadius: 4
184
+ };
185
+ }
171
186
  /**
172
187
  * Renders a loaded email for one recipient's merge values (AGL-768). No
173
188
  * Firestore access — call {@link loadSystemEmail} once, then this per
@@ -186,12 +201,29 @@ export { isPlatformBrandedSend };
186
201
  * marketing site, or whose blocks could not be read. The chrome is in the
187
202
  * brand of the send — the org's email logo rides in its header, so a
188
203
  * white-label mail carries exactly one logo.
204
+ *
205
+ * Either way, a band the design draws itself is left out of the chrome
206
+ * (AGL-3372): a design that opens with its own Header gets the footer only.
189
207
  */ export function renderLoadedSystemEmail(loaded, merge = {}, options = {}) {
190
208
  var _rendered_text;
191
209
  const merged = _extends({}, PLATFORM_BRAND_MERGE_TOKENS, merge);
192
210
  const blocks = drawsPlatformBlocks(loaded, merged, options);
211
+ const nodes = blocks ? composeReusableComponentNodes(loaded.nodes, loaded.components) : loaded.nodes;
212
+ // The brand's chrome around every copy, less any band the design draws
213
+ // itself (AGL-3372). Unexpanded placements draw nothing in this send, so
214
+ // they only count when the blocks are drawn.
215
+ const chrome = chromeForDesign({
216
+ stored: blocks ? loaded.nodes : null,
217
+ composed: nodes,
218
+ chrome: buildSystemEmailChrome({
219
+ definition: loaded.definition,
220
+ merged,
221
+ brandLogoUrl: options.brandLogoUrl,
222
+ brandHomeUrl: options.brandHomeUrl
223
+ })
224
+ });
193
225
  const rendered = renderEmailHtml(_extends({
194
- nodes: blocks ? composeReusableComponentNodes(loaded.nodes, loaded.components) : loaded.nodes,
226
+ nodes: nodes,
195
227
  // Besigner maps are rooted at '_@_', not renderEmailHtml's default
196
228
  // 'root' — without this a designed template rendered empty and the send
197
229
  // fell back to built-in copy, so designing did nothing (AGL-765).
@@ -208,14 +240,9 @@ export { isPlatformBrandedSend };
208
240
  // which is a broken-image box in the recipient's inbox (AGL-1224). The
209
241
  // chrome's logo resolves against it too.
210
242
  mediaOrigin: CONSOLE_ORIGIN
211
- }, blocks ? {} : {
212
- chrome: buildSystemEmailChrome({
213
- definition: loaded.definition,
214
- merged,
215
- brandLogoUrl: options.brandLogoUrl,
216
- brandHomeUrl: options.brandHomeUrl
217
- })
218
- }));
243
+ }, systemEmailTheme(options), chrome ? {
244
+ chrome
245
+ } : {}));
219
246
  if (!(rendered == null ? void 0 : rendered.html)) return null;
220
247
  return {
221
248
  subject: blankUnresolvedTokens(substituteMergeTokens(loaded.subjectTemplate, merged)),
@@ -265,7 +292,8 @@ export { isPlatformBrandedSend };
265
292
  merge: brandMergeTokens(branding),
266
293
  options: {
267
294
  brandLogoUrl: branding.emailLogoUrl,
268
- brandHomeUrl: branding.homeUrl
295
+ brandHomeUrl: branding.homeUrl,
296
+ brandPrimaryColor: branding.primaryColor
269
297
  },
270
298
  fromName: branding.fromName
271
299
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/render-system-email.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 EMAIL_NODE_ROOT_ID,\n SYSTEM_EMAIL_COLLECTION,\n buildDefaultEmailNodeMap,\n getSystemEmailTemplate,\n renderEmailHtml,\n substituteMergeTokens,\n type SystemEmailTemplateDefinition,\n} from '@aglyn/shared-util-email'\nimport { firebaseAdmin } from './firebase-admin'\nimport { platformMarketingHostId } from './platform-marketing-consent'\nimport {\n brandMergeTokens,\n collectReferencedComponentIds,\n composeReusableComponentNodes,\n decodeStoredNodes,\n resolveBrandingProfile,\n sanitizeAuthorHtml,\n type ReusableComponentTree,\n} from '@aglyn/aglyn/server'\nimport {\n hostComponentReader,\n loadReferencedComponents,\n type ComponentStoreLike,\n} from '@aglyn/aglyn/app-utils/load-referenced-components'\nimport {\n PLATFORM_BRAND_MERGE_TOKENS,\n buildSystemEmailChrome,\n isPlatformBrandedSend,\n type SystemEmailBrandOptions,\n} from '@aglyn/aglyn/app-utils/system-email-chrome'\n\n// Defined beside the chrome they decide, and re-exported for the callers and\n// specs that reach them through this module.\nexport { isPlatformBrandedSend }\nexport type { SystemEmailBrandOptions }\n\n/**\n * Blanks any `{{token}}` the caller did not supply.\n *\n * `substituteMergeTokens` leaves unknown tokens untouched, which is right for\n * campaigns (an author wants to see the tag they mistyped) but wrong here —\n * a system email goes to a customer, and \"You've been invited to\n * {{org.name}}\" is worse than a gap. Applied only in this resolver so\n * campaign behaviour is unchanged.\n */\nfunction blankUnresolvedTokens(value: string): string {\n return value.replace(/\\{\\{[^}]*\\}\\}/g, '')\n}\n\n/**\n * Origin a picked image in a system email is fetched from (AGL-1224).\n *\n * A system email is staff-designed and its media lives in the platform\n * library, which the CONSOLE's `/api/media/cdn` route serves — the tenant app\n * mounts the same route, but a recipient of an org invite has no site to\n * resolve against. Same expression `app/layout.tsx` uses for the console's\n * own canonical URL, so the two cannot drift per deploy.\n */\nconst CONSOLE_ORIGIN =\n process.env.NEXT_PUBLIC_CONSOLE_URL ?? 'https://app.aglyn.com'\n\n/**\n * Which copy a system email carries: a design staff published, or the\n * catalog's built-in copy, which is what every key without a usable design\n * sends (AGL-3322).\n */\nexport type SystemEmailSource = 'designed' | 'default'\n\nexport interface RenderedSystemEmail {\n subject: string\n html: string\n text: string\n source: SystemEmailSource\n}\n\n/**\n * A system email loaded once, ready to render for any number of recipients\n * without touching Firestore again (AGL-768): the published staff design, or\n * the catalog's built-in copy when there is none.\n */\nexport interface LoadedSystemEmail {\n definition: SystemEmailTemplateDefinition\n /** The design as stored, each email block still a placement; or the built-in copy's node map. */\n nodes: Record<string, unknown>\n subjectTemplate: string\n preheaderTemplate: string\n source: SystemEmailSource\n /**\n * The published definitions of the email blocks `nodes` places, keyed by\n * id, read from the platform marketing site ONCE with the template\n * (AGL-3318), so a batch composes every recipient's copy without another\n * read. Absent when the design places none, when the deployment names no\n * marketing site, and when the read failed; each placement then renders as\n * nothing.\n */\n components?: Record<string, ReusableComponentTree>\n}\n\n/**\n * The email blocks a platform email places, read from the site that owns\n * them (AGL-3318), or `undefined` when there is nothing to read or the read\n * failed.\n *\n * WHICH SITE. A block is a reusable component, and a component belongs to a\n * site; a platform email belongs to none. The header and footer Aglyn's own\n * mail wears are the ones on its marketing site, which the deployment names\n * in `PLATFORM_MARKETING_HOST_ID`, the setting product-email consent already\n * reads. An install that names none reads nothing, and a block placed in its\n * system emails renders as nothing.\n *\n * NEVER A REASON NOT TO SEND. A site's own email fails its whole load when a\n * component read fails, and falls back to its built-in copy. Here the copy is\n * staff's design and the blocks are another site's chrome, so a failed read\n * costs the blocks and nothing else: it is logged, and the design renders\n * inside the coded header and footer instead (see\n * {@link renderLoadedSystemEmail}) rather than being traded for the built-in\n * copy. A password reset whose header could not be read still carries its\n * link.\n */\nasync function loadPlatformEmailBlocks(\n firestore: ComponentStoreLike,\n templateKey: string,\n nodes: Record<string, unknown>,\n): Promise<Record<string, ReusableComponentTree> | undefined> {\n // A design that places nothing costs nothing: no setting read, no read.\n if (!collectReferencedComponentIds(nodes as never).size) return undefined\n const hostId = platformMarketingHostId()\n if (!hostId) return undefined\n try {\n return await loadReferencedComponents(\n nodes,\n hostComponentReader(firestore, hostId),\n {\n onSkipped: (skipped) =>\n console.warn(\n JSON.stringify({\n tag: 'AGL-3318:platform-email-blocks-skipped',\n templateKey,\n hostId,\n skipped,\n }),\n ),\n },\n )\n } catch (error) {\n console.error(\n `system email template ${templateKey}: its email blocks failed to load from ${hostId}`,\n error,\n )\n return undefined\n }\n}\n\n/**\n * Whether one send draws the design's email blocks: a send under the\n * platform's own brand (see {@link isPlatformBrandedSend}) of a design whose\n * blocks the load could read. Every other send renders the stored nodes, each\n * block a placement that draws nothing, inside the coded chrome.\n */\nfunction drawsPlatformBlocks(\n loaded: LoadedSystemEmail,\n merged: Readonly<Record<string, string>>,\n options: SystemEmailBrandOptions,\n): loaded is LoadedSystemEmail & {\n components: Record<string, ReusableComponentTree>\n} {\n return (\n Boolean(loaded.components && Object.keys(loaded.components).length) &&\n isPlatformBrandedSend(merged, options)\n )\n}\n\n/**\n * A key's built-in copy as a loaded email (AGL-3322): the node map the editor\n * seeds a first design with, and the catalog subject. What a key sends until\n * staff publish a design, and whenever the published one cannot be read.\n */\nfunction catalogDefault(\n definition: SystemEmailTemplateDefinition,\n): LoadedSystemEmail {\n return {\n definition,\n nodes: buildDefaultEmailNodeMap(definition),\n subjectTemplate: definition.defaultSubject,\n preheaderTemplate: '',\n source: 'default',\n }\n}\n\n/**\n * Loads what a key sends — ONE Firestore read — ready to render per\n * recipient: the published staff design, or the catalog's built-in copy\n * (AGL-3322) when there is none to use (no document, no published version,\n * an empty node map, or a read failure).\n *\n * `null` only for a key that is unknown or not Resend-delivered, which has\n * nothing here to render. Split out from {@link renderSystemEmail} so a batch\n * send resolves the template once and renders it per recipient, rather than\n * one read each (AGL-768): a Firestore read per invite is fine, one per\n * recipient in a usage-email batch is not.\n *\n * Reads through the Admin SDK, which bypasses the staff-only rules on the\n * collection — the send path runs as the server, not as a signed-in user.\n *\n * A design that places email blocks costs one more read per block, from the\n * platform marketing site, made here with the template so a batch pays it\n * once too (AGL-3318). Whether a recipient's copy expands them is decided per\n * send, by {@link renderLoadedSystemEmail}.\n */\nexport async function loadSystemEmail(\n templateKey: string,\n): Promise<LoadedSystemEmail | null> {\n const definition = getSystemEmailTemplate(templateKey)\n if (!definition) return null\n // A Firebase- or Stripe-delivered email is sent by that service from its\n // own templates, so rendering one here would produce output nothing ever\n // sends (AGL-767).\n if (definition.deliveredBy !== 'resend') return null\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const templateRef = firestore\n .collection(SYSTEM_EMAIL_COLLECTION)\n .doc(templateKey)\n const templateSnapshot = await templateRef.get()\n if (!templateSnapshot.exists) return catalogDefault(definition)\n\n const versionId = templateSnapshot.get('versionId')\n // Cleared by \"reset to default\", which nulls the pointer rather than\n // deleting the document so the audit trail survives.\n if (!versionId) return catalogDefault(definition)\n\n const versionSnapshot = await templateRef\n .collection('versions')\n .doc(String(versionId))\n .get()\n // BOTH stored forms (AGL-1223). The email besigner compresses like every\n // other besigner document, and versions written before it did are still\n // plain maps — `decodeStoredNodes` returns those unchanged.\n //\n // Reading raw is the failure the guard below cannot catch: over a\n // `Buffer`, `Object.keys` counts BYTE INDICES, so the emptiness test\n // passes and a staff-authored email renders empty rather than falling\n // back to its built-in copy.\n const nodes = decodeStoredNodes<Record<string, unknown>>(\n versionSnapshot.get('nodes'),\n )\n if (!nodes || !Object.keys(nodes).length) return catalogDefault(definition)\n\n const components = await loadPlatformEmailBlocks(\n firestore,\n templateKey,\n nodes,\n )\n return {\n definition,\n nodes,\n subjectTemplate:\n String(templateSnapshot.get('subject') ?? '') ||\n definition.defaultSubject,\n preheaderTemplate: String(templateSnapshot.get('preheader') ?? ''),\n source: 'designed',\n ...(components && { components }),\n }\n } catch (error) {\n // Never let a template problem block the send, or strip it bare: the\n // recipient gets the built-in copy, in the same header and footer.\n console.error(\n `system email template ${templateKey} failed to load; sending its built-in copy`,\n error,\n )\n return catalogDefault(definition)\n }\n}\n\n/**\n * Renders a loaded email for one recipient's merge values (AGL-768). No\n * Firestore access — call {@link loadSystemEmail} once, then this per\n * recipient. `null` only if the renderer produced no document at all.\n *\n * Which header and footer it wears is decided here, per send, because one\n * batch-loaded design goes to recipients of different brands:\n *\n * - A send under the platform's own brand of a design whose email blocks the\n * load read draws those blocks (AGL-3318), and nothing else: they ARE its\n * header and footer, and a second one around them would draw both.\n * - Every other send is drawn inside {@link buildSystemEmailChrome}'s chrome\n * (AGL-3322): the built-in copy, which places no blocks; a design sent to a\n * white-label org's people, whose blocks stay unexpanded and draw nothing,\n * since they are the platform's; and a design on a deployment that names no\n * marketing site, or whose blocks could not be read. The chrome is in the\n * brand of the send — the org's email logo rides in its header, so a\n * white-label mail carries exactly one logo.\n */\nexport function renderLoadedSystemEmail(\n loaded: LoadedSystemEmail,\n merge: Record<string, string> = {},\n options: SystemEmailBrandOptions = {},\n): RenderedSystemEmail | null {\n const merged = { ...PLATFORM_BRAND_MERGE_TOKENS, ...merge }\n const blocks = drawsPlatformBlocks(loaded, merged, options)\n const rendered = renderEmailHtml({\n nodes: (blocks\n ? composeReusableComponentNodes(loaded.nodes as never, loaded.components)\n : loaded.nodes) as never,\n // Besigner maps are rooted at '_@_', not renderEmailHtml's default\n // 'root' — without this a designed template rendered empty and the send\n // fell back to built-in copy, so designing did nothing (AGL-765).\n rootId: EMAIL_NODE_ROOT_ID,\n subject: substituteMergeTokens(loaded.subjectTemplate, merged),\n preheader: substituteMergeTokens(loaded.preheaderTemplate, merged),\n merge: merged,\n // The same policy the console's own preview of these nodes applies —\n // `sanitizeCustomHtml` in the email plugin delegates to this exact\n // function, so what staff review and what the recipient receives are one\n // function of one string rather than two that have to be kept in step.\n sanitize: sanitizeAuthorHtml,\n // Without this a staff-picked image resolves to a site-relative CDN path,\n // which is a broken-image box in the recipient's inbox (AGL-1224). The\n // chrome's logo resolves against it too.\n mediaOrigin: CONSOLE_ORIGIN,\n // No `brandLogoUrl`: the org's email logo (AGL-2139) is the chrome\n // header's logo whenever there is one to draw, and a send that draws the\n // platform's blocks is by definition one that carries none.\n ...(blocks\n ? {}\n : {\n chrome: buildSystemEmailChrome({\n definition: loaded.definition,\n merged,\n brandLogoUrl: options.brandLogoUrl,\n brandHomeUrl: options.brandHomeUrl,\n }),\n }),\n })\n if (!rendered?.html) return null\n return {\n subject: blankUnresolvedTokens(\n substituteMergeTokens(loaded.subjectTemplate, merged),\n ),\n html: blankUnresolvedTokens(rendered.html),\n text: blankUnresolvedTokens(rendered.text ?? ''),\n source: loaded.source,\n }\n}\n\n/**\n * Renders the system email a key sends to one recipient (AGL-750): the\n * published staff design, or the catalog's built-in copy (AGL-3322), in the\n * header and footer of the brand it is sent under.\n *\n * **`null` only for a key that is unknown or not Resend-delivered**, which\n * has nothing here to render. No template document, no published version, an\n * empty node map and a Firestore failure all render the built-in copy — the\n * same copy the editor seeds a first design with — so a missing, half-saved\n * or broken template can never mean a customer receives no email, or a bare\n * one. Every caller still keeps its own hard-coded copy behind this, as the\n * last resort for a render that produced nothing.\n *\n * A single-recipient convenience over {@link loadSystemEmail} +\n * {@link renderLoadedSystemEmail}; a batch should call those two directly so\n * it reads the template once.\n */\nexport async function renderSystemEmail(\n templateKey: string,\n merge: Record<string, string> = {},\n options: SystemEmailBrandOptions = {},\n): Promise<RenderedSystemEmail | null> {\n const loaded = await loadSystemEmail(templateKey)\n return loaded ? renderLoadedSystemEmail(loaded, merge, options) : null\n}\n\n/**\n * The email a template sends right now, for the staff test-send endpoint\n * (AGL-766): exactly what a real send of it renders — the published design,\n * or the built-in copy, with the same header and footer — so a test shows\n * what recipients receive. `null` for an unknown or non-Resend key.\n */\nexport async function renderEffectiveSystemEmail(\n templateKey: string,\n merge: Record<string, string> = {},\n options: SystemEmailBrandOptions = {},\n): Promise<RenderedSystemEmail | null> {\n return renderSystemEmail(templateKey, merge, options)\n}\n\n/**\n * The brand one system email is sent under (AGL-3367): the merge tokens, the\n * chrome options and the sender name, from one place.\n *\n * `null` is the platform's own brand, and it is the answer for everything\n * the platform says on its own behalf: a staff alert, a legal receipt, an\n * enforcement notice. A workspace-context email passes the workspace's org\n * document, and `resolveBrandingProfile` decides from there: a white-label\n * org gets its own name, logo, home and support in the header and footer,\n * and every other org resolves to the platform profile, so the same call is\n * right for both.\n */\nexport interface SystemEmailBrand {\n merge: Record<string, string>\n options: SystemEmailBrandOptions\n /** The display name to send under; absent sends under the platform's. */\n fromName?: string\n /** The org's own name, for the `{{org.name}}` its copy may use. */\n orgName?: string\n}\n\n/** See {@link SystemEmailBrand}. */\nexport function systemEmailBrand(\n org: Readonly<Record<string, unknown>> | null | undefined,\n): SystemEmailBrand {\n if (!org) return { merge: {}, options: {} }\n const branding = resolveBrandingProfile(org as never)\n const orgName = typeof org['name'] === 'string' ? org['name'].trim() : ''\n return {\n ...(orgName ? { orgName } : {}),\n merge: brandMergeTokens(branding),\n options: {\n brandLogoUrl: branding.emailLogoUrl,\n brandHomeUrl: branding.homeUrl,\n },\n fromName: branding.fromName,\n }\n}\n\n/**\n * {@link systemEmailBrand} for an org read by id, for a sender that holds\n * only the id. A failed or missing read is the platform's brand: the email\n * still goes, and a white-label org's people see the platform's chrome on\n * one message rather than no message.\n */\nexport async function orgSystemEmailBrand(\n orgId: string | null | undefined,\n): Promise<SystemEmailBrand> {\n if (!orgId) return systemEmailBrand(null)\n try {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .get()\n return systemEmailBrand(snapshot.exists ? (snapshot.data() ?? null) : null)\n } catch (error) {\n console.error(`system email brand for org ${orgId} failed to load`, error)\n return systemEmailBrand(null)\n }\n}\n\n/** What a send takes from a rendered system email. */\nexport interface SystemEmailContent {\n subject: string\n text: string\n html?: string\n}\n\n/**\n * The subject, text and HTML to send, with the caller's own copy as the\n * last resort for a render that produced nothing: the shape every sender of\n * a system email repeats, written once.\n */\nexport function systemEmailContent(\n rendered: RenderedSystemEmail | null,\n fallback: { subject: string; text: string },\n): SystemEmailContent {\n return {\n subject: rendered?.subject || fallback.subject,\n text: rendered?.text || fallback.text,\n ...(rendered?.html ? { html: rendered.html } : {}),\n }\n}\n\n/**\n * Renders a key for one recipient in a brand and returns what to send\n * (AGL-3367): {@link renderSystemEmail} and {@link systemEmailContent} in\n * one call, for the senders with one message to build. Never throws; a\n * render that fails sends the fallback copy, which is what it sent before.\n */\nexport async function renderSystemEmailContent(\n templateKey: string,\n merge: Record<string, string>,\n brand: SystemEmailBrand,\n fallback: { subject: string; text: string },\n): Promise<SystemEmailContent> {\n const rendered = await renderSystemEmail(\n templateKey,\n { ...brand.merge, ...merge },\n brand.options,\n ).catch((error) => {\n console.error(`system email ${templateKey} failed to render`, error)\n return null\n })\n return systemEmailContent(rendered, fallback)\n}\n\nexport default renderSystemEmail\n"],"names":["process","EMAIL_NODE_ROOT_ID","SYSTEM_EMAIL_COLLECTION","buildDefaultEmailNodeMap","getSystemEmailTemplate","renderEmailHtml","substituteMergeTokens","firebaseAdmin","platformMarketingHostId","brandMergeTokens","collectReferencedComponentIds","composeReusableComponentNodes","decodeStoredNodes","resolveBrandingProfile","sanitizeAuthorHtml","hostComponentReader","loadReferencedComponents","PLATFORM_BRAND_MERGE_TOKENS","buildSystemEmailChrome","isPlatformBrandedSend","blankUnresolvedTokens","value","replace","CONSOLE_ORIGIN","env","NEXT_PUBLIC_CONSOLE_URL","loadPlatformEmailBlocks","firestore","templateKey","nodes","size","undefined","hostId","onSkipped","skipped","console","warn","JSON","stringify","tag","error","drawsPlatformBlocks","loaded","merged","options","Boolean","components","Object","keys","length","catalogDefault","definition","subjectTemplate","defaultSubject","preheaderTemplate","source","loadSystemEmail","deliveredBy","templateSnapshot","app","templateRef","collection","doc","get","exists","versionId","versionSnapshot","String","renderLoadedSystemEmail","merge","rendered","blocks","rootId","subject","preheader","sanitize","mediaOrigin","chrome","brandLogoUrl","brandHomeUrl","html","text","renderSystemEmail","renderEffectiveSystemEmail","systemEmailBrand","org","branding","orgName","trim","emailLogoUrl","homeUrl","fromName","orgSystemEmailBrand","orgId","snapshot","data","systemEmailContent","fallback","renderSystemEmailContent","brand","catch"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/render-system-email.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 EMAIL_NODE_ROOT_ID,\n PLATFORM_EMAIL_PALETTE,\n chromeForDesign,\n SYSTEM_EMAIL_COLLECTION,\n buildEmailPalette,\n buildDefaultEmailNodeMap,\n getSystemEmailTemplate,\n renderEmailHtml,\n substituteMergeTokens,\n type EmailTheme,\n type SystemEmailTemplateDefinition,\n} from '@aglyn/shared-util-email'\nimport { firebaseAdmin } from './firebase-admin'\nimport { platformMarketingHostId } from './platform-marketing-consent'\nimport {\n brandMergeTokens,\n collectReferencedComponentIds,\n composeReusableComponentNodes,\n decodeStoredNodes,\n resolveBrandingProfile,\n sanitizeAuthorHtml,\n type ReusableComponentTree,\n} from '@aglyn/aglyn/server'\nimport {\n hostComponentReader,\n loadReferencedComponents,\n type ComponentStoreLike,\n} from '@aglyn/aglyn/app-utils/load-referenced-components'\nimport {\n PLATFORM_BRAND_MERGE_TOKENS,\n buildSystemEmailChrome,\n isPlatformBrandedSend,\n type SystemEmailBrandOptions,\n} from '@aglyn/aglyn/app-utils/system-email-chrome'\n\n// Defined beside the chrome they decide, and re-exported for the callers and\n// specs that reach them through this module.\nexport { isPlatformBrandedSend }\nexport type { SystemEmailBrandOptions }\n\n/**\n * Blanks any `{{token}}` the caller did not supply.\n *\n * `substituteMergeTokens` leaves unknown tokens untouched, which is right for\n * campaigns (an author wants to see the tag they mistyped) but wrong here —\n * a system email goes to a customer, and \"You've been invited to\n * {{org.name}}\" is worse than a gap. Applied only in this resolver so\n * campaign behaviour is unchanged.\n */\nfunction blankUnresolvedTokens(value: string): string {\n return value.replace(/\\{\\{[^}]*\\}\\}/g, '')\n}\n\n/**\n * Origin a picked image in a system email is fetched from (AGL-1224).\n *\n * A system email is staff-designed and its media lives in the platform\n * library, which the CONSOLE's `/api/media/cdn` route serves — the tenant app\n * mounts the same route, but a recipient of an org invite has no site to\n * resolve against. Same expression `app/layout.tsx` uses for the console's\n * own canonical URL, so the two cannot drift per deploy.\n */\nconst CONSOLE_ORIGIN =\n process.env.NEXT_PUBLIC_CONSOLE_URL ?? 'https://app.aglyn.com'\n\n/**\n * Which copy a system email carries: a design staff published, or the\n * catalog's built-in copy, which is what every key without a usable design\n * sends (AGL-3322).\n */\nexport type SystemEmailSource = 'designed' | 'default'\n\nexport interface RenderedSystemEmail {\n subject: string\n html: string\n text: string\n source: SystemEmailSource\n}\n\n/**\n * A system email loaded once, ready to render for any number of recipients\n * without touching Firestore again (AGL-768): the published staff design, or\n * the catalog's built-in copy when there is none.\n */\nexport interface LoadedSystemEmail {\n definition: SystemEmailTemplateDefinition\n /** The design as stored, each email block still a placement; or the built-in copy's node map. */\n nodes: Record<string, unknown>\n subjectTemplate: string\n preheaderTemplate: string\n source: SystemEmailSource\n /**\n * The published definitions of the email blocks `nodes` places, keyed by\n * id, read from the platform marketing site ONCE with the template\n * (AGL-3318), so a batch composes every recipient's copy without another\n * read. Absent when the design places none, when the deployment names no\n * marketing site, and when the read failed; each placement then renders as\n * nothing.\n */\n components?: Record<string, ReusableComponentTree>\n}\n\n/**\n * The email blocks a platform email places, read from the site that owns\n * them (AGL-3318), or `undefined` when there is nothing to read or the read\n * failed.\n *\n * WHICH SITE. A block is a reusable component, and a component belongs to a\n * site; a platform email belongs to none. The header and footer Aglyn's own\n * mail wears are the ones on its marketing site, which the deployment names\n * in `PLATFORM_MARKETING_HOST_ID`, the setting product-email consent already\n * reads. An install that names none reads nothing, and a block placed in its\n * system emails renders as nothing.\n *\n * NEVER A REASON NOT TO SEND. A site's own email fails its whole load when a\n * component read fails, and falls back to its built-in copy. Here the copy is\n * staff's design and the blocks are another site's chrome, so a failed read\n * costs the blocks and nothing else: it is logged, and the design renders\n * inside the coded header and footer instead (see\n * {@link renderLoadedSystemEmail}) rather than being traded for the built-in\n * copy. A password reset whose header could not be read still carries its\n * link.\n */\nasync function loadPlatformEmailBlocks(\n firestore: ComponentStoreLike,\n templateKey: string,\n nodes: Record<string, unknown>,\n): Promise<Record<string, ReusableComponentTree> | undefined> {\n // A design that places nothing costs nothing: no setting read, no read.\n if (!collectReferencedComponentIds(nodes as never).size) return undefined\n const hostId = platformMarketingHostId()\n if (!hostId) return undefined\n try {\n return await loadReferencedComponents(\n nodes,\n hostComponentReader(firestore, hostId),\n {\n onSkipped: (skipped) =>\n console.warn(\n JSON.stringify({\n tag: 'AGL-3318:platform-email-blocks-skipped',\n templateKey,\n hostId,\n skipped,\n }),\n ),\n },\n )\n } catch (error) {\n console.error(\n `system email template ${templateKey}: its email blocks failed to load from ${hostId}`,\n error,\n )\n return undefined\n }\n}\n\n/**\n * Whether one send draws the design's email blocks: a send under the\n * platform's own brand (see {@link isPlatformBrandedSend}) of a design whose\n * blocks the load could read. Every other send renders the stored nodes, each\n * block a placement that draws nothing, inside the coded chrome.\n */\nfunction drawsPlatformBlocks(\n loaded: LoadedSystemEmail,\n merged: Readonly<Record<string, string>>,\n options: SystemEmailBrandOptions,\n): loaded is LoadedSystemEmail & {\n components: Record<string, ReusableComponentTree>\n} {\n return (\n Boolean(loaded.components && Object.keys(loaded.components).length) &&\n isPlatformBrandedSend(merged, options)\n )\n}\n\n/**\n * A key's built-in copy as a loaded email (AGL-3322): the node map the editor\n * seeds a first design with, and the catalog subject. What a key sends until\n * staff publish a design, and whenever the published one cannot be read.\n */\nfunction catalogDefault(\n definition: SystemEmailTemplateDefinition,\n): LoadedSystemEmail {\n return {\n definition,\n nodes: buildDefaultEmailNodeMap(definition),\n subjectTemplate: definition.defaultSubject,\n preheaderTemplate: '',\n source: 'default',\n }\n}\n\n/**\n * Loads what a key sends — ONE Firestore read — ready to render per\n * recipient: the published staff design, or the catalog's built-in copy\n * (AGL-3322) when there is none to use (no document, no published version,\n * an empty node map, or a read failure).\n *\n * `null` only for a key that is unknown or not Resend-delivered, which has\n * nothing here to render. Split out from {@link renderSystemEmail} so a batch\n * send resolves the template once and renders it per recipient, rather than\n * one read each (AGL-768): a Firestore read per invite is fine, one per\n * recipient in a usage-email batch is not.\n *\n * Reads through the Admin SDK, which bypasses the staff-only rules on the\n * collection — the send path runs as the server, not as a signed-in user.\n *\n * A design that places email blocks costs one more read per block, from the\n * platform marketing site, made here with the template so a batch pays it\n * once too (AGL-3318). Whether a recipient's copy expands them is decided per\n * send, by {@link renderLoadedSystemEmail}.\n */\nexport async function loadSystemEmail(\n templateKey: string,\n): Promise<LoadedSystemEmail | null> {\n const definition = getSystemEmailTemplate(templateKey)\n if (!definition) return null\n // A Firebase- or Stripe-delivered email is sent by that service from its\n // own templates, so rendering one here would produce output nothing ever\n // sends (AGL-767).\n if (definition.deliveredBy !== 'resend') return null\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const templateRef = firestore\n .collection(SYSTEM_EMAIL_COLLECTION)\n .doc(templateKey)\n const templateSnapshot = await templateRef.get()\n if (!templateSnapshot.exists) return catalogDefault(definition)\n\n const versionId = templateSnapshot.get('versionId')\n // Cleared by \"reset to default\", which nulls the pointer rather than\n // deleting the document so the audit trail survives.\n if (!versionId) return catalogDefault(definition)\n\n const versionSnapshot = await templateRef\n .collection('versions')\n .doc(String(versionId))\n .get()\n // BOTH stored forms (AGL-1223). The email besigner compresses like every\n // other besigner document, and versions written before it did are still\n // plain maps — `decodeStoredNodes` returns those unchanged.\n //\n // Reading raw is the failure the guard below cannot catch: over a\n // `Buffer`, `Object.keys` counts BYTE INDICES, so the emptiness test\n // passes and a staff-authored email renders empty rather than falling\n // back to its built-in copy.\n const nodes = decodeStoredNodes<Record<string, unknown>>(\n versionSnapshot.get('nodes'),\n )\n if (!nodes || !Object.keys(nodes).length) return catalogDefault(definition)\n\n const components = await loadPlatformEmailBlocks(\n firestore,\n templateKey,\n nodes,\n )\n return {\n definition,\n nodes,\n subjectTemplate:\n String(templateSnapshot.get('subject') ?? '') ||\n definition.defaultSubject,\n preheaderTemplate: String(templateSnapshot.get('preheader') ?? ''),\n source: 'designed',\n ...(components && { components }),\n }\n } catch (error) {\n // Never let a template problem block the send, or strip it bare: the\n // recipient gets the built-in copy, in the same header and footer.\n console.error(\n `system email template ${templateKey} failed to load; sending its built-in copy`,\n error,\n )\n return catalogDefault(definition)\n }\n}\n\n/**\n * The theme a system email is drawn in (AGL-3370): the console's palette,\n * or, for a white-label org that set a brand color, the same palette with\n * that color as its primary. Buttons use the console's corner radius.\n */\nfunction systemEmailTheme(options: SystemEmailBrandOptions): EmailTheme {\n const primaryColor = String(options.brandPrimaryColor ?? '').trim()\n return {\n palette: primaryColor\n ? buildEmailPalette({ base: 'platform', primaryColor })\n : PLATFORM_EMAIL_PALETTE,\n buttonRadius: 4,\n }\n}\n\n/**\n * Renders a loaded email for one recipient's merge values (AGL-768). No\n * Firestore access — call {@link loadSystemEmail} once, then this per\n * recipient. `null` only if the renderer produced no document at all.\n *\n * Which header and footer it wears is decided here, per send, because one\n * batch-loaded design goes to recipients of different brands:\n *\n * - A send under the platform's own brand of a design whose email blocks the\n * load read draws those blocks (AGL-3318), and nothing else: they ARE its\n * header and footer, and a second one around them would draw both.\n * - Every other send is drawn inside {@link buildSystemEmailChrome}'s chrome\n * (AGL-3322): the built-in copy, which places no blocks; a design sent to a\n * white-label org's people, whose blocks stay unexpanded and draw nothing,\n * since they are the platform's; and a design on a deployment that names no\n * marketing site, or whose blocks could not be read. The chrome is in the\n * brand of the send — the org's email logo rides in its header, so a\n * white-label mail carries exactly one logo.\n *\n * Either way, a band the design draws itself is left out of the chrome\n * (AGL-3372): a design that opens with its own Header gets the footer only.\n */\nexport function renderLoadedSystemEmail(\n loaded: LoadedSystemEmail,\n merge: Record<string, string> = {},\n options: SystemEmailBrandOptions = {},\n): RenderedSystemEmail | null {\n const merged = { ...PLATFORM_BRAND_MERGE_TOKENS, ...merge }\n const blocks = drawsPlatformBlocks(loaded, merged, options)\n const nodes = (\n blocks\n ? composeReusableComponentNodes(loaded.nodes as never, loaded.components)\n : loaded.nodes\n ) as Record<string, unknown>\n // The brand's chrome around every copy, less any band the design draws\n // itself (AGL-3372). Unexpanded placements draw nothing in this send, so\n // they only count when the blocks are drawn.\n const chrome = chromeForDesign({\n stored: blocks ? loaded.nodes : null,\n composed: nodes,\n chrome: buildSystemEmailChrome({\n definition: loaded.definition,\n merged,\n brandLogoUrl: options.brandLogoUrl,\n brandHomeUrl: options.brandHomeUrl,\n }),\n })\n const rendered = renderEmailHtml({\n nodes: nodes as never,\n // Besigner maps are rooted at '_@_', not renderEmailHtml's default\n // 'root' — without this a designed template rendered empty and the send\n // fell back to built-in copy, so designing did nothing (AGL-765).\n rootId: EMAIL_NODE_ROOT_ID,\n subject: substituteMergeTokens(loaded.subjectTemplate, merged),\n preheader: substituteMergeTokens(loaded.preheaderTemplate, merged),\n merge: merged,\n // The same policy the console's own preview of these nodes applies —\n // `sanitizeCustomHtml` in the email plugin delegates to this exact\n // function, so what staff review and what the recipient receives are one\n // function of one string rather than two that have to be kept in step.\n sanitize: sanitizeAuthorHtml,\n // Without this a staff-picked image resolves to a site-relative CDN path,\n // which is a broken-image box in the recipient's inbox (AGL-1224). The\n // chrome's logo resolves against it too.\n mediaOrigin: CONSOLE_ORIGIN,\n // In the console's theme (AGL-3370), under a white-label org's own color\n // when it set one: a color staff picked in the Besigner is a palette\n // token, and a button nobody colored wears the brand's accent.\n ...systemEmailTheme(options),\n // No `brandLogoUrl`: the org's email logo (AGL-2139) is the chrome\n // header's logo whenever there is one to draw, and a send that draws the\n // platform's blocks is by definition one that carries none.\n ...(chrome ? { chrome } : {}),\n })\n if (!rendered?.html) return null\n return {\n subject: blankUnresolvedTokens(\n substituteMergeTokens(loaded.subjectTemplate, merged),\n ),\n html: blankUnresolvedTokens(rendered.html),\n text: blankUnresolvedTokens(rendered.text ?? ''),\n source: loaded.source,\n }\n}\n\n/**\n * Renders the system email a key sends to one recipient (AGL-750): the\n * published staff design, or the catalog's built-in copy (AGL-3322), in the\n * header and footer of the brand it is sent under.\n *\n * **`null` only for a key that is unknown or not Resend-delivered**, which\n * has nothing here to render. No template document, no published version, an\n * empty node map and a Firestore failure all render the built-in copy — the\n * same copy the editor seeds a first design with — so a missing, half-saved\n * or broken template can never mean a customer receives no email, or a bare\n * one. Every caller still keeps its own hard-coded copy behind this, as the\n * last resort for a render that produced nothing.\n *\n * A single-recipient convenience over {@link loadSystemEmail} +\n * {@link renderLoadedSystemEmail}; a batch should call those two directly so\n * it reads the template once.\n */\nexport async function renderSystemEmail(\n templateKey: string,\n merge: Record<string, string> = {},\n options: SystemEmailBrandOptions = {},\n): Promise<RenderedSystemEmail | null> {\n const loaded = await loadSystemEmail(templateKey)\n return loaded ? renderLoadedSystemEmail(loaded, merge, options) : null\n}\n\n/**\n * The email a template sends right now, for the staff test-send endpoint\n * (AGL-766): exactly what a real send of it renders — the published design,\n * or the built-in copy, with the same header and footer — so a test shows\n * what recipients receive. `null` for an unknown or non-Resend key.\n */\nexport async function renderEffectiveSystemEmail(\n templateKey: string,\n merge: Record<string, string> = {},\n options: SystemEmailBrandOptions = {},\n): Promise<RenderedSystemEmail | null> {\n return renderSystemEmail(templateKey, merge, options)\n}\n\n/**\n * The brand one system email is sent under (AGL-3367): the merge tokens, the\n * chrome options and the sender name, from one place.\n *\n * `null` is the platform's own brand, and it is the answer for everything\n * the platform says on its own behalf: a staff alert, a legal receipt, an\n * enforcement notice. A workspace-context email passes the workspace's org\n * document, and `resolveBrandingProfile` decides from there: a white-label\n * org gets its own name, logo, home and support in the header and footer,\n * and every other org resolves to the platform profile, so the same call is\n * right for both.\n */\nexport interface SystemEmailBrand {\n merge: Record<string, string>\n options: SystemEmailBrandOptions\n /** The display name to send under; absent sends under the platform's. */\n fromName?: string\n /** The org's own name, for the `{{org.name}}` its copy may use. */\n orgName?: string\n}\n\n/** See {@link SystemEmailBrand}. */\nexport function systemEmailBrand(\n org: Readonly<Record<string, unknown>> | null | undefined,\n): SystemEmailBrand {\n if (!org) return { merge: {}, options: {} }\n const branding = resolveBrandingProfile(org as never)\n const orgName = typeof org['name'] === 'string' ? org['name'].trim() : ''\n return {\n ...(orgName ? { orgName } : {}),\n merge: brandMergeTokens(branding),\n options: {\n brandLogoUrl: branding.emailLogoUrl,\n brandHomeUrl: branding.homeUrl,\n brandPrimaryColor: branding.primaryColor,\n },\n fromName: branding.fromName,\n }\n}\n\n/**\n * {@link systemEmailBrand} for an org read by id, for a sender that holds\n * only the id. A failed or missing read is the platform's brand: the email\n * still goes, and a white-label org's people see the platform's chrome on\n * one message rather than no message.\n */\nexport async function orgSystemEmailBrand(\n orgId: string | null | undefined,\n): Promise<SystemEmailBrand> {\n if (!orgId) return systemEmailBrand(null)\n try {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .get()\n return systemEmailBrand(snapshot.exists ? (snapshot.data() ?? null) : null)\n } catch (error) {\n console.error(`system email brand for org ${orgId} failed to load`, error)\n return systemEmailBrand(null)\n }\n}\n\n/** What a send takes from a rendered system email. */\nexport interface SystemEmailContent {\n subject: string\n text: string\n html?: string\n}\n\n/**\n * The subject, text and HTML to send, with the caller's own copy as the\n * last resort for a render that produced nothing: the shape every sender of\n * a system email repeats, written once.\n */\nexport function systemEmailContent(\n rendered: RenderedSystemEmail | null,\n fallback: { subject: string; text: string },\n): SystemEmailContent {\n return {\n subject: rendered?.subject || fallback.subject,\n text: rendered?.text || fallback.text,\n ...(rendered?.html ? { html: rendered.html } : {}),\n }\n}\n\n/**\n * Renders a key for one recipient in a brand and returns what to send\n * (AGL-3367): {@link renderSystemEmail} and {@link systemEmailContent} in\n * one call, for the senders with one message to build. Never throws; a\n * render that fails sends the fallback copy, which is what it sent before.\n */\nexport async function renderSystemEmailContent(\n templateKey: string,\n merge: Record<string, string>,\n brand: SystemEmailBrand,\n fallback: { subject: string; text: string },\n): Promise<SystemEmailContent> {\n const rendered = await renderSystemEmail(\n templateKey,\n { ...brand.merge, ...merge },\n brand.options,\n ).catch((error) => {\n console.error(`system email ${templateKey} failed to render`, error)\n return null\n })\n return systemEmailContent(rendered, fallback)\n}\n\nexport default renderSystemEmail\n"],"names":["process","EMAIL_NODE_ROOT_ID","PLATFORM_EMAIL_PALETTE","chromeForDesign","SYSTEM_EMAIL_COLLECTION","buildEmailPalette","buildDefaultEmailNodeMap","getSystemEmailTemplate","renderEmailHtml","substituteMergeTokens","firebaseAdmin","platformMarketingHostId","brandMergeTokens","collectReferencedComponentIds","composeReusableComponentNodes","decodeStoredNodes","resolveBrandingProfile","sanitizeAuthorHtml","hostComponentReader","loadReferencedComponents","PLATFORM_BRAND_MERGE_TOKENS","buildSystemEmailChrome","isPlatformBrandedSend","blankUnresolvedTokens","value","replace","CONSOLE_ORIGIN","env","NEXT_PUBLIC_CONSOLE_URL","loadPlatformEmailBlocks","firestore","templateKey","nodes","size","undefined","hostId","onSkipped","skipped","console","warn","JSON","stringify","tag","error","drawsPlatformBlocks","loaded","merged","options","Boolean","components","Object","keys","length","catalogDefault","definition","subjectTemplate","defaultSubject","preheaderTemplate","source","loadSystemEmail","deliveredBy","templateSnapshot","app","templateRef","collection","doc","get","exists","versionId","versionSnapshot","String","systemEmailTheme","primaryColor","brandPrimaryColor","trim","palette","base","buttonRadius","renderLoadedSystemEmail","merge","rendered","blocks","chrome","stored","composed","brandLogoUrl","brandHomeUrl","rootId","subject","preheader","sanitize","mediaOrigin","html","text","renderSystemEmail","renderEffectiveSystemEmail","systemEmailBrand","org","branding","orgName","emailLogoUrl","homeUrl","fromName","orgSystemEmailBrand","orgId","snapshot","data","systemEmailContent","fallback","renderSystemEmailContent","brand","catch"],"mappings":"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"}