@aglyn/aglyn 1.0.0-beta.202 → 1.0.0-beta.204

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 (38) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/console-routes.d.ts +4 -0
  3. package/src/lib/app-utils/console-routes.js +4 -0
  4. package/src/lib/app-utils/console-routes.js.map +1 -1
  5. package/src/lib/app-utils/csv-import.d.ts +12 -0
  6. package/src/lib/app-utils/csv-import.js +13 -3
  7. package/src/lib/app-utils/csv-import.js.map +1 -1
  8. package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
  9. package/src/lib/app-utils/docs-help.generated.js +1 -0
  10. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  11. package/src/lib/app-utils/docs-index.generated.js +76 -6
  12. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  13. package/src/lib/app-utils/health-report.js +10 -0
  14. package/src/lib/app-utils/health-report.js.map +1 -1
  15. package/src/lib/app-utils/marketplace-listing-visibility.d.ts +1 -0
  16. package/src/lib/app-utils/marketplace-listing-visibility.js +6 -0
  17. package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
  18. package/src/lib/app-utils/notifications.d.ts +1 -1
  19. package/src/lib/app-utils/notifications.js +4 -1
  20. package/src/lib/app-utils/notifications.js.map +1 -1
  21. package/src/lib/app-utils/plan-entitlements.js +23 -0
  22. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  23. package/src/lib/app-utils/recipient-email-render.d.ts +21 -1
  24. package/src/lib/app-utils/recipient-email-render.js +34 -5
  25. package/src/lib/app-utils/recipient-email-render.js.map +1 -1
  26. package/src/lib/app-utils/server.d.ts +1 -1
  27. package/src/lib/app-utils/server.js +1 -1
  28. package/src/lib/app-utils/server.js.map +1 -1
  29. package/src/lib/app-utils/site-email-chrome.d.ts +52 -0
  30. package/src/lib/app-utils/site-email-chrome.js +82 -0
  31. package/src/lib/app-utils/site-email-chrome.js.map +1 -0
  32. package/src/lib/app-utils/system-email-chrome.d.ts +6 -0
  33. package/src/lib/app-utils/system-email-chrome.js.map +1 -1
  34. package/src/lib/plugin-manager/plugin-org-lockdown.d.ts +77 -0
  35. package/src/lib/plugin-manager/plugin-org-lockdown.js +88 -0
  36. package/src/lib/plugin-manager/plugin-org-lockdown.js.map +1 -0
  37. package/src/lib/plugin-manager/plugin-record-email-state.d.ts +7 -0
  38. package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
@@ -41,7 +41,7 @@
41
41
  * are already-loaded data — a node map, resolved products, a recipient — and
42
42
  * the loading stays with the caller that has the credentials for it.
43
43
  */
44
- import { type EmailRenderProduct } from '@aglyn/shared-util-email/email-render';
44
+ import { type EmailChrome, type EmailRenderProduct, type EmailTheme } from '@aglyn/shared-util-email/email-render';
45
45
  import { type MergeTagRecipient } from '@aglyn/shared-util-email/email-merge';
46
46
  /**
47
47
  * A designed email, loaded. `nodes` is the DECODED besigner map: the stored
@@ -55,6 +55,12 @@ export interface DesignedEmailTemplate {
55
55
  preheader?: string;
56
56
  /** The template's own subject, used when the campaign names none. */
57
57
  subject?: string;
58
+ /**
59
+ * The design as saved, its reusable blocks still placements, when `nodes`
60
+ * has them grafted in: an unmarked placed block keeps the site's chrome off
61
+ * the design (AGL-3372). Absent reads `nodes` itself.
62
+ */
63
+ storedNodes?: Record<string, unknown>;
58
64
  }
59
65
  /**
60
66
  * WHERE THE MESSAGE COMES FROM — one source, named.
@@ -133,6 +139,20 @@ export interface RecipientEmailRenderInput {
133
139
  hostId?: string;
134
140
  /** The recipient's signed opt-out link, appended to the text part. */
135
141
  unsubscribeUrl?: string;
142
+ /**
143
+ * The site's header, footer and theme (AGL-3370). The header and footer
144
+ * are drawn around a typed message, and around a design less any band the
145
+ * design draws itself (AGL-3372). The theme applies to both: a design's
146
+ * picked colors are palette tokens only the site's theme resolves. Absent
147
+ * sends the typed message in the unbranded card and drops a design's
148
+ * tokens, as the composer's own checks do.
149
+ */
150
+ frame?: {
151
+ chrome: EmailChrome;
152
+ theme?: EmailTheme;
153
+ mediaOrigin?: string;
154
+ mediaHostId?: string;
155
+ };
136
156
  }
137
157
  export interface RenderedRecipientEmail {
138
158
  /** The subject with this recipient's merge values resolved. */
@@ -45,7 +45,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
45
45
  import { EMAIL_NODE_ROOT_ID, renderEmailHtml } from "@aglyn/shared-util-email/email-render";
46
46
  import { resolveMergeTags } from "@aglyn/shared-util-email/email-merge";
47
47
  import { appendUnsubscribeHtml, UNSUBSCRIBE_FOOTER_LABEL } from "@aglyn/shared-util-email/marketing-send";
48
- import { renderTextEmailHtml } from "@aglyn/shared-util-email/text-email-html";
48
+ import { renderFramedTextEmail, renderTextEmailHtml } from "@aglyn/shared-util-email/text-email-html";
49
+ import { chromeForDesign } from "@aglyn/shared-util-email/email-design-chrome";
49
50
  /*
50
51
  * The LEAF app-util, not `@aglyn/aglyn/server`: this module is pure and is
51
52
  * imported by client components, so a server entry point here would pull the
@@ -88,7 +89,7 @@ export function emailPlainTextState(record, /** The design's version as it stand
88
89
  * subject, so a plain-text campaign is byte-identical either way.
89
90
  */ export function renderRecipientEmail(input) {
90
91
  var _recipient_name, _name_split_, _content_template_products;
91
- var _input_preheader, _content_plainText;
92
+ var _input_preheader, _input_frame, _content_plainText;
92
93
  const { content, recipient, siteBase = '', hostId, unsubscribeUrl } = input;
93
94
  const template = content.mode === 'design' ? content.template : null;
94
95
  const subject = resolveMergeTags(input.subject || (template == null ? void 0 : template.subject) || '', recipient);
@@ -100,6 +101,22 @@ export function emailPlainTextState(record, /** The design's version as it stand
100
101
  * write into their own footers from the same constant.
101
102
  */ const unsubscribeLine = unsubscribeUrl ? `\n\n—\n${UNSUBSCRIBE_FOOTER_LABEL}: ${unsubscribeUrl}` : '';
102
103
  if (content.mode === 'text') {
104
+ if (input.frame) {
105
+ const messageText = resolveMergeTags(content.body, recipient);
106
+ // The same message in the site's header and footer; the opt-out still
107
+ // follows everything, as it does on a design.
108
+ const framed = renderFramedTextEmail(_extends({
109
+ text: messageText,
110
+ subject,
111
+ preheader
112
+ }, input.frame));
113
+ return {
114
+ subject,
115
+ html: unsubscribeUrl ? appendUnsubscribeHtml(framed.html, unsubscribeUrl) : framed.html,
116
+ text: `${framed.text}${unsubscribeLine}`,
117
+ messageText
118
+ };
119
+ }
103
120
  const messageText = resolveMergeTags(content.body, recipient);
104
121
  const text = `${messageText}${unsubscribeLine}`;
105
122
  return {
@@ -128,7 +145,7 @@ export function emailPlainTextState(record, /** The design's version as it stand
128
145
  messageText
129
146
  };
130
147
  }
131
- const rendered = renderEmailHtml({
148
+ const rendered = renderEmailHtml(_extends({
132
149
  nodes: content.template.nodes,
133
150
  // Besigner maps are rooted at `_@_`, not the renderer's default `root`:
134
151
  // rendering one as `root` finds no root and emits an empty 600px shell.
@@ -140,7 +157,19 @@ export function emailPlainTextState(record, /** The design's version as it stand
140
157
  // site-relative; an inbox has no page to resolve it against, so without an
141
158
  // origin the renderer drops it.
142
159
  mediaOrigin: siteBase || undefined,
143
- mediaHostId: hostId,
160
+ mediaHostId: hostId
161
+ }, (_input_frame = input.frame) == null ? void 0 : _input_frame.theme, (()=>{
162
+ var _content_template_storedNodes;
163
+ var _input_frame;
164
+ const chrome = chromeForDesign({
165
+ stored: (_content_template_storedNodes = content.template.storedNodes) != null ? _content_template_storedNodes : content.template.nodes,
166
+ composed: content.template.nodes,
167
+ chrome: (_input_frame = input.frame) == null ? void 0 : _input_frame.chrome
168
+ });
169
+ return chrome ? {
170
+ chrome
171
+ } : {};
172
+ })(), {
144
173
  merge: {
145
174
  'contact.email': recipient.email,
146
175
  'contact.name': name,
@@ -157,7 +186,7 @@ export function emailPlainTextState(record, /** The design's version as it stand
157
186
  })
158
187
  ];
159
188
  }))
160
- });
189
+ }));
161
190
  /*
162
191
  * THE PLAIN-TEXT PART: the design's own, unless somebody wrote one.
163
192
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/recipient-email-render.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 * ONE MESSAGE FOR ONE RECIPIENT, RENDERED — the step between a composed email\n * and the two parts that reach an inbox.\n *\n * It sits on the platform's mail rail, in the core, because more than one\n * plugin needs the SAME rendering: the plugin that sends a campaign renders\n * each recipient's copy through it, and the plugin that designs an email\n * checks a design through it before anything is sent. Neither may import the\n * other, and a second renderer in either would be a preview of something else.\n * It composes `@aglyn/shared-util-email`'s primitives with the core's HTML\n * sanitizer, which is why it cannot live in `shared`. Imported by its own\n * subpath; it is not in the barrel.\n *\n * It exists as a shared function because a preview that renders the message a\n * second way is a preview of something else. The class of defect it is meant\n * to catch has bitten twice on this send path — a designed campaign mailed\n * with its product blocks missing, and merge tags that resolved to empty\n * strings for an entire member audience — and neither would have been visible\n * in a preview that did not go through the same renderer, the same merge-tag\n * resolution and the same plain-text synthesis as the send.\n *\n * PURE, and free of `firebase-admin`, so the same call answers in three\n * places: the per-recipient loop in `campaign-send.ts`, the composer's\n * `renderPreview` action, and the console's own client components. Its inputs\n * are already-loaded data — a node map, resolved products, a recipient — and\n * the loading stays with the caller that has the credentials for it.\n */\n\n// By leaf, never the library's index: the index reaches the sender and the\n// health check, which this module has no use for.\nimport {\n EMAIL_NODE_ROOT_ID,\n renderEmailHtml,\n type EmailRenderProduct,\n} from '@aglyn/shared-util-email/email-render'\nimport {\n resolveMergeTags,\n type MergeTagRecipient,\n} from '@aglyn/shared-util-email/email-merge'\nimport {\n appendUnsubscribeHtml,\n UNSUBSCRIBE_FOOTER_LABEL,\n} from '@aglyn/shared-util-email/marketing-send'\nimport { renderTextEmailHtml } from '@aglyn/shared-util-email/text-email-html'\n/*\n * The LEAF app-util, not `@aglyn/aglyn/server`: this module is pure and is\n * imported by client components, so a server entry point here would pull the\n * Admin SDK into a browser bundle. The email blocks the besigner previews\n * these same nodes under call this same function — so the mailed copy and the\n * previewed copy are one policy over one string rather than two kept in step\n * by hand.\n */\nimport { sanitizeAuthorHtml } from './author-html';\n\n/**\n * A designed email, loaded. `nodes` is the DECODED besigner map: the stored\n * form is compressed msgpack from the first designer save onward, and a\n * caller that passes the raw value renders an empty shell.\n */\nexport interface DesignedEmailTemplate {\n nodes: Record<string, unknown>\n products?: Record<string, EmailRenderProduct | undefined>\n /** The template's own preview line, used when the campaign names none. */\n preheader?: string\n /** The template's own subject, used when the campaign names none. */\n subject?: string\n}\n\n/**\n * WHERE THE MESSAGE COMES FROM — one source, named.\n *\n * A union rather than two optional fields, and that is the whole point. The\n * design already supplies BOTH parts an inbox receives: `renderEmailHtml`\n * returns the HTML and a plain-text rendering of the same nodes, links\n * included. So on a designed send a typed BODY — the whole message of a\n * plain-text email — has no part to occupy, and the earlier shape, which\n * accepted both and read the body only when no template was given, computed it\n * and threw it away without saying so.\n *\n * What an author actually wants there is named for its job instead:\n * `plainText`, the text HALF of a designed message, generated from the design\n * by default. That is a different offer from reinterpreting a body, and\n * deliberately — see the field.\n *\n * Two members and not three: a saved TEMPLATE is a besigner email screen, the\n * same document the design picker offers, so \"designed\" and \"from a template\"\n * name one thing.\n *\n * With the two collapsed into a union the discard is not merely refused, it is\n * unsayable: the designed branch has no `body` in scope to drop, and a caller\n * holding both has to decide which it means before it can call at all.\n */\nexport type RecipientEmailContent =\n | {\n /** Written in the composer; the HTML part is synthesized from it. */\n mode: 'text'\n /** The plain-text body, after any A/B variant override. */\n body: string\n }\n | {\n /** Built in the besigner; the HTML part comes from the nodes. */\n mode: 'design'\n template: DesignedEmailTemplate\n /**\n * The author's OWN plain-text part, replacing the one the design\n * generates. Absent — not empty — means generated.\n *\n * Presence is the whole signal, which is why this is a separate\n * optional field rather than a flag beside a string: a flag and a value\n * are two things that can disagree, and the disagreement is invisible.\n *\n * Worth having because the generated text is good but not perfect. A\n * button and a product keep their URLs in it; an inline link inside an\n * `emailRichtext` block does not — the synthesis strips tags, so the\n * href goes with them. An author who cares about what a text-only\n * reader can actually follow needs a way to say so.\n */\n plainText?: string\n }\n\n/** Which of the two ways one email is written. */\nexport type EmailMessageMode = RecipientEmailContent['mode']\n\n/**\n * Which mode a stored email is in, from the one field that decides it.\n *\n * Naming a template IS being designed — there is no separate flag, and adding\n * one would be a second answer to drift from this one. Read by the composer,\n * by the surfaces that describe a message and by the send path, so a record\n * cannot be shown as one thing and mailed as another.\n */\nexport function emailMessageMode(record: {\n templateScreenId?: string | null\n}): EmailMessageMode {\n return record.templateScreenId ? 'design' : 'text'\n}\n\nexport interface RecipientEmailRenderInput {\n /** The campaign's subject, after any A/B variant override. */\n subject: string\n /** The one source this message is written from. */\n content: RecipientEmailContent\n /** The composer's preview line; overrides the template's own. */\n preheader?: string\n /** Who this copy is being personalized for. */\n recipient: MergeTagRecipient\n /** The site's own public origin, absolutizing media and product links. */\n siteBase?: string\n /** Qualifies an org-scoped media asset for the unauthenticated CDN. */\n hostId?: string\n /** The recipient's signed opt-out link, appended to the text part. */\n unsubscribeUrl?: string\n}\n\nexport interface RenderedRecipientEmail {\n /** The subject with this recipient's merge values resolved. */\n subject: string\n /** The HTML part, which every campaign message carries. */\n html: string\n /** The plain-text part, ending in the opt-out line. */\n text: string\n /**\n * The message's own plain text, WITHOUT the opt-out line.\n *\n * What the composer fills an authored plain-text part from. Prefilling from\n * `text` would fold this send's footer into the stored override, and the\n * next send would append a second one — so the two are reported apart\n * rather than the caller trying to trim one off the other.\n */\n messageText: string\n}\n\n/**\n * WHERE A DESIGNED EMAIL'S PLAIN-TEXT PART COMES FROM, and whether it still\n * describes the design.\n *\n * Two facts a composer has to state rather than imply. An authored text part\n * is not overwritten when the design is edited — losing somebody's writing\n * because they touched the canvas afterwards is not a trade worth making —\n * which leaves the other half of the problem: an override written against a\n * design that has since changed says something the HTML no longer says, and\n * nothing about it is visible. A value that cannot be read scores the same as\n * one that is absent, which is the shape of the defect this whole area is\n * being fixed for.\n *\n * So the design's VERSION is recorded beside the override, and the two are\n * compared here. Pure, and read by the composer, so the notice a merchant sees\n * and the state the record is in cannot disagree.\n */\nexport interface EmailPlainTextState {\n /** `authored` when somebody wrote it; `generated` from the design. */\n source: 'generated' | 'authored'\n /**\n * An authored part written against a design that has moved since.\n *\n * False for a generated part, which cannot go stale — it is derived from\n * whatever the design says at send time — and false when either version is\n * unknown, because an unanswerable question must not render as a warning.\n */\n stale: boolean\n}\n\nexport function emailPlainTextState(\n record: {\n plainText?: string | null\n /** The design version the override was written against. */\n plainTextVersionId?: string | null\n },\n /** The design's version as it stands now. */\n currentVersionId?: string | null,\n): EmailPlainTextState {\n if (!record.plainText?.trim()) return { source: 'generated', stale: false }\n const written = record.plainTextVersionId ?? ''\n return {\n source: 'authored',\n stale: Boolean(written && currentVersionId && written !== currentVersionId),\n }\n}\n\n/**\n * Renders one campaign message for one recipient.\n *\n * The HTML part is produced here rather than left to `sendEmail`'s fallback\n * so that a caller can SEE it. `sendEmail` synthesizes an HTML part from the\n * text of any message that arrives without one — a text-only message has no\n * anchors, so its links are inert and click tracking has nothing to rewrite —\n * and this calls the same `renderTextEmailHtml` with the same text and\n * subject, so a plain-text campaign is byte-identical either way.\n */\nexport function renderRecipientEmail(\n input: RecipientEmailRenderInput,\n): RenderedRecipientEmail {\n const { content, recipient, siteBase = '', hostId, unsubscribeUrl } = input\n const template = content.mode === 'design' ? content.template : null\n const subject = resolveMergeTags(\n input.subject || template?.subject || '',\n recipient,\n )\n const preheader = input.preheader?.trim() || template?.preheader || ''\n const name = (recipient.name ?? '').trim()\n /*\n * The plain-text footer, named for what the link actually opens — see\n * {@link UNSUBSCRIBE_FOOTER_LABEL}, which the non-campaign marketing paths\n * write into their own footers from the same constant.\n */\n const unsubscribeLine = unsubscribeUrl\n ? `\\n\\n—\\n${UNSUBSCRIBE_FOOTER_LABEL}: ${unsubscribeUrl}`\n : ''\n\n if (content.mode === 'text') {\n const messageText = resolveMergeTags(content.body, recipient)\n const text = `${messageText}${unsubscribeLine}`\n return {\n subject,\n /*\n * THE HTML FOOTER IS RENDERED, NOT LINKIFIED OUT OF THE TEXT.\n *\n * Synthesizing the HTML from `text` — footer included — used to work,\n * and produced a footer whose visible words were the raw signed URL:\n * `renderTextEmailHtml` turns a bare URL into an anchor labelled with\n * itself, which is right for a link the AUTHOR typed and wrong for the\n * one line this renderer writes. A hundred characters of `hostId`,\n * `sig` and `cid` is not a thing a person reads, and it buries the only\n * sentence in the message that tells them they can leave one stream\n * instead of all of them.\n *\n * So the message is linkified WITHOUT the footer, and the footer is\n * appended by the same helper the designed branch uses — same styling,\n * same words, tracking-rewritten by the provider exactly as before,\n * because rewriting keys on the `href` and not on the label.\n *\n * The TEXT part keeps the bare URL. Markup is invisible there and an\n * address a reader can copy is the only form that works.\n */\n html: unsubscribeUrl\n ? appendUnsubscribeHtml(\n renderTextEmailHtml(messageText, subject, preheader),\n unsubscribeUrl,\n )\n : renderTextEmailHtml(text, subject, preheader),\n text,\n messageText,\n }\n }\n\n const rendered = renderEmailHtml({\n nodes: content.template.nodes as never,\n // Besigner maps are rooted at `_@_`, not the renderer's default `root`:\n // rendering one as `root` finds no root and emits an empty 600px shell.\n rootId: EMAIL_NODE_ROOT_ID,\n subject,\n preheader,\n sanitize: sanitizeAuthorHtml,\n // An author-picked image is stored as a `media:` reference and resolves\n // site-relative; an inbox has no page to resolve it against, so without an\n // origin the renderer drops it.\n mediaOrigin: siteBase || undefined,\n mediaHostId: hostId,\n merge: {\n 'contact.email': recipient.email,\n 'contact.name': name,\n 'contact.firstName': name.split(/\\s+/)[0] ?? '',\n 'site.url': siteBase,\n unsubscribeUrl: unsubscribeUrl ?? '',\n },\n products: Object.fromEntries(\n Object.entries(content.template.products ?? {}).map(([id, product]) => [\n id,\n product && {\n ...product,\n url: product.url?.startsWith('/')\n ? `${siteBase}${product.url}`\n : product.url,\n },\n ]),\n ),\n })\n /*\n * THE PLAIN-TEXT PART: the design's own, unless somebody wrote one.\n *\n * Merge tags are resolved in an authored part exactly as they are in a\n * plain-text campaign's body — a text part that ships `{{firstName|there}}`\n * literally is worse than the generated one it replaced. The design's own\n * text arrives already substituted, through `renderEmailHtml`'s `merge` map.\n *\n * The opt-out line is appended either way, and it is a BARE URL rather than\n * an anchor: markup is invisible in a text part, so a link a text-only\n * reader can copy is the only form that works. It is the one part of the\n * footer an author cannot write away.\n */\n const authored = content.plainText?.trim()\n const messageText = authored\n ? resolveMergeTags(authored, recipient)\n : rendered.text\n return {\n subject,\n /*\n * THE HTML PART GETS THE SAME FOOTER THE TEXT PART GETS.\n *\n * A designed template carries an opt-out only if its author placed a\n * `{{unsubscribeUrl}}` somewhere, and nothing made them: a template whose\n * footer block says only the copyright line renders an HTML part with no\n * way out of the list at all, which is the half of the message almost\n * every recipient reads. The text part had the link and the HTML did not,\n * so the defect was invisible to anyone reading their own test send.\n *\n * `appendUnsubscribeHtml` is idempotent by URL, so an author who DID\n * place the merge token keeps their own placement and gets no second\n * footer — the same rule `sendEmail` applies to every other marketing\n * path, applied here because the campaign sender passes no `marketing`\n * context and so never reaches that append.\n */\n html: unsubscribeUrl\n ? appendUnsubscribeHtml(rendered.html, unsubscribeUrl)\n : rendered.html,\n text: `${messageText}${unsubscribeLine}`,\n messageText,\n }\n}\n\n/**\n * The merge tags the send path resolves, for the composer to offer.\n *\n * Kept beside the renderer because `resolveMergeTags` recognizes exactly\n * these three and substitutes the fallback for anything else: a tag a\n * merchant types from memory that is not on this list does not fail, it\n * silently renders as nothing in mail that has already gone out. A composer\n * that lists them is the difference between a documented feature and a\n * guess.\n */\nexport const EMAIL_MERGE_TAGS: ReadonlyArray<{\n /** The full token, fallback included, as it should be typed. */\n token: string\n /** What it resolves to, per recipient. */\n description: string\n}> = [\n {\n token: '{{firstName|there}}',\n description: 'The recipient’s first name, or “there” when none is stored',\n },\n {\n token: '{{name}}',\n description: 'The recipient’s full name, where the audience stores one',\n },\n { token: '{{email}}', description: 'The address this copy is going to' },\n]\n"],"names":["EMAIL_NODE_ROOT_ID","renderEmailHtml","resolveMergeTags","appendUnsubscribeHtml","UNSUBSCRIBE_FOOTER_LABEL","renderTextEmailHtml","sanitizeAuthorHtml","emailMessageMode","record","templateScreenId","emailPlainTextState","currentVersionId","plainText","trim","source","stale","written","plainTextVersionId","Boolean","renderRecipientEmail","input","recipient","name","content","siteBase","hostId","unsubscribeUrl","template","mode","subject","preheader","unsubscribeLine","messageText","body","text","html","rendered","nodes","rootId","sanitize","mediaOrigin","undefined","mediaHostId","merge","email","split","products","Object","fromEntries","entries","map","id","product","url","startsWith","authored","EMAIL_MERGE_TAGS","token","description"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/recipient-email-render.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 * ONE MESSAGE FOR ONE RECIPIENT, RENDERED — the step between a composed email\n * and the two parts that reach an inbox.\n *\n * It sits on the platform's mail rail, in the core, because more than one\n * plugin needs the SAME rendering: the plugin that sends a campaign renders\n * each recipient's copy through it, and the plugin that designs an email\n * checks a design through it before anything is sent. Neither may import the\n * other, and a second renderer in either would be a preview of something else.\n * It composes `@aglyn/shared-util-email`'s primitives with the core's HTML\n * sanitizer, which is why it cannot live in `shared`. Imported by its own\n * subpath; it is not in the barrel.\n *\n * It exists as a shared function because a preview that renders the message a\n * second way is a preview of something else. The class of defect it is meant\n * to catch has bitten twice on this send path — a designed campaign mailed\n * with its product blocks missing, and merge tags that resolved to empty\n * strings for an entire member audience — and neither would have been visible\n * in a preview that did not go through the same renderer, the same merge-tag\n * resolution and the same plain-text synthesis as the send.\n *\n * PURE, and free of `firebase-admin`, so the same call answers in three\n * places: the per-recipient loop in `campaign-send.ts`, the composer's\n * `renderPreview` action, and the console's own client components. Its inputs\n * are already-loaded data — a node map, resolved products, a recipient — and\n * the loading stays with the caller that has the credentials for it.\n */\n\n// By leaf, never the library's index: the index reaches the sender and the\n// health check, which this module has no use for.\nimport {\n EMAIL_NODE_ROOT_ID,\n renderEmailHtml,\n type EmailChrome,\n type EmailRenderProduct,\n type EmailTheme,\n} from '@aglyn/shared-util-email/email-render'\nimport {\n resolveMergeTags,\n type MergeTagRecipient,\n} from '@aglyn/shared-util-email/email-merge'\nimport {\n appendUnsubscribeHtml,\n UNSUBSCRIBE_FOOTER_LABEL,\n} from '@aglyn/shared-util-email/marketing-send'\nimport {\n renderFramedTextEmail,\n renderTextEmailHtml,\n} from '@aglyn/shared-util-email/text-email-html'\nimport { chromeForDesign } from '@aglyn/shared-util-email/email-design-chrome'\n/*\n * The LEAF app-util, not `@aglyn/aglyn/server`: this module is pure and is\n * imported by client components, so a server entry point here would pull the\n * Admin SDK into a browser bundle. The email blocks the besigner previews\n * these same nodes under call this same function — so the mailed copy and the\n * previewed copy are one policy over one string rather than two kept in step\n * by hand.\n */\nimport { sanitizeAuthorHtml } from './author-html';\n\n/**\n * A designed email, loaded. `nodes` is the DECODED besigner map: the stored\n * form is compressed msgpack from the first designer save onward, and a\n * caller that passes the raw value renders an empty shell.\n */\nexport interface DesignedEmailTemplate {\n nodes: Record<string, unknown>\n products?: Record<string, EmailRenderProduct | undefined>\n /** The template's own preview line, used when the campaign names none. */\n preheader?: string\n /** The template's own subject, used when the campaign names none. */\n subject?: string\n /**\n * The design as saved, its reusable blocks still placements, when `nodes`\n * has them grafted in: an unmarked placed block keeps the site's chrome off\n * the design (AGL-3372). Absent reads `nodes` itself.\n */\n storedNodes?: Record<string, unknown>\n}\n\n/**\n * WHERE THE MESSAGE COMES FROM — one source, named.\n *\n * A union rather than two optional fields, and that is the whole point. The\n * design already supplies BOTH parts an inbox receives: `renderEmailHtml`\n * returns the HTML and a plain-text rendering of the same nodes, links\n * included. So on a designed send a typed BODY — the whole message of a\n * plain-text email — has no part to occupy, and the earlier shape, which\n * accepted both and read the body only when no template was given, computed it\n * and threw it away without saying so.\n *\n * What an author actually wants there is named for its job instead:\n * `plainText`, the text HALF of a designed message, generated from the design\n * by default. That is a different offer from reinterpreting a body, and\n * deliberately — see the field.\n *\n * Two members and not three: a saved TEMPLATE is a besigner email screen, the\n * same document the design picker offers, so \"designed\" and \"from a template\"\n * name one thing.\n *\n * With the two collapsed into a union the discard is not merely refused, it is\n * unsayable: the designed branch has no `body` in scope to drop, and a caller\n * holding both has to decide which it means before it can call at all.\n */\nexport type RecipientEmailContent =\n | {\n /** Written in the composer; the HTML part is synthesized from it. */\n mode: 'text'\n /** The plain-text body, after any A/B variant override. */\n body: string\n }\n | {\n /** Built in the besigner; the HTML part comes from the nodes. */\n mode: 'design'\n template: DesignedEmailTemplate\n /**\n * The author's OWN plain-text part, replacing the one the design\n * generates. Absent — not empty — means generated.\n *\n * Presence is the whole signal, which is why this is a separate\n * optional field rather than a flag beside a string: a flag and a value\n * are two things that can disagree, and the disagreement is invisible.\n *\n * Worth having because the generated text is good but not perfect. A\n * button and a product keep their URLs in it; an inline link inside an\n * `emailRichtext` block does not — the synthesis strips tags, so the\n * href goes with them. An author who cares about what a text-only\n * reader can actually follow needs a way to say so.\n */\n plainText?: string\n }\n\n/** Which of the two ways one email is written. */\nexport type EmailMessageMode = RecipientEmailContent['mode']\n\n/**\n * Which mode a stored email is in, from the one field that decides it.\n *\n * Naming a template IS being designed — there is no separate flag, and adding\n * one would be a second answer to drift from this one. Read by the composer,\n * by the surfaces that describe a message and by the send path, so a record\n * cannot be shown as one thing and mailed as another.\n */\nexport function emailMessageMode(record: {\n templateScreenId?: string | null\n}): EmailMessageMode {\n return record.templateScreenId ? 'design' : 'text'\n}\n\nexport interface RecipientEmailRenderInput {\n /** The campaign's subject, after any A/B variant override. */\n subject: string\n /** The one source this message is written from. */\n content: RecipientEmailContent\n /** The composer's preview line; overrides the template's own. */\n preheader?: string\n /** Who this copy is being personalized for. */\n recipient: MergeTagRecipient\n /** The site's own public origin, absolutizing media and product links. */\n siteBase?: string\n /** Qualifies an org-scoped media asset for the unauthenticated CDN. */\n hostId?: string\n /** The recipient's signed opt-out link, appended to the text part. */\n unsubscribeUrl?: string\n /**\n * The site's header, footer and theme (AGL-3370). The header and footer\n * are drawn around a typed message, and around a design less any band the\n * design draws itself (AGL-3372). The theme applies to both: a design's\n * picked colors are palette tokens only the site's theme resolves. Absent\n * sends the typed message in the unbranded card and drops a design's\n * tokens, as the composer's own checks do.\n */\n frame?: {\n chrome: EmailChrome\n theme?: EmailTheme\n mediaOrigin?: string\n mediaHostId?: string\n }\n}\n\nexport interface RenderedRecipientEmail {\n /** The subject with this recipient's merge values resolved. */\n subject: string\n /** The HTML part, which every campaign message carries. */\n html: string\n /** The plain-text part, ending in the opt-out line. */\n text: string\n /**\n * The message's own plain text, WITHOUT the opt-out line.\n *\n * What the composer fills an authored plain-text part from. Prefilling from\n * `text` would fold this send's footer into the stored override, and the\n * next send would append a second one — so the two are reported apart\n * rather than the caller trying to trim one off the other.\n */\n messageText: string\n}\n\n/**\n * WHERE A DESIGNED EMAIL'S PLAIN-TEXT PART COMES FROM, and whether it still\n * describes the design.\n *\n * Two facts a composer has to state rather than imply. An authored text part\n * is not overwritten when the design is edited — losing somebody's writing\n * because they touched the canvas afterwards is not a trade worth making —\n * which leaves the other half of the problem: an override written against a\n * design that has since changed says something the HTML no longer says, and\n * nothing about it is visible. A value that cannot be read scores the same as\n * one that is absent, which is the shape of the defect this whole area is\n * being fixed for.\n *\n * So the design's VERSION is recorded beside the override, and the two are\n * compared here. Pure, and read by the composer, so the notice a merchant sees\n * and the state the record is in cannot disagree.\n */\nexport interface EmailPlainTextState {\n /** `authored` when somebody wrote it; `generated` from the design. */\n source: 'generated' | 'authored'\n /**\n * An authored part written against a design that has moved since.\n *\n * False for a generated part, which cannot go stale — it is derived from\n * whatever the design says at send time — and false when either version is\n * unknown, because an unanswerable question must not render as a warning.\n */\n stale: boolean\n}\n\nexport function emailPlainTextState(\n record: {\n plainText?: string | null\n /** The design version the override was written against. */\n plainTextVersionId?: string | null\n },\n /** The design's version as it stands now. */\n currentVersionId?: string | null,\n): EmailPlainTextState {\n if (!record.plainText?.trim()) return { source: 'generated', stale: false }\n const written = record.plainTextVersionId ?? ''\n return {\n source: 'authored',\n stale: Boolean(written && currentVersionId && written !== currentVersionId),\n }\n}\n\n/**\n * Renders one campaign message for one recipient.\n *\n * The HTML part is produced here rather than left to `sendEmail`'s fallback\n * so that a caller can SEE it. `sendEmail` synthesizes an HTML part from the\n * text of any message that arrives without one — a text-only message has no\n * anchors, so its links are inert and click tracking has nothing to rewrite —\n * and this calls the same `renderTextEmailHtml` with the same text and\n * subject, so a plain-text campaign is byte-identical either way.\n */\nexport function renderRecipientEmail(\n input: RecipientEmailRenderInput,\n): RenderedRecipientEmail {\n const { content, recipient, siteBase = '', hostId, unsubscribeUrl } = input\n const template = content.mode === 'design' ? content.template : null\n const subject = resolveMergeTags(\n input.subject || template?.subject || '',\n recipient,\n )\n const preheader = input.preheader?.trim() || template?.preheader || ''\n const name = (recipient.name ?? '').trim()\n /*\n * The plain-text footer, named for what the link actually opens — see\n * {@link UNSUBSCRIBE_FOOTER_LABEL}, which the non-campaign marketing paths\n * write into their own footers from the same constant.\n */\n const unsubscribeLine = unsubscribeUrl\n ? `\\n\\n—\\n${UNSUBSCRIBE_FOOTER_LABEL}: ${unsubscribeUrl}`\n : ''\n\n if (content.mode === 'text') {\n if (input.frame) {\n const messageText = resolveMergeTags(content.body, recipient)\n // The same message in the site's header and footer; the opt-out still\n // follows everything, as it does on a design.\n const framed = renderFramedTextEmail({\n text: messageText,\n subject,\n preheader,\n ...input.frame,\n })\n return {\n subject,\n html: unsubscribeUrl\n ? appendUnsubscribeHtml(framed.html, unsubscribeUrl)\n : framed.html,\n text: `${framed.text}${unsubscribeLine}`,\n messageText,\n }\n }\n const messageText = resolveMergeTags(content.body, recipient)\n const text = `${messageText}${unsubscribeLine}`\n return {\n subject,\n /*\n * THE HTML FOOTER IS RENDERED, NOT LINKIFIED OUT OF THE TEXT.\n *\n * Synthesizing the HTML from `text` — footer included — used to work,\n * and produced a footer whose visible words were the raw signed URL:\n * `renderTextEmailHtml` turns a bare URL into an anchor labelled with\n * itself, which is right for a link the AUTHOR typed and wrong for the\n * one line this renderer writes. A hundred characters of `hostId`,\n * `sig` and `cid` is not a thing a person reads, and it buries the only\n * sentence in the message that tells them they can leave one stream\n * instead of all of them.\n *\n * So the message is linkified WITHOUT the footer, and the footer is\n * appended by the same helper the designed branch uses — same styling,\n * same words, tracking-rewritten by the provider exactly as before,\n * because rewriting keys on the `href` and not on the label.\n *\n * The TEXT part keeps the bare URL. Markup is invisible there and an\n * address a reader can copy is the only form that works.\n */\n html: unsubscribeUrl\n ? appendUnsubscribeHtml(\n renderTextEmailHtml(messageText, subject, preheader),\n unsubscribeUrl,\n )\n : renderTextEmailHtml(text, subject, preheader),\n text,\n messageText,\n }\n }\n\n\n const rendered = renderEmailHtml({\n nodes: content.template.nodes as never,\n // Besigner maps are rooted at `_@_`, not the renderer's default `root`:\n // rendering one as `root` finds no root and emits an empty 600px shell.\n rootId: EMAIL_NODE_ROOT_ID,\n subject,\n preheader,\n sanitize: sanitizeAuthorHtml,\n // An author-picked image is stored as a `media:` reference and resolves\n // site-relative; an inbox has no page to resolve it against, so without an\n // origin the renderer drops it.\n mediaOrigin: siteBase || undefined,\n mediaHostId: hostId,\n // The site's theme resolves the colors the merchant picked (AGL-3370),\n // and its chrome frames the design, less the bands it draws (AGL-3372).\n ...input.frame?.theme,\n ...(() => {\n const chrome = chromeForDesign({\n stored: content.template.storedNodes ?? content.template.nodes,\n composed: content.template.nodes,\n chrome: input.frame?.chrome,\n })\n return chrome ? { chrome } : {}\n })(),\n merge: {\n 'contact.email': recipient.email,\n 'contact.name': name,\n 'contact.firstName': name.split(/\\s+/)[0] ?? '',\n 'site.url': siteBase,\n unsubscribeUrl: unsubscribeUrl ?? '',\n },\n products: Object.fromEntries(\n Object.entries(content.template.products ?? {}).map(([id, product]) => [\n id,\n product && {\n ...product,\n url: product.url?.startsWith('/')\n ? `${siteBase}${product.url}`\n : product.url,\n },\n ]),\n ),\n })\n /*\n * THE PLAIN-TEXT PART: the design's own, unless somebody wrote one.\n *\n * Merge tags are resolved in an authored part exactly as they are in a\n * plain-text campaign's body — a text part that ships `{{firstName|there}}`\n * literally is worse than the generated one it replaced. The design's own\n * text arrives already substituted, through `renderEmailHtml`'s `merge` map.\n *\n * The opt-out line is appended either way, and it is a BARE URL rather than\n * an anchor: markup is invisible in a text part, so a link a text-only\n * reader can copy is the only form that works. It is the one part of the\n * footer an author cannot write away.\n */\n const authored = content.plainText?.trim()\n const messageText = authored\n ? resolveMergeTags(authored, recipient)\n : rendered.text\n return {\n subject,\n /*\n * THE HTML PART GETS THE SAME FOOTER THE TEXT PART GETS.\n *\n * A designed template carries an opt-out only if its author placed a\n * `{{unsubscribeUrl}}` somewhere, and nothing made them: a template whose\n * footer block says only the copyright line renders an HTML part with no\n * way out of the list at all, which is the half of the message almost\n * every recipient reads. The text part had the link and the HTML did not,\n * so the defect was invisible to anyone reading their own test send.\n *\n * `appendUnsubscribeHtml` is idempotent by URL, so an author who DID\n * place the merge token keeps their own placement and gets no second\n * footer — the same rule `sendEmail` applies to every other marketing\n * path, applied here because the campaign sender passes no `marketing`\n * context and so never reaches that append.\n */\n html: unsubscribeUrl\n ? appendUnsubscribeHtml(rendered.html, unsubscribeUrl)\n : rendered.html,\n text: `${messageText}${unsubscribeLine}`,\n messageText,\n }\n}\n\n/**\n * The merge tags the send path resolves, for the composer to offer.\n *\n * Kept beside the renderer because `resolveMergeTags` recognizes exactly\n * these three and substitutes the fallback for anything else: a tag a\n * merchant types from memory that is not on this list does not fail, it\n * silently renders as nothing in mail that has already gone out. A composer\n * that lists them is the difference between a documented feature and a\n * guess.\n */\nexport const EMAIL_MERGE_TAGS: ReadonlyArray<{\n /** The full token, fallback included, as it should be typed. */\n token: string\n /** What it resolves to, per recipient. */\n description: string\n}> = [\n {\n token: '{{firstName|there}}',\n description: 'The recipient’s first name, or “there” when none is stored',\n },\n {\n token: '{{name}}',\n description: 'The recipient’s full name, where the audience stores one',\n },\n { token: '{{email}}', description: 'The address this copy is going to' },\n]\n"],"names":["EMAIL_NODE_ROOT_ID","renderEmailHtml","resolveMergeTags","appendUnsubscribeHtml","UNSUBSCRIBE_FOOTER_LABEL","renderFramedTextEmail","renderTextEmailHtml","chromeForDesign","sanitizeAuthorHtml","emailMessageMode","record","templateScreenId","emailPlainTextState","currentVersionId","plainText","trim","source","stale","written","plainTextVersionId","Boolean","renderRecipientEmail","input","recipient","name","content","siteBase","hostId","unsubscribeUrl","template","mode","subject","preheader","unsubscribeLine","frame","messageText","body","framed","text","html","rendered","nodes","rootId","sanitize","mediaOrigin","undefined","mediaHostId","theme","chrome","stored","storedNodes","composed","merge","email","split","products","Object","fromEntries","entries","map","id","product","url","startsWith","authored","EMAIL_MERGE_TAGS","token","description"],"mappings":"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"}
@@ -59,7 +59,7 @@ export * from './crm-task-reminders';
59
59
  export * from './crm-inbound';
60
60
  export * from './member-email-aliases';
61
61
  export * from './crm-next-activity';
62
- export { csvCell, csvDocument, customImportTarget, customImportTargetKey, } from './csv-import';
62
+ export { csvCell, csvDocument, customImportTarget, customImportTargetKey, importNoMailServerSentence, } from './csv-import';
63
63
  export type { ImportChunkResult, ImportDroppedValue, ImportSkippedRow, } from './csv-import';
64
64
  export * from './contact-custom-fields';
65
65
  export * from './consent-groups';
@@ -122,7 +122,7 @@ export * from "./crm-next-activity.js";
122
122
  // mapped column names for a custom field — named rather than starred, because
123
123
  // an importer built on this file exports the shared ceilings under its own
124
124
  // names.
125
- export { csvCell, csvDocument, customImportTarget, customImportTargetKey } from "./csv-import.js";
125
+ export { csvCell, csvDocument, customImportTarget, customImportTargetKey, importNoMailServerSentence } from "./csv-import.js";
126
126
  // What a VALUE under a custom contact field may be, and the coercion every
127
127
  // door applies to one (AGL-2601): a mapped form field, an API `custom` body,
128
128
  // and the carry-over that keeps a mapping across a publish. Pure like `crm`.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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 * Server-safe app-utils: every app-util EXCEPT the client-only React\n * contexts (see ./contexts). Nothing here calls `createContext` at module\n * scope, so this barrel — re-exported by `@aglyn/aglyn/server` — is safe to\n * import from tenant Server Components (AGL-405). The full `./index` barrel\n * re-adds the contexts for client consumers.\n */\nexport * from './binding-token-catalog'\nexport * from './binding-tokens'\nexport * from './breadcrumb-json-ld'\n// What a search listing holds and how long each field may be (AGL-2910):\n// the SEO editors and anything that proposes a value read one catalog.\nexport * from './seo-listing-fields'\n// `VideoObject` for the Video elements a page actually ships (AGL-2747).\n// Beside the other structured-data builders, and server-only like them: only\n// the route emits JSON-LD, and a crawler-facing block has no client caller.\nexport * from './video-object'\nexport * from './collection-delete'\nexport * from './collection-entries'\nexport * from './collection-kind'\nexport * from './collection-slug'\nexport * from './content-authors'\nexport * from './content-query-fields'\nexport * from './content-schema-type'\nexport * from './content-author-profile'\nexport * from './url-slug'\nexport * from './child-contract'\nexport * from './child-contract-compose'\n// The one reading of `restrictChildren`/`restrictParent`, shared by the\n// besigner's drop check and the AI plugin's node-tree validator (AGL-2905).\nexport * from './lineal-order'\n// What a node tree must satisfy before it lands on a canvas that did not\n// author it (AGL-2939): the marketplace's install sanitizer and the AI\n// plugin's node-tree validator both pass through it.\nexport * from './node-definition-sanitizer'\nexport * from './console-routes'\nexport * from './console-record-links'\n// What a stored screen-link value means, with no React attached — the\n// where-used scan reads these on the server (AGL-703).\nexport * from './screen-link-value'\n// Which entries a page's links name (AGL-3118). The tenant render reads\n// exactly those entry documents, so the walk runs on the server.\nexport * from './link-references'\nexport * from './analytics-path-key'\nexport * from './contacts'\n// A lead's or a contact's email state (AGL-3245): the last verdict on its\n// address, in words a person can act on, mirrored from the senders' lists.\nexport * from './email-state'\n// The records that sit beside a contact — companies, pipelines, deals, tasks,\n// activities, field definitions (AGL-2595). Pure like `contacts`, and beside\n// it because `ContactFacet` names its lifecycle stage and custom-value types;\n// the scope stamp every CRM creator writes lives here too.\nexport * from './crm'\n// Email templates, snippets and the merge-field resolver (AGL-2658): the\n// letter a rep keeps under a name, and the one grammar the send route and\n// the dialog fill it in by. Pure like `crm`, and beside it because a\n// template is scoped the way a task is.\nexport * from './crm-email-templates'\n// The booking door's shared vocabulary (AGL-2660): the query keys a booking\n// link carries, the record reference inside it, and the wording and due\n// date of what a booking files back on the record.\nexport * from './crm-booking'\n// Two contact documents folded into one (AGL-2625): the plan the console\n// previews and the server writes, reading the facet shape from `contacts`\n// and the company mirror from `crm`.\nexport * from './contact-merge'\n// A person's privacy erasure from one workspace (AGL-2623): the request's\n// shape and id, the marker a record carries while it waits, and the lists\n// the dialog and the docs share. Pure like `crm`; the sweep is server-side.\nexport * from './person-erasure'\n// What the CRM's reports compute from what they read — periods, weekly\n// buckets, the lifecycle funnel, pipeline totals (AGL-2604). Pure like `crm`,\n// and beside it because it reads the stage list and the deal weighting from\n// there rather than restating either.\nexport * from './crm-reports'\n// Every CRM CSV, written once (AGL-2662): the column lists and the cells\n// each section's Export button writes, and the server's whole-collection\n// export streams — one implementation, because the console app may not\n// import the plugin whose sections used to hold it.\nexport * from './crm-csv'\n// Which holder a cross-site reader sees one contact through (AGL-2630),\n// beside the files that write it: the org-level list and the server's\n// export both flatten a contact this way.\nexport * from './contact-holder'\n// What one member is owed this morning — the day's window in a named zone,\n// the task and lead bucketing, and the words the notification and the mail\n// say (AGL-2619). Pure like `crm-reports`, read by the scheduled route.\nexport * from './crm-digest'\n// A task's reminder at its own time (AGL-2659): which reminder a save\n// leaves, whether one is owed, and the words the notification and the mail\n// say. Pure like `crm-digest`, read by the hourly route and by every\n// writer of the field.\nexport * from './crm-task-reminders'\n// Email capture (AGL-2657): the address's shape and token, which of a\n// message's addresses is the correspondent, the excerpt, and the row a\n// captured message becomes. Pure like `crm-digest`; the webhook route and\n// the address route are the readers with Firestore.\nexport * from './crm-inbound'\n// A member's own addresses in a workspace (AGL-2975): the ones they have\n// confirmed count as theirs wherever the roster is asked who wrote a\n// message. Pure; the store and the confirmation link are server-side.\nexport * from './member-email-aliases'\n// The next activity a record carries (AGL-2661): what `nextTaskAtMs` IS given\n// a record's open tasks, the view clause a list filters on, the report figure.\n// Pure like `crm`; the admin library's `recomputeCrmNextTaskAt` is the writer.\nexport * from './crm-next-activity'\n// The CSV cell grammar the exports write with (AGL-2621), and the target a\n// mapped column names for a custom field — named rather than starred, because\n// an importer built on this file exports the shared ceilings under its own\n// names.\nexport {\n csvCell,\n csvDocument,\n customImportTarget,\n customImportTargetKey,\n} from './csv-import'\nexport type {\n ImportChunkResult,\n ImportDroppedValue,\n ImportSkippedRow,\n} from './csv-import'\n// What a VALUE under a custom contact field may be, and the coercion every\n// door applies to one (AGL-2601): a mapped form field, an API `custom` body,\n// and the carry-over that keeps a mapping across a publish. Pure like `crm`.\nexport * from './contact-custom-fields'\n// The consent JOIN (`docs/specs/email-overhaul.md` §3f). Pure, and it composes\n// `contacts` for the normalizer, so it sits directly beside it. No Node\n// builtin, which is what keeps it out of the `/server`-only group `person-key`\n// belongs to.\nexport * from './consent-groups'\nexport * from './marketing-consent'\n// The ENROLLMENT-time half of the same question, beside its reader for the\n// same reason: `marketing-consent` decides whether a recorded basis lets us\n// mail somebody, and this decides what basis putting them on a list may\n// record. Two enrollment surfaces import it — the Inbox assignment route and\n// the Emails console's audience card — and neither could import the other.\nexport * from './list-assignment-policy'\n// Reading the FILE a merchant arrives with (`docs/specs/email-competitive-gaps.md`\n// G5/P4), directly after the policy it hands its addresses to: parsing and\n// mechanical screening only, so that an import asks the enrollment question\n// through the same module the one-address add path asks it through rather\n// than answering it a second way.\nexport * from './list-import'\n// The dynamic-list rule (§3b/§3c), beside it for the same reasons: pure, and\n// it composes `contacts` for the segment vocabulary rather than restating it.\nexport * from './dynamic-list-rule'\n// The subscribable streams a recipient can leave one at a time\n// (`docs/specs/email-competitive-gaps.md` §1f). Pure, and read from all three\n// sides of the feature: the composer picks one, the send path signs it into\n// the unsubscribe link, and the unauthenticated preference page renders the\n// catalog. No Node builtin, so it stays out of the `/server`-only group.\nexport * from './email-topics'\nexport * from './compose-layout-nodes'\n// Layout properties applied through the chain (AGL-2893).\nexport * from './compose-layout-props'\n// A screen's per-page restyling of its layout's elements (AGL-3286).\nexport * from './layout-style-overrides'\nexport * from './document-landmark'\nexport * from './functions'\nexport * from './compose-reusable-components'\n// Where a reusable component is placed — pages or emails (AGL-3287).\nexport * from './reusable-component-kind'\nexport * from './compress'\nexport * from './merge-node-sx'\nexport * from './palette-sx'\nexport * from './scheme-sx'\n// An element pinned to one color scheme (AGL-3284).\nexport * from './element-color-scheme'\nexport * from './create-resource-uid'\nexport * from './decompress'\nexport * from './stored-nodes'\nexport * from './strip-undefined'\nexport * from './organizations'\nexport * from './org-permissions'\n// The AI verdict on either membership axis (AGL-2927). After both modules it\n// composes: the org catalog for org-wide members, the host role for site\n// collaborators.\nexport * from './host-permissions'\nexport * from './password-policy'\nexport * from './idp-profile'\nexport * from './onboarding-deep-link'\n// Where an account came from (AGL-1731). Beside the plan intent because\n// they are the same hop — the marketing CTA's query string — and both are\n// remembered on `users/{uid}` across the verification wall.\nexport * from './campaign-attribution'\n// The edge between a campaign and the forms, screens and contacts a push is\n// coordinated across. Beside the attribution above because the two answer\n// opposite questions about the same word: attribution records where somebody\n// CAME from, this records what a merchant DECLARED belongs together.\nexport * from './campaign-membership'\nexport * from './deployment-shape'\n// Which browser origins may complete a signed direct-to-GCS upload (AGL-1452).\n// GCS matches the origin list EXACTLY, so every serving console name needs its\n// own entry — the rule nobody could be expected to remember at attach time.\nexport * from './upload-cors'\n// The docs-help subset the first-party plugin consoles read (AGL-2213) — the\n// console's own registry lives in apps/console/constants and a lib cannot\n// import an app.\nexport * from './docs-help'\nexport * from './platform-brand'\nexport * from './plan-entitlements'\n// The free plan's bandwidth hard cap (AGL-1967/2070/2155). After\n// `plan-entitlements`, which owns the predicate it keys off.\nexport * from './bandwidth-cap'\nexport * from './form-abuse-ceiling'\nexport * from './forms'\n// What a form's DESIGN must still satisfy for its submissions to arrive.\n// After `forms`, whose field walk it reads the drawn fields with.\nexport * from './form-contract'\nexport * from './visitor-record-ceiling'\nexport * from './health-report'\n// Did a webhook delivery actually DO anything (AGL-1954)? After\n// `health-report`, whose billing verdict consumes the counts this produces.\nexport * from './webhook-delivery'\nexport * from './support-tiers'\nexport * from './release-flags'\n// Who runs THIS deployment, and where its notices go (AGL-2016). Ordered\n// before the four surfaces that print it, all of which used to declare their\n// own `support@aglyn.com` literal.\nexport * from './operator-identity'\nexport * from './lockdown'\nexport * from './media-quarantine'\nexport * from './media-takedown-reach'\nexport * from './upload-inspection'\nexport * from './abuse-report'\n// The other three quarters of §512 (AGL-1983): the put-back procedure that\n// answers a notice, and the strike ledger that conditions the safe harbour.\nexport * from './dmca-counter-notice'\nexport * from './repeat-infringer'\n// `addBusinessDays` briefly existed TWICE — once here in './support-tiers'\n// and once in './dmca-counter-notice' — and two `export *` made the name\n// ambiguous, TS2308'ing this barrel and every tsconfig that reads it. The\n// interleave was fixed twice in parallel: once by naming a winner here, and\n// once by deleting the second definition so './dmca-counter-notice' imports\n// this one. Only the deletion survives, so the disambiguating re-export that\n// stood on this line is gone with it — `export *` is unambiguous again.\n//\n// The concern that argued for keeping two copies is worth answering rather\n// than dropping: a later tweak to the SUPPORT SLA window must never silently\n// move the §512(g)(2)(C) put-back deadline. It cannot. This function embeds\n// no window — it is \"add N business days\", and N is the argument. The two\n// windows live in their own constants (`SUPPORT_*` there,\n// `COUNTER_NOTICE_*_BUSINESS_DAYS` in the DMCA module) and never meet. What\n// SHOULD stay shared is the day arithmetic itself: two copies of a weekend\n// rule are two chances to disagree about which day a deadline falls on.\nexport * from './org-override-reason'\nexport * from './host-tokens'\nexport * from './variables'\nexport * from './workflows'\nexport * from './datasets'\nexport * from './expand-repeatables'\nexport * from './org-roles'\nexport * from './markdown-lite'\nexport * from './notifications'\nexport * from './definition-canvas-tree'\nexport * from './ensure-canvas-root'\nexport * from './repair-canvas-nodes'\nexport * from './detect-template-placeholders'\nexport * from './guarded-version-save'\nexport * from './local-storage-budget'\nexport * from './measure-node-map'\nexport * from './resolve-named-tokens'\nexport * from './screen-route'\nexport * from './host-naming'\nexport * from './name-search'\nexport * from './artifact-list-keys'\nexport * from './name-match'\nexport * from './api-plugins'\nexport * from './plugin-api-rate-limit'\n// The same-origin gate on visitor-facing plugin writes (AGL-1880). After\n// `plugin-api-rate-limit`, whose `isMachinePluginApiPath` it reuses so the\n// two gates exempt exactly the same machine surfaces.\nexport * from './plugin-api-cross-origin'\nexport * from './actions'\nexport * from './element-animation'\nexport * from './attribution-guard'\nexport * from './node-interactions'\nexport * from './element-ui'\nexport * from './image-upload-types'\nexport * from './sanitize-svg'\nexport * from './media-folders'\nexport * from './media-metadata'\nexport * from './media-ref'\n// After `media-ref`, whose reference grammar it reads stored URLs with.\nexport * from './paid-media-source'\nexport * from './author-css'\n// The isomorphic HTML rule (AGL-1901), after the CSS one it depends on.\nexport * from './author-html'\nexport * from './dataset-models'\nexport * from './dataset-csv'\nexport * from './marketplace-provenance'\nexport * from './marketplace-update-state'\nexport * from './dataset-query'\nexport * from './plugin-manifest'\n// After `plugin-manifest`, whose revocation predicates it asks the kill\n// question with, and after `media-ref`, whose grammar decides what counts as a\n// first-party image.\nexport * from './email-starter-policy'\nexport * from './safe-json-ld'\n// Agent readiness (AGL-2716): content negotiation, the Markdown rendering of a\n// page, `/llms.txt` and the per-site OpenAPI document. All pure, all server-safe.\nexport * from './accept-negotiation'\nexport * from './agent-api-catalog'\nexport * from './agent-openapi'\nexport * from './author-html-markdown'\nexport * from './llms-txt'\nexport * from './page-markdown'\nexport * from './request-geo'\nexport * from './request-ip'\nexport * from './search-indexing'\nexport * from './seo-title'\nexport * from './sitemap'\nexport * from './visitor-consent'\nexport * from './social-image'\nexport * from './scope-tokens'\nexport * from './org-billing-doc'\nexport * from './stripe-deployment-mode'\nexport * from './deferred-image'\n"],"names":["csvCell","csvDocument","customImportTarget","customImportTargetKey"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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 * Server-safe app-utils: every app-util EXCEPT the client-only React\n * contexts (see ./contexts). Nothing here calls `createContext` at module\n * scope, so this barrel — re-exported by `@aglyn/aglyn/server` — is safe to\n * import from tenant Server Components (AGL-405). The full `./index` barrel\n * re-adds the contexts for client consumers.\n */\nexport * from './binding-token-catalog'\nexport * from './binding-tokens'\nexport * from './breadcrumb-json-ld'\n// What a search listing holds and how long each field may be (AGL-2910):\n// the SEO editors and anything that proposes a value read one catalog.\nexport * from './seo-listing-fields'\n// `VideoObject` for the Video elements a page actually ships (AGL-2747).\n// Beside the other structured-data builders, and server-only like them: only\n// the route emits JSON-LD, and a crawler-facing block has no client caller.\nexport * from './video-object'\nexport * from './collection-delete'\nexport * from './collection-entries'\nexport * from './collection-kind'\nexport * from './collection-slug'\nexport * from './content-authors'\nexport * from './content-query-fields'\nexport * from './content-schema-type'\nexport * from './content-author-profile'\nexport * from './url-slug'\nexport * from './child-contract'\nexport * from './child-contract-compose'\n// The one reading of `restrictChildren`/`restrictParent`, shared by the\n// besigner's drop check and the AI plugin's node-tree validator (AGL-2905).\nexport * from './lineal-order'\n// What a node tree must satisfy before it lands on a canvas that did not\n// author it (AGL-2939): the marketplace's install sanitizer and the AI\n// plugin's node-tree validator both pass through it.\nexport * from './node-definition-sanitizer'\nexport * from './console-routes'\nexport * from './console-record-links'\n// What a stored screen-link value means, with no React attached — the\n// where-used scan reads these on the server (AGL-703).\nexport * from './screen-link-value'\n// Which entries a page's links name (AGL-3118). The tenant render reads\n// exactly those entry documents, so the walk runs on the server.\nexport * from './link-references'\nexport * from './analytics-path-key'\nexport * from './contacts'\n// A lead's or a contact's email state (AGL-3245): the last verdict on its\n// address, in words a person can act on, mirrored from the senders' lists.\nexport * from './email-state'\n// The records that sit beside a contact — companies, pipelines, deals, tasks,\n// activities, field definitions (AGL-2595). Pure like `contacts`, and beside\n// it because `ContactFacet` names its lifecycle stage and custom-value types;\n// the scope stamp every CRM creator writes lives here too.\nexport * from './crm'\n// Email templates, snippets and the merge-field resolver (AGL-2658): the\n// letter a rep keeps under a name, and the one grammar the send route and\n// the dialog fill it in by. Pure like `crm`, and beside it because a\n// template is scoped the way a task is.\nexport * from './crm-email-templates'\n// The booking door's shared vocabulary (AGL-2660): the query keys a booking\n// link carries, the record reference inside it, and the wording and due\n// date of what a booking files back on the record.\nexport * from './crm-booking'\n// Two contact documents folded into one (AGL-2625): the plan the console\n// previews and the server writes, reading the facet shape from `contacts`\n// and the company mirror from `crm`.\nexport * from './contact-merge'\n// A person's privacy erasure from one workspace (AGL-2623): the request's\n// shape and id, the marker a record carries while it waits, and the lists\n// the dialog and the docs share. Pure like `crm`; the sweep is server-side.\nexport * from './person-erasure'\n// What the CRM's reports compute from what they read — periods, weekly\n// buckets, the lifecycle funnel, pipeline totals (AGL-2604). Pure like `crm`,\n// and beside it because it reads the stage list and the deal weighting from\n// there rather than restating either.\nexport * from './crm-reports'\n// Every CRM CSV, written once (AGL-2662): the column lists and the cells\n// each section's Export button writes, and the server's whole-collection\n// export streams — one implementation, because the console app may not\n// import the plugin whose sections used to hold it.\nexport * from './crm-csv'\n// Which holder a cross-site reader sees one contact through (AGL-2630),\n// beside the files that write it: the org-level list and the server's\n// export both flatten a contact this way.\nexport * from './contact-holder'\n// What one member is owed this morning — the day's window in a named zone,\n// the task and lead bucketing, and the words the notification and the mail\n// say (AGL-2619). Pure like `crm-reports`, read by the scheduled route.\nexport * from './crm-digest'\n// A task's reminder at its own time (AGL-2659): which reminder a save\n// leaves, whether one is owed, and the words the notification and the mail\n// say. Pure like `crm-digest`, read by the hourly route and by every\n// writer of the field.\nexport * from './crm-task-reminders'\n// Email capture (AGL-2657): the address's shape and token, which of a\n// message's addresses is the correspondent, the excerpt, and the row a\n// captured message becomes. Pure like `crm-digest`; the webhook route and\n// the address route are the readers with Firestore.\nexport * from './crm-inbound'\n// A member's own addresses in a workspace (AGL-2975): the ones they have\n// confirmed count as theirs wherever the roster is asked who wrote a\n// message. Pure; the store and the confirmation link are server-side.\nexport * from './member-email-aliases'\n// The next activity a record carries (AGL-2661): what `nextTaskAtMs` IS given\n// a record's open tasks, the view clause a list filters on, the report figure.\n// Pure like `crm`; the admin library's `recomputeCrmNextTaskAt` is the writer.\nexport * from './crm-next-activity'\n// The CSV cell grammar the exports write with (AGL-2621), and the target a\n// mapped column names for a custom field — named rather than starred, because\n// an importer built on this file exports the shared ceilings under its own\n// names.\nexport {\n csvCell,\n csvDocument,\n customImportTarget,\n customImportTargetKey,\n importNoMailServerSentence,\n} from './csv-import'\nexport type {\n ImportChunkResult,\n ImportDroppedValue,\n ImportSkippedRow,\n} from './csv-import'\n// What a VALUE under a custom contact field may be, and the coercion every\n// door applies to one (AGL-2601): a mapped form field, an API `custom` body,\n// and the carry-over that keeps a mapping across a publish. Pure like `crm`.\nexport * from './contact-custom-fields'\n// The consent JOIN (`docs/specs/email-overhaul.md` §3f). Pure, and it composes\n// `contacts` for the normalizer, so it sits directly beside it. No Node\n// builtin, which is what keeps it out of the `/server`-only group `person-key`\n// belongs to.\nexport * from './consent-groups'\nexport * from './marketing-consent'\n// The ENROLLMENT-time half of the same question, beside its reader for the\n// same reason: `marketing-consent` decides whether a recorded basis lets us\n// mail somebody, and this decides what basis putting them on a list may\n// record. Two enrollment surfaces import it — the Inbox assignment route and\n// the Emails console's audience card — and neither could import the other.\nexport * from './list-assignment-policy'\n// Reading the FILE a merchant arrives with (`docs/specs/email-competitive-gaps.md`\n// G5/P4), directly after the policy it hands its addresses to: parsing and\n// mechanical screening only, so that an import asks the enrollment question\n// through the same module the one-address add path asks it through rather\n// than answering it a second way.\nexport * from './list-import'\n// The dynamic-list rule (§3b/§3c), beside it for the same reasons: pure, and\n// it composes `contacts` for the segment vocabulary rather than restating it.\nexport * from './dynamic-list-rule'\n// The subscribable streams a recipient can leave one at a time\n// (`docs/specs/email-competitive-gaps.md` §1f). Pure, and read from all three\n// sides of the feature: the composer picks one, the send path signs it into\n// the unsubscribe link, and the unauthenticated preference page renders the\n// catalog. No Node builtin, so it stays out of the `/server`-only group.\nexport * from './email-topics'\nexport * from './compose-layout-nodes'\n// Layout properties applied through the chain (AGL-2893).\nexport * from './compose-layout-props'\n// A screen's per-page restyling of its layout's elements (AGL-3286).\nexport * from './layout-style-overrides'\nexport * from './document-landmark'\nexport * from './functions'\nexport * from './compose-reusable-components'\n// Where a reusable component is placed — pages or emails (AGL-3287).\nexport * from './reusable-component-kind'\nexport * from './compress'\nexport * from './merge-node-sx'\nexport * from './palette-sx'\nexport * from './scheme-sx'\n// An element pinned to one color scheme (AGL-3284).\nexport * from './element-color-scheme'\nexport * from './create-resource-uid'\nexport * from './decompress'\nexport * from './stored-nodes'\nexport * from './strip-undefined'\nexport * from './organizations'\nexport * from './org-permissions'\n// The AI verdict on either membership axis (AGL-2927). After both modules it\n// composes: the org catalog for org-wide members, the host role for site\n// collaborators.\nexport * from './host-permissions'\nexport * from './password-policy'\nexport * from './idp-profile'\nexport * from './onboarding-deep-link'\n// Where an account came from (AGL-1731). Beside the plan intent because\n// they are the same hop — the marketing CTA's query string — and both are\n// remembered on `users/{uid}` across the verification wall.\nexport * from './campaign-attribution'\n// The edge between a campaign and the forms, screens and contacts a push is\n// coordinated across. Beside the attribution above because the two answer\n// opposite questions about the same word: attribution records where somebody\n// CAME from, this records what a merchant DECLARED belongs together.\nexport * from './campaign-membership'\nexport * from './deployment-shape'\n// Which browser origins may complete a signed direct-to-GCS upload (AGL-1452).\n// GCS matches the origin list EXACTLY, so every serving console name needs its\n// own entry — the rule nobody could be expected to remember at attach time.\nexport * from './upload-cors'\n// The docs-help subset the first-party plugin consoles read (AGL-2213) — the\n// console's own registry lives in apps/console/constants and a lib cannot\n// import an app.\nexport * from './docs-help'\nexport * from './platform-brand'\nexport * from './plan-entitlements'\n// The free plan's bandwidth hard cap (AGL-1967/2070/2155). After\n// `plan-entitlements`, which owns the predicate it keys off.\nexport * from './bandwidth-cap'\nexport * from './form-abuse-ceiling'\nexport * from './forms'\n// What a form's DESIGN must still satisfy for its submissions to arrive.\n// After `forms`, whose field walk it reads the drawn fields with.\nexport * from './form-contract'\nexport * from './visitor-record-ceiling'\nexport * from './health-report'\n// Did a webhook delivery actually DO anything (AGL-1954)? After\n// `health-report`, whose billing verdict consumes the counts this produces.\nexport * from './webhook-delivery'\nexport * from './support-tiers'\nexport * from './release-flags'\n// Who runs THIS deployment, and where its notices go (AGL-2016). Ordered\n// before the four surfaces that print it, all of which used to declare their\n// own `support@aglyn.com` literal.\nexport * from './operator-identity'\nexport * from './lockdown'\nexport * from './media-quarantine'\nexport * from './media-takedown-reach'\nexport * from './upload-inspection'\nexport * from './abuse-report'\n// The other three quarters of §512 (AGL-1983): the put-back procedure that\n// answers a notice, and the strike ledger that conditions the safe harbour.\nexport * from './dmca-counter-notice'\nexport * from './repeat-infringer'\n// `addBusinessDays` briefly existed TWICE — once here in './support-tiers'\n// and once in './dmca-counter-notice' — and two `export *` made the name\n// ambiguous, TS2308'ing this barrel and every tsconfig that reads it. The\n// interleave was fixed twice in parallel: once by naming a winner here, and\n// once by deleting the second definition so './dmca-counter-notice' imports\n// this one. Only the deletion survives, so the disambiguating re-export that\n// stood on this line is gone with it — `export *` is unambiguous again.\n//\n// The concern that argued for keeping two copies is worth answering rather\n// than dropping: a later tweak to the SUPPORT SLA window must never silently\n// move the §512(g)(2)(C) put-back deadline. It cannot. This function embeds\n// no window — it is \"add N business days\", and N is the argument. The two\n// windows live in their own constants (`SUPPORT_*` there,\n// `COUNTER_NOTICE_*_BUSINESS_DAYS` in the DMCA module) and never meet. What\n// SHOULD stay shared is the day arithmetic itself: two copies of a weekend\n// rule are two chances to disagree about which day a deadline falls on.\nexport * from './org-override-reason'\nexport * from './host-tokens'\nexport * from './variables'\nexport * from './workflows'\nexport * from './datasets'\nexport * from './expand-repeatables'\nexport * from './org-roles'\nexport * from './markdown-lite'\nexport * from './notifications'\nexport * from './definition-canvas-tree'\nexport * from './ensure-canvas-root'\nexport * from './repair-canvas-nodes'\nexport * from './detect-template-placeholders'\nexport * from './guarded-version-save'\nexport * from './local-storage-budget'\nexport * from './measure-node-map'\nexport * from './resolve-named-tokens'\nexport * from './screen-route'\nexport * from './host-naming'\nexport * from './name-search'\nexport * from './artifact-list-keys'\nexport * from './name-match'\nexport * from './api-plugins'\nexport * from './plugin-api-rate-limit'\n// The same-origin gate on visitor-facing plugin writes (AGL-1880). After\n// `plugin-api-rate-limit`, whose `isMachinePluginApiPath` it reuses so the\n// two gates exempt exactly the same machine surfaces.\nexport * from './plugin-api-cross-origin'\nexport * from './actions'\nexport * from './element-animation'\nexport * from './attribution-guard'\nexport * from './node-interactions'\nexport * from './element-ui'\nexport * from './image-upload-types'\nexport * from './sanitize-svg'\nexport * from './media-folders'\nexport * from './media-metadata'\nexport * from './media-ref'\n// After `media-ref`, whose reference grammar it reads stored URLs with.\nexport * from './paid-media-source'\nexport * from './author-css'\n// The isomorphic HTML rule (AGL-1901), after the CSS one it depends on.\nexport * from './author-html'\nexport * from './dataset-models'\nexport * from './dataset-csv'\nexport * from './marketplace-provenance'\nexport * from './marketplace-update-state'\nexport * from './dataset-query'\nexport * from './plugin-manifest'\n// After `plugin-manifest`, whose revocation predicates it asks the kill\n// question with, and after `media-ref`, whose grammar decides what counts as a\n// first-party image.\nexport * from './email-starter-policy'\nexport * from './safe-json-ld'\n// Agent readiness (AGL-2716): content negotiation, the Markdown rendering of a\n// page, `/llms.txt` and the per-site OpenAPI document. All pure, all server-safe.\nexport * from './accept-negotiation'\nexport * from './agent-api-catalog'\nexport * from './agent-openapi'\nexport * from './author-html-markdown'\nexport * from './llms-txt'\nexport * from './page-markdown'\nexport * from './request-geo'\nexport * from './request-ip'\nexport * from './search-indexing'\nexport * from './seo-title'\nexport * from './sitemap'\nexport * from './visitor-consent'\nexport * from './social-image'\nexport * from './scope-tokens'\nexport * from './org-billing-doc'\nexport * from './stripe-deployment-mode'\nexport * from './deferred-image'\n"],"names":["csvCell","csvDocument","customImportTarget","customImportTargetKey","importNoMailServerSentence"],"mappings":"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"}
@@ -0,0 +1,52 @@
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
+ * The header and footer a SITE's email is drawn inside (AGL-3370).
19
+ *
20
+ * The site-side twin of `system-email-chrome.ts`. A site's mail to its own
21
+ * customers (an order receipt, a member's password reset, a newsletter
22
+ * confirmation) is the site's, so everything in the chrome is read off the
23
+ * site: its logo or its name in bold, linked to its address; a footer of why
24
+ * the reader is getting the mail, the site's support address, and a
25
+ * `© {year} {business name} · {postal address}` line.
26
+ *
27
+ * Nothing here names the platform. A site's customer bought from the site,
28
+ * not from Aglyn, and a vendor's name at the foot of a receipt is the same
29
+ * leak white-label exists to close, on every plan.
30
+ *
31
+ * Which emails wear it is the caller's decision: the built-in copy and the
32
+ * text-only messages do; a design the site owner published is theirs, placed
33
+ * header and footer included, and is sent as they built it.
34
+ *
35
+ * Pure: no reads, and a clock a test can replace.
36
+ */
37
+ import type { EmailChrome } from '@aglyn/shared-util-email/email-render';
38
+ import { type HostTokenSource } from './host-tokens';
39
+ export interface SiteEmailChromeInput {
40
+ /** The site's host document, or the fields of it the tokens read. */
41
+ host: HostTokenSource | null | undefined;
42
+ /**
43
+ * Why the recipient is getting this email, as a template: the renderer
44
+ * substitutes `{{host.businessName}}` and the email's own tokens in it, as
45
+ * it does in the body.
46
+ */
47
+ reason?: string;
48
+ /** The clock the copyright year is read from. */
49
+ now?: () => Date;
50
+ }
51
+ /** See the module note. */
52
+ export declare function buildSiteEmailChrome(input: SiteEmailChromeInput): EmailChrome;
@@ -0,0 +1,82 @@
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
+ * The header and footer a SITE's email is drawn inside (AGL-3370).
18
+ *
19
+ * The site-side twin of `system-email-chrome.ts`. A site's mail to its own
20
+ * customers (an order receipt, a member's password reset, a newsletter
21
+ * confirmation) is the site's, so everything in the chrome is read off the
22
+ * site: its logo or its name in bold, linked to its address; a footer of why
23
+ * the reader is getting the mail, the site's support address, and a
24
+ * `© {year} {business name} · {postal address}` line.
25
+ *
26
+ * Nothing here names the platform. A site's customer bought from the site,
27
+ * not from Aglyn, and a vendor's name at the foot of a receipt is the same
28
+ * leak white-label exists to close, on every plan.
29
+ *
30
+ * Which emails wear it is the caller's decision: the built-in copy and the
31
+ * text-only messages do; a design the site owner published is theirs, placed
32
+ * header and footer included, and is sent as they built it.
33
+ *
34
+ * Pure: no reads, and a clock a test can replace.
35
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
36
+ import { resolveHostToken } from "./host-tokens.js";
37
+ /**
38
+ * A logo an inbox can draw, or undefined.
39
+ *
40
+ * An SVG is refused: Gmail and Outlook draw no SVG in mail, so a site whose
41
+ * logo is one would open every email with an empty box where its name
42
+ * belongs. The business name in bold is the header instead.
43
+ */ function inboxLogo(url) {
44
+ if (!url) return undefined;
45
+ return /\.svg(?:[?#]|$)/i.test(url) || /^data:image\/svg/i.test(url) ? undefined : url;
46
+ }
47
+ /** See the module note. */ export function buildSiteEmailChrome(input) {
48
+ var _input_now, _resolveHostToken, _input_reason;
49
+ const { host } = input;
50
+ const now = (_input_now = input.now) != null ? _input_now : ()=>new Date();
51
+ const name = (_resolveHostToken = resolveHostToken('businessName', host)) != null ? _resolveHostToken : '';
52
+ const logo = inboxLogo(resolveHostToken('logo', host));
53
+ const url = resolveHostToken('url', host);
54
+ const supportEmail = resolveHostToken('supportEmail', host);
55
+ const address = resolveHostToken('address', host);
56
+ // The site's name is resolved here rather than left to the merge, so a
57
+ // site that has not set one reads "this site" where a merge would leave a
58
+ // hole in the sentence ("you placed an order with .").
59
+ const reason = String((_input_reason = input.reason) != null ? _input_reason : '').replace(/\{\{\s*host\.businessName\s*\}\}/g, name || 'this site').trim();
60
+ const footer = _extends({}, reason ? {
61
+ reason
62
+ } : {}, supportEmail ? {
63
+ support: {
64
+ label: supportEmail,
65
+ href: `mailto:${supportEmail}`
66
+ }
67
+ } : {}, name ? {
68
+ legal: `© ${now().getUTCFullYear()} ${name}` + (address ? ` · ${address.replace(/\s*\n\s*/g, ', ')}` : '')
69
+ } : {});
70
+ return {
71
+ header: _extends({}, logo ? {
72
+ logoUrl: logo
73
+ } : {}, {
74
+ logoAlt: name
75
+ }, url ? {
76
+ href: url
77
+ } : {}),
78
+ footer
79
+ };
80
+ }
81
+
82
+ //# sourceMappingURL=site-email-chrome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-email-chrome.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 * The header and footer a SITE's email is drawn inside (AGL-3370).\n *\n * The site-side twin of `system-email-chrome.ts`. A site's mail to its own\n * customers (an order receipt, a member's password reset, a newsletter\n * confirmation) is the site's, so everything in the chrome is read off the\n * site: its logo or its name in bold, linked to its address; a footer of why\n * the reader is getting the mail, the site's support address, and a\n * `© {year} {business name} · {postal address}` line.\n *\n * Nothing here names the platform. A site's customer bought from the site,\n * not from Aglyn, and a vendor's name at the foot of a receipt is the same\n * leak white-label exists to close, on every plan.\n *\n * Which emails wear it is the caller's decision: the built-in copy and the\n * text-only messages do; a design the site owner published is theirs, placed\n * header and footer included, and is sent as they built it.\n *\n * Pure: no reads, and a clock a test can replace.\n */\n\nimport type {\n EmailChrome,\n EmailChromeFooter,\n} from '@aglyn/shared-util-email/email-render'\n\nimport { resolveHostToken, type HostTokenSource } from './host-tokens'\n\nexport interface SiteEmailChromeInput {\n /** The site's host document, or the fields of it the tokens read. */\n host: HostTokenSource | null | undefined\n /**\n * Why the recipient is getting this email, as a template: the renderer\n * substitutes `{{host.businessName}}` and the email's own tokens in it, as\n * it does in the body.\n */\n reason?: string\n /** The clock the copyright year is read from. */\n now?: () => Date\n}\n\n/**\n * A logo an inbox can draw, or undefined.\n *\n * An SVG is refused: Gmail and Outlook draw no SVG in mail, so a site whose\n * logo is one would open every email with an empty box where its name\n * belongs. The business name in bold is the header instead.\n */\nfunction inboxLogo(url: string | undefined): string | undefined {\n if (!url) return undefined\n return /\\.svg(?:[?#]|$)/i.test(url) || /^data:image\\/svg/i.test(url)\n ? undefined\n : url\n}\n\n/** See the module note. */\nexport function buildSiteEmailChrome(input: SiteEmailChromeInput): EmailChrome {\n const { host } = input\n const now = input.now ?? (() => new Date())\n const name = resolveHostToken('businessName', host) ?? ''\n const logo = inboxLogo(resolveHostToken('logo', host))\n const url = resolveHostToken('url', host)\n const supportEmail = resolveHostToken('supportEmail', host)\n const address = resolveHostToken('address', host)\n\n // The site's name is resolved here rather than left to the merge, so a\n // site that has not set one reads \"this site\" where a merge would leave a\n // hole in the sentence (\"you placed an order with .\").\n const reason = String(input.reason ?? '')\n .replace(/\\{\\{\\s*host\\.businessName\\s*\\}\\}/g, name || 'this site')\n .trim()\n const footer: EmailChromeFooter = {\n ...(reason ? { reason } : {}),\n ...(supportEmail\n ? { support: { label: supportEmail, href: `mailto:${supportEmail}` } }\n : {}),\n ...(name\n ? {\n legal:\n `© ${now().getUTCFullYear()} ${name}` +\n (address ? ` · ${address.replace(/\\s*\\n\\s*/g, ', ')}` : ''),\n }\n : {}),\n }\n\n return {\n header: {\n ...(logo ? { logoUrl: logo } : {}),\n logoAlt: name,\n ...(url ? { href: url } : {}),\n },\n footer,\n }\n}\n"],"names":["resolveHostToken","inboxLogo","url","undefined","test","buildSiteEmailChrome","input","host","now","Date","name","logo","supportEmail","address","reason","String","replace","trim","footer","support","label","href","legal","getUTCFullYear","header","logoUrl","logoAlt"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;CAmBC;AAOD,SAASA,gBAAgB,QAA8B,mBAAe;AAetE;;;;;;CAMC,GACD,SAASC,UAAUC,GAAuB;IACxC,IAAI,CAACA,KAAK,OAAOC;IACjB,OAAO,mBAAmBC,IAAI,CAACF,QAAQ,oBAAoBE,IAAI,CAACF,OAC5DC,YACAD;AACN;AAEA,yBAAyB,GACzB,OAAO,SAASG,qBAAqBC,KAA2B;QAElDA,YACCN,mBASSM;IAXtB,MAAM,EAAEC,IAAI,EAAE,GAAGD;IACjB,MAAME,OAAMF,aAAAA,MAAME,GAAG,YAATF,aAAc,IAAM,IAAIG;IACpC,MAAMC,QAAOV,oBAAAA,iBAAiB,gBAAgBO,iBAAjCP,oBAA0C;IACvD,MAAMW,OAAOV,UAAUD,iBAAiB,QAAQO;IAChD,MAAML,MAAMF,iBAAiB,OAAOO;IACpC,MAAMK,eAAeZ,iBAAiB,gBAAgBO;IACtD,MAAMM,UAAUb,iBAAiB,WAAWO;IAE5C,uEAAuE;IACvE,0EAA0E;IAC1E,uDAAuD;IACvD,MAAMO,SAASC,QAAOT,gBAAAA,MAAMQ,MAAM,YAAZR,gBAAgB,IACnCU,OAAO,CAAC,qCAAqCN,QAAQ,aACrDO,IAAI;IACP,MAAMC,SAA4B,aAC5BJ,SAAS;QAAEA;IAAO,IAAI,CAAC,GACvBF,eACA;QAAEO,SAAS;YAAEC,OAAOR;YAAcS,MAAM,CAAC,OAAO,EAAET,cAAc;QAAC;IAAE,IACnE,CAAC,GACDF,OACA;QACEY,OACE,CAAC,EAAE,EAAEd,MAAMe,cAAc,GAAG,CAAC,EAAEb,MAAM,GACpCG,CAAAA,UAAU,CAAC,GAAG,EAAEA,QAAQG,OAAO,CAAC,aAAa,OAAO,GAAG,EAAC;IAC7D,IACA,CAAC;IAGP,OAAO;QACLQ,QAAQ,aACFb,OAAO;YAAEc,SAASd;QAAK,IAAI,CAAC;YAChCe,SAAShB;WACLR,MAAM;YAAEmB,MAAMnB;QAAI,IAAI,CAAC;QAE7BgB;IACF;AACF"}
@@ -82,6 +82,12 @@ export interface SystemEmailBrandOptions {
82
82
  * `http(s)` URL is linked; a `mailto:` home is no destination for a logo.
83
83
  */
84
84
  brandHomeUrl?: string | null;
85
+ /**
86
+ * The org's resolved `primaryColor` (AGL-3370): a white-label send's
87
+ * buttons and links wear it, over the platform palette. Absent or blank is
88
+ * the platform's own accent.
89
+ */
90
+ brandPrimaryColor?: string | null;
85
91
  }
86
92
  /**
87
93
  * Whether a send goes out under the PLATFORM's brand, which is the only kind
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/system-email-chrome.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 * The header and footer a system email is drawn inside, for the brand it is\n * sent under (AGL-3322).\n *\n * A system email's built-in copy is a few lines of text, and only a staff\n * design can place the platform marketing site's email blocks. Without this,\n * the mail the platform sends most — every password reset, invite and alert\n * nobody has designed — would name its sender nowhere but the From line, and\n * tell the reader neither why it came nor where to get help. The renderer\n * draws whatever `EmailChrome` it is handed; this module decides what that\n * is, which comes down to one question: whose mail is this?\n *\n * ## The platform's own\n *\n * Its logo linked to its home page, and a footer of the email's reason, the\n * support link and a `© {year} {legal name} · {postal address}` line. Every\n * value is read from `platform-brand.ts`, so a renamed or self-hosted\n * deployment prints its own configuration: its logo or its name in bold, its\n * home page or no link, its address or none — and never ours. The Aglyn\n * defaults there apply only while the deployment is still called Aglyn.\n *\n * ## A white-label organization's\n *\n * Its own email logo, or its product name in bold; the reason, whose brand\n * tokens are the agency's; and a support link only when the agency set one.\n * No legal line: the only legal name and address this module knows are the\n * platform's, and the recipient is the agency's customer, who must not learn\n * the platform exists from the foot of an invite.\n *\n * Pure: no reads, no clock of its own that a test cannot replace.\n */\n\nimport type {\n EmailChrome,\n EmailChromeFooter,\n} from '@aglyn/shared-util-email/email-render'\nimport type { SystemEmailTemplateDefinition } from '@aglyn/shared-util-email/system-email-catalog'\n\nimport { brandMergeTokens } from './plan-entitlements'\nimport {\n PLATFORM_BRANDING_PROFILE,\n PLATFORM_BRAND_LEGAL_NAME,\n PLATFORM_BRAND_NAME,\n PLATFORM_EMAIL_LOGO_URL,\n PLATFORM_HOME_URL,\n PLATFORM_POSTAL_ADDRESS,\n} from './platform-brand'\n\n/**\n * `brand.*` tokens every system email resolves, UNDER whatever the caller\n * supplies (AGL-2139).\n *\n * The order is the whole safety property. A system email blanks any token\n * the caller did not provide, so a designed template written against\n * `{{brand.productName}}` would ship with a hole in the sentence at any send\n * site that forgot to pass one. Defaulting to the platform profile means the\n * worst case is the platform's own copy rather than a gap — and an\n * org-context sender that spreads `brandMergeTokens(branding)` into its merge\n * map wins, because a later key overwrites an earlier one.\n *\n * The platform-scoped senders — password reset, email verification, the\n * security alerts, the staff erasure-hold alert — genuinely have no org (each\n * says so at its own call site, and password reset deliberately refuses to\n * look one up because that would be an enumeration oracle). The platform IS\n * their brand, so these are the right answer for them.\n */\nexport const PLATFORM_BRAND_MERGE_TOKENS: Readonly<Record<string, string>> =\n brandMergeTokens(PLATFORM_BRANDING_PROFILE)\n\n/**\n * Brand inputs that are NOT merge tokens (AGL-2139).\n *\n * `brand.productName` and friends ride the merge map because a designer types\n * them into copy. The logo and the home page are structural — the header the\n * renderer draws, or does not — so they are options rather than tokens a\n * template could forget to place.\n */\nexport interface SystemEmailBrandOptions {\n /** The org's resolved `emailLogoUrl`; absent or blank is no org logo. */\n brandLogoUrl?: string | null\n /**\n * The org's resolved `homeUrl`: where a white-label header links. Only an\n * `http(s)` URL is linked; a `mailto:` home is no destination for a logo.\n */\n brandHomeUrl?: string | null\n}\n\n/**\n * Whether a send goes out under the PLATFORM's brand, which is the only kind\n * that may carry the platform's header and footer — the marketing site's\n * email blocks (AGL-3318), or the chrome {@link buildSystemEmailChrome} draws\n * in their place (AGL-3322).\n *\n * Those are the platform's logo, its address, its line telling the reader why\n * they get the mail. An agency on white-label sells its clients a product\n * with no vendor in it, and an invite or a usage summary sent to one of their\n * people reads as the agency's own mail. One platform footer in it names the\n * vendor the whole tier is bought to hide.\n *\n * Read off what the send is actually branded as, rather than asked of the\n * caller, so no sender can forget to say. An org-context sender spreads\n * `brandMergeTokens(resolveBrandingProfile(org))` over the defaults and passes\n * the profile's email logo. A white-label profile never inherits the\n * platform's support URL (AGL-2428), so its tokens differ from\n * {@link PLATFORM_BRAND_MERGE_TOKENS} on `brand.supportUrl` at least, and the\n * platform profile has no email logo of its own, so any logo is an org's. The\n * org-less senders, and every org without white-label, which resolves to the\n * platform profile itself, match the defaults exactly: those sends are the\n * platform's.\n */\nexport function isPlatformBrandedSend(\n merged: Readonly<Record<string, string>>,\n options: SystemEmailBrandOptions = {},\n): boolean {\n if (String(options.brandLogoUrl ?? '').trim()) return false\n return Object.entries(PLATFORM_BRAND_MERGE_TOKENS).every(\n ([token, value]) => merged[token] === value,\n )\n}\n\nexport interface SystemEmailChromeInput extends SystemEmailBrandOptions {\n /** The email being sent; its `footerReason` is the footer's first line. */\n definition: Pick<SystemEmailTemplateDefinition, 'footerReason'>\n /**\n * The send's merge map, platform brand tokens under the caller's — the map\n * the body renders with, which is what makes the reason's tokens resolve to\n * the same names the body uses.\n */\n merged: Readonly<Record<string, string>>\n /** The clock the copyright year is read from. */\n now?: () => Date\n}\n\n/** A non-empty trimmed string, else undefined. */\nconst clean = (value: string | null | undefined): string | undefined => {\n const text = typeof value === 'string' ? value.trim() : ''\n return text.length ? text : undefined\n}\n\n/**\n * The footer's support link, from the send's `brand.supportUrl`, or\n * undefined when it has none.\n *\n * Empty is a decision, not a gap: a white-label org that set no support URL\n * gets NO line rather than the platform's desk (AGL-2428), which cannot help\n * its customers and would name the vendor to them. A `mailto:` reads as the\n * mailbox, which is what the reader has to type into a client that does not\n * follow the link; a web page reads as an instruction.\n */\nfunction supportLink(\n url: string | undefined,\n): EmailChromeFooter['support'] | undefined {\n const href = clean(url)\n if (!href) return undefined\n if (/^mailto:/i.test(href)) {\n const address = clean(href.slice('mailto:'.length).split('?')[0])\n return address ? { label: address, href } : undefined\n }\n return { label: 'Get help', href }\n}\n\n/**\n * The chrome one system email is drawn inside, for the brand it is sent\n * under. See the module comment for the two shapes and why they differ.\n *\n * The reason and the other lines go out as templates: the renderer runs them\n * through the same merge substitution as the body, so `{{org.name}}` and\n * `{{brand.productName}}` read exactly as they do in the copy above them.\n */\nexport function buildSystemEmailChrome(\n input: SystemEmailChromeInput,\n): EmailChrome {\n const { definition, merged, brandLogoUrl, brandHomeUrl } = input\n const now = input.now ?? (() => new Date())\n const reason = clean(definition.footerReason)\n const support = supportLink(merged['brand.supportUrl'])\n const footer: EmailChromeFooter = {\n ...(reason ? { reason } : {}),\n ...(support ? { support } : {}),\n }\n\n if (isPlatformBrandedSend(merged, { brandLogoUrl })) {\n const legal =\n `© ${now().getUTCFullYear()} ${PLATFORM_BRAND_LEGAL_NAME}` +\n (PLATFORM_POSTAL_ADDRESS ? ` · ${PLATFORM_POSTAL_ADDRESS}` : '')\n return {\n header: {\n ...(PLATFORM_EMAIL_LOGO_URL ? { logoUrl: PLATFORM_EMAIL_LOGO_URL } : {}),\n logoAlt: PLATFORM_BRAND_NAME,\n ...(PLATFORM_HOME_URL ? { href: PLATFORM_HOME_URL } : {}),\n },\n footer: { ...footer, legal },\n }\n }\n\n const logoUrl = clean(brandLogoUrl)\n const home = clean(brandHomeUrl)\n return {\n header: {\n ...(logoUrl ? { logoUrl } : {}),\n logoAlt: merged['brand.productName'] ?? '',\n ...(home && /^https?:\\/\\//i.test(home) ? { href: home } : {}),\n },\n footer,\n }\n}\n"],"names":["brandMergeTokens","PLATFORM_BRANDING_PROFILE","PLATFORM_BRAND_LEGAL_NAME","PLATFORM_BRAND_NAME","PLATFORM_EMAIL_LOGO_URL","PLATFORM_HOME_URL","PLATFORM_POSTAL_ADDRESS","PLATFORM_BRAND_MERGE_TOKENS","isPlatformBrandedSend","merged","options","String","brandLogoUrl","trim","Object","entries","every","token","value","clean","text","length","undefined","supportLink","url","href","test","address","slice","split","label","buildSystemEmailChrome","input","definition","brandHomeUrl","now","Date","reason","footerReason","support","footer","legal","getUTCFullYear","header","logoUrl","logoAlt","home"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC;AAQD,SAASA,gBAAgB,QAAQ,yBAAqB;AACtD,SACEC,yBAAyB,EACzBC,yBAAyB,EACzBC,mBAAmB,EACnBC,uBAAuB,EACvBC,iBAAiB,EACjBC,uBAAuB,QAClB,sBAAkB;AAEzB;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMC,8BACXP,iBAAiBC,2BAA0B;AAoB7C;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASO,sBACdC,MAAwC,EACxCC,UAAmC,CAAC,CAAC;QAE1BA;IAAX,IAAIC,QAAOD,wBAAAA,QAAQE,YAAY,YAApBF,wBAAwB,IAAIG,IAAI,IAAI,OAAO;IACtD,OAAOC,OAAOC,OAAO,CAACR,6BAA6BS,KAAK,CACtD,CAAC,CAACC,OAAOC,MAAM,GAAKT,MAAM,CAACQ,MAAM,KAAKC;AAE1C;AAeA,gDAAgD,GAChD,MAAMC,QAAQ,CAACD;IACb,MAAME,OAAO,OAAOF,UAAU,WAAWA,MAAML,IAAI,KAAK;IACxD,OAAOO,KAAKC,MAAM,GAAGD,OAAOE;AAC9B;AAEA;;;;;;;;;CASC,GACD,SAASC,YACPC,GAAuB;IAEvB,MAAMC,OAAON,MAAMK;IACnB,IAAI,CAACC,MAAM,OAAOH;IAClB,IAAI,YAAYI,IAAI,CAACD,OAAO;QAC1B,MAAME,UAAUR,MAAMM,KAAKG,KAAK,CAAC,UAAUP,MAAM,EAAEQ,KAAK,CAAC,IAAI,CAAC,EAAE;QAChE,OAAOF,UAAU;YAAEG,OAAOH;YAASF;QAAK,IAAIH;IAC9C;IACA,OAAO;QAAEQ,OAAO;QAAYL;IAAK;AACnC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASM,uBACdC,KAA6B;QAGjBA,YA2BCvB;IA5Bb,MAAM,EAAEwB,UAAU,EAAExB,MAAM,EAAEG,YAAY,EAAEsB,YAAY,EAAE,GAAGF;IAC3D,MAAMG,OAAMH,aAAAA,MAAMG,GAAG,YAATH,aAAc,IAAM,IAAII;IACpC,MAAMC,SAASlB,MAAMc,WAAWK,YAAY;IAC5C,MAAMC,UAAUhB,YAAYd,MAAM,CAAC,mBAAmB;IACtD,MAAM+B,SAA4B,aAC5BH,SAAS;QAAEA;IAAO,IAAI,CAAC,GACvBE,UAAU;QAAEA;IAAQ,IAAI,CAAC;IAG/B,IAAI/B,sBAAsBC,QAAQ;QAAEG;IAAa,IAAI;QACnD,MAAM6B,QACJ,CAAC,EAAE,EAAEN,MAAMO,cAAc,GAAG,CAAC,EAAExC,2BAA2B,GACzDI,CAAAA,0BAA0B,CAAC,GAAG,EAAEA,yBAAyB,GAAG,EAAC;QAChE,OAAO;YACLqC,QAAQ,aACFvC,0BAA0B;gBAAEwC,SAASxC;YAAwB,IAAI,CAAC;gBACtEyC,SAAS1C;eACLE,oBAAoB;gBAAEoB,MAAMpB;YAAkB,IAAI,CAAC;YAEzDmC,QAAQ,aAAKA;gBAAQC;;QACvB;IACF;IAEA,MAAMG,UAAUzB,MAAMP;IACtB,MAAMkC,OAAO3B,MAAMe;IACnB,OAAO;QACLS,QAAQ,aACFC,UAAU;YAAEA;QAAQ,IAAI,CAAC;YAC7BC,OAAO,GAAEpC,2BAAAA,MAAM,CAAC,oBAAoB,YAA3BA,2BAA+B;WACpCqC,QAAQ,gBAAgBpB,IAAI,CAACoB,QAAQ;YAAErB,MAAMqB;QAAK,IAAI,CAAC;QAE7DN;IACF;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/system-email-chrome.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 * The header and footer a system email is drawn inside, for the brand it is\n * sent under (AGL-3322).\n *\n * A system email's built-in copy is a few lines of text, and only a staff\n * design can place the platform marketing site's email blocks. Without this,\n * the mail the platform sends most — every password reset, invite and alert\n * nobody has designed — would name its sender nowhere but the From line, and\n * tell the reader neither why it came nor where to get help. The renderer\n * draws whatever `EmailChrome` it is handed; this module decides what that\n * is, which comes down to one question: whose mail is this?\n *\n * ## The platform's own\n *\n * Its logo linked to its home page, and a footer of the email's reason, the\n * support link and a `© {year} {legal name} · {postal address}` line. Every\n * value is read from `platform-brand.ts`, so a renamed or self-hosted\n * deployment prints its own configuration: its logo or its name in bold, its\n * home page or no link, its address or none — and never ours. The Aglyn\n * defaults there apply only while the deployment is still called Aglyn.\n *\n * ## A white-label organization's\n *\n * Its own email logo, or its product name in bold; the reason, whose brand\n * tokens are the agency's; and a support link only when the agency set one.\n * No legal line: the only legal name and address this module knows are the\n * platform's, and the recipient is the agency's customer, who must not learn\n * the platform exists from the foot of an invite.\n *\n * Pure: no reads, no clock of its own that a test cannot replace.\n */\n\nimport type {\n EmailChrome,\n EmailChromeFooter,\n} from '@aglyn/shared-util-email/email-render'\nimport type { SystemEmailTemplateDefinition } from '@aglyn/shared-util-email/system-email-catalog'\n\nimport { brandMergeTokens } from './plan-entitlements'\nimport {\n PLATFORM_BRANDING_PROFILE,\n PLATFORM_BRAND_LEGAL_NAME,\n PLATFORM_BRAND_NAME,\n PLATFORM_EMAIL_LOGO_URL,\n PLATFORM_HOME_URL,\n PLATFORM_POSTAL_ADDRESS,\n} from './platform-brand'\n\n/**\n * `brand.*` tokens every system email resolves, UNDER whatever the caller\n * supplies (AGL-2139).\n *\n * The order is the whole safety property. A system email blanks any token\n * the caller did not provide, so a designed template written against\n * `{{brand.productName}}` would ship with a hole in the sentence at any send\n * site that forgot to pass one. Defaulting to the platform profile means the\n * worst case is the platform's own copy rather than a gap — and an\n * org-context sender that spreads `brandMergeTokens(branding)` into its merge\n * map wins, because a later key overwrites an earlier one.\n *\n * The platform-scoped senders — password reset, email verification, the\n * security alerts, the staff erasure-hold alert — genuinely have no org (each\n * says so at its own call site, and password reset deliberately refuses to\n * look one up because that would be an enumeration oracle). The platform IS\n * their brand, so these are the right answer for them.\n */\nexport const PLATFORM_BRAND_MERGE_TOKENS: Readonly<Record<string, string>> =\n brandMergeTokens(PLATFORM_BRANDING_PROFILE)\n\n/**\n * Brand inputs that are NOT merge tokens (AGL-2139).\n *\n * `brand.productName` and friends ride the merge map because a designer types\n * them into copy. The logo and the home page are structural — the header the\n * renderer draws, or does not — so they are options rather than tokens a\n * template could forget to place.\n */\nexport interface SystemEmailBrandOptions {\n /** The org's resolved `emailLogoUrl`; absent or blank is no org logo. */\n brandLogoUrl?: string | null\n /**\n * The org's resolved `homeUrl`: where a white-label header links. Only an\n * `http(s)` URL is linked; a `mailto:` home is no destination for a logo.\n */\n brandHomeUrl?: string | null\n /**\n * The org's resolved `primaryColor` (AGL-3370): a white-label send's\n * buttons and links wear it, over the platform palette. Absent or blank is\n * the platform's own accent.\n */\n brandPrimaryColor?: string | null\n}\n\n/**\n * Whether a send goes out under the PLATFORM's brand, which is the only kind\n * that may carry the platform's header and footer — the marketing site's\n * email blocks (AGL-3318), or the chrome {@link buildSystemEmailChrome} draws\n * in their place (AGL-3322).\n *\n * Those are the platform's logo, its address, its line telling the reader why\n * they get the mail. An agency on white-label sells its clients a product\n * with no vendor in it, and an invite or a usage summary sent to one of their\n * people reads as the agency's own mail. One platform footer in it names the\n * vendor the whole tier is bought to hide.\n *\n * Read off what the send is actually branded as, rather than asked of the\n * caller, so no sender can forget to say. An org-context sender spreads\n * `brandMergeTokens(resolveBrandingProfile(org))` over the defaults and passes\n * the profile's email logo. A white-label profile never inherits the\n * platform's support URL (AGL-2428), so its tokens differ from\n * {@link PLATFORM_BRAND_MERGE_TOKENS} on `brand.supportUrl` at least, and the\n * platform profile has no email logo of its own, so any logo is an org's. The\n * org-less senders, and every org without white-label, which resolves to the\n * platform profile itself, match the defaults exactly: those sends are the\n * platform's.\n */\nexport function isPlatformBrandedSend(\n merged: Readonly<Record<string, string>>,\n options: SystemEmailBrandOptions = {},\n): boolean {\n if (String(options.brandLogoUrl ?? '').trim()) return false\n return Object.entries(PLATFORM_BRAND_MERGE_TOKENS).every(\n ([token, value]) => merged[token] === value,\n )\n}\n\nexport interface SystemEmailChromeInput extends SystemEmailBrandOptions {\n /** The email being sent; its `footerReason` is the footer's first line. */\n definition: Pick<SystemEmailTemplateDefinition, 'footerReason'>\n /**\n * The send's merge map, platform brand tokens under the caller's — the map\n * the body renders with, which is what makes the reason's tokens resolve to\n * the same names the body uses.\n */\n merged: Readonly<Record<string, string>>\n /** The clock the copyright year is read from. */\n now?: () => Date\n}\n\n/** A non-empty trimmed string, else undefined. */\nconst clean = (value: string | null | undefined): string | undefined => {\n const text = typeof value === 'string' ? value.trim() : ''\n return text.length ? text : undefined\n}\n\n/**\n * The footer's support link, from the send's `brand.supportUrl`, or\n * undefined when it has none.\n *\n * Empty is a decision, not a gap: a white-label org that set no support URL\n * gets NO line rather than the platform's desk (AGL-2428), which cannot help\n * its customers and would name the vendor to them. A `mailto:` reads as the\n * mailbox, which is what the reader has to type into a client that does not\n * follow the link; a web page reads as an instruction.\n */\nfunction supportLink(\n url: string | undefined,\n): EmailChromeFooter['support'] | undefined {\n const href = clean(url)\n if (!href) return undefined\n if (/^mailto:/i.test(href)) {\n const address = clean(href.slice('mailto:'.length).split('?')[0])\n return address ? { label: address, href } : undefined\n }\n return { label: 'Get help', href }\n}\n\n/**\n * The chrome one system email is drawn inside, for the brand it is sent\n * under. See the module comment for the two shapes and why they differ.\n *\n * The reason and the other lines go out as templates: the renderer runs them\n * through the same merge substitution as the body, so `{{org.name}}` and\n * `{{brand.productName}}` read exactly as they do in the copy above them.\n */\nexport function buildSystemEmailChrome(\n input: SystemEmailChromeInput,\n): EmailChrome {\n const { definition, merged, brandLogoUrl, brandHomeUrl } = input\n const now = input.now ?? (() => new Date())\n const reason = clean(definition.footerReason)\n const support = supportLink(merged['brand.supportUrl'])\n const footer: EmailChromeFooter = {\n ...(reason ? { reason } : {}),\n ...(support ? { support } : {}),\n }\n\n if (isPlatformBrandedSend(merged, { brandLogoUrl })) {\n const legal =\n `© ${now().getUTCFullYear()} ${PLATFORM_BRAND_LEGAL_NAME}` +\n (PLATFORM_POSTAL_ADDRESS ? ` · ${PLATFORM_POSTAL_ADDRESS}` : '')\n return {\n header: {\n ...(PLATFORM_EMAIL_LOGO_URL ? { logoUrl: PLATFORM_EMAIL_LOGO_URL } : {}),\n logoAlt: PLATFORM_BRAND_NAME,\n ...(PLATFORM_HOME_URL ? { href: PLATFORM_HOME_URL } : {}),\n },\n footer: { ...footer, legal },\n }\n }\n\n const logoUrl = clean(brandLogoUrl)\n const home = clean(brandHomeUrl)\n return {\n header: {\n ...(logoUrl ? { logoUrl } : {}),\n logoAlt: merged['brand.productName'] ?? '',\n ...(home && /^https?:\\/\\//i.test(home) ? { href: home } : {}),\n },\n footer,\n }\n}\n"],"names":["brandMergeTokens","PLATFORM_BRANDING_PROFILE","PLATFORM_BRAND_LEGAL_NAME","PLATFORM_BRAND_NAME","PLATFORM_EMAIL_LOGO_URL","PLATFORM_HOME_URL","PLATFORM_POSTAL_ADDRESS","PLATFORM_BRAND_MERGE_TOKENS","isPlatformBrandedSend","merged","options","String","brandLogoUrl","trim","Object","entries","every","token","value","clean","text","length","undefined","supportLink","url","href","test","address","slice","split","label","buildSystemEmailChrome","input","definition","brandHomeUrl","now","Date","reason","footerReason","support","footer","legal","getUTCFullYear","header","logoUrl","logoAlt","home"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC;AAQD,SAASA,gBAAgB,QAAQ,yBAAqB;AACtD,SACEC,yBAAyB,EACzBC,yBAAyB,EACzBC,mBAAmB,EACnBC,uBAAuB,EACvBC,iBAAiB,EACjBC,uBAAuB,QAClB,sBAAkB;AAEzB;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMC,8BACXP,iBAAiBC,2BAA0B;AA0B7C;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASO,sBACdC,MAAwC,EACxCC,UAAmC,CAAC,CAAC;QAE1BA;IAAX,IAAIC,QAAOD,wBAAAA,QAAQE,YAAY,YAApBF,wBAAwB,IAAIG,IAAI,IAAI,OAAO;IACtD,OAAOC,OAAOC,OAAO,CAACR,6BAA6BS,KAAK,CACtD,CAAC,CAACC,OAAOC,MAAM,GAAKT,MAAM,CAACQ,MAAM,KAAKC;AAE1C;AAeA,gDAAgD,GAChD,MAAMC,QAAQ,CAACD;IACb,MAAME,OAAO,OAAOF,UAAU,WAAWA,MAAML,IAAI,KAAK;IACxD,OAAOO,KAAKC,MAAM,GAAGD,OAAOE;AAC9B;AAEA;;;;;;;;;CASC,GACD,SAASC,YACPC,GAAuB;IAEvB,MAAMC,OAAON,MAAMK;IACnB,IAAI,CAACC,MAAM,OAAOH;IAClB,IAAI,YAAYI,IAAI,CAACD,OAAO;QAC1B,MAAME,UAAUR,MAAMM,KAAKG,KAAK,CAAC,UAAUP,MAAM,EAAEQ,KAAK,CAAC,IAAI,CAAC,EAAE;QAChE,OAAOF,UAAU;YAAEG,OAAOH;YAASF;QAAK,IAAIH;IAC9C;IACA,OAAO;QAAEQ,OAAO;QAAYL;IAAK;AACnC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASM,uBACdC,KAA6B;QAGjBA,YA2BCvB;IA5Bb,MAAM,EAAEwB,UAAU,EAAExB,MAAM,EAAEG,YAAY,EAAEsB,YAAY,EAAE,GAAGF;IAC3D,MAAMG,OAAMH,aAAAA,MAAMG,GAAG,YAATH,aAAc,IAAM,IAAII;IACpC,MAAMC,SAASlB,MAAMc,WAAWK,YAAY;IAC5C,MAAMC,UAAUhB,YAAYd,MAAM,CAAC,mBAAmB;IACtD,MAAM+B,SAA4B,aAC5BH,SAAS;QAAEA;IAAO,IAAI,CAAC,GACvBE,UAAU;QAAEA;IAAQ,IAAI,CAAC;IAG/B,IAAI/B,sBAAsBC,QAAQ;QAAEG;IAAa,IAAI;QACnD,MAAM6B,QACJ,CAAC,EAAE,EAAEN,MAAMO,cAAc,GAAG,CAAC,EAAExC,2BAA2B,GACzDI,CAAAA,0BAA0B,CAAC,GAAG,EAAEA,yBAAyB,GAAG,EAAC;QAChE,OAAO;YACLqC,QAAQ,aACFvC,0BAA0B;gBAAEwC,SAASxC;YAAwB,IAAI,CAAC;gBACtEyC,SAAS1C;eACLE,oBAAoB;gBAAEoB,MAAMpB;YAAkB,IAAI,CAAC;YAEzDmC,QAAQ,aAAKA;gBAAQC;;QACvB;IACF;IAEA,MAAMG,UAAUzB,MAAMP;IACtB,MAAMkC,OAAO3B,MAAMe;IACnB,OAAO;QACLS,QAAQ,aACFC,UAAU;YAAEA;QAAQ,IAAI,CAAC;YAC7BC,OAAO,GAAEpC,2BAAAA,MAAM,CAAC,oBAAoB,YAA3BA,2BAA+B;WACpCqC,QAAQ,gBAAgBpB,IAAI,CAACoB,QAAQ;YAAErB,MAAMqB;QAAK,IAAI,CAAC;QAE7DN;IACF;AACF"}