@aglyn/tenant-data-admin 1.0.0-beta.214 → 1.0.0-beta.216

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/tenant-data-admin",
3
- "version": "1.0.0-beta.214",
3
+ "version": "1.0.0-beta.216",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,12 +25,12 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.214",
29
- "@aglyn/shared-util-email": "1.0.0-beta.214",
30
- "@aglyn/shared-util-fbserver": "1.0.0-beta.214",
31
- "@aglyn/shared-util-first-touch": "1.0.0-beta.214",
32
- "@aglyn/shared-util-http": "1.0.0-beta.214",
33
- "@aglyn/shared-util-tools": "1.0.0-beta.214",
28
+ "@aglyn/aglyn": "1.0.0-beta.216",
29
+ "@aglyn/shared-util-email": "1.0.0-beta.216",
30
+ "@aglyn/shared-util-fbserver": "1.0.0-beta.216",
31
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.216",
32
+ "@aglyn/shared-util-http": "1.0.0-beta.216",
33
+ "@aglyn/shared-util-tools": "1.0.0-beta.216",
34
34
  "@msgpack/msgpack": "^3.1.3",
35
35
  "@swc/helpers": "0.5.23",
36
36
  "sharp": "^0.35.3",
@@ -14,7 +14,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { sanitizeAuthorHtml } from "@aglyn/aglyn/app-utils/author-html";
17
+ */ import { resolveSiteTheme } from "@aglyn/aglyn/app-utils/marketplace-theme";
18
+ import { sanitizeAuthorHtml } from "@aglyn/aglyn/app-utils/author-html";
18
19
  import { hostTokenMerge } from "@aglyn/aglyn/app-utils/host-tokens";
19
20
  import { composeHostComponentNodes } from "@aglyn/aglyn/app-utils/load-referenced-components";
20
21
  import { buildSiteEmailChrome } from "@aglyn/aglyn/app-utils/site-email-chrome";
@@ -94,12 +95,16 @@ import { buildEmailPalette, emailPaletteBaseForHost, getTenantEmail, hostEmailOr
94
95
  * same base the site itself does.
95
96
  */ function siteThemeFor(host) {
96
97
  var _ref;
97
- var _host_theme_shape, _host_theme, _host_theme_colorSchemes, _host_theme1;
98
- const radius = host == null ? void 0 : (_host_theme = host.theme) == null ? void 0 : (_host_theme_shape = _host_theme.shape) == null ? void 0 : _host_theme_shape.borderRadius;
98
+ var _theme_shape, _theme_colorSchemes;
99
+ // Resolved, not read off `theme`: a site's edits are an override on the
100
+ // theme it picked (AGL-3404), so the stored theme alone is the design
101
+ // before anything the site changed.
102
+ const theme = resolveSiteTheme(host);
103
+ const radius = theme == null ? void 0 : (_theme_shape = theme.shape) == null ? void 0 : _theme_shape.borderRadius;
99
104
  return _extends({
100
105
  palette: buildEmailPalette({
101
106
  base: emailPaletteBaseForHost(host),
102
- colors: (_ref = host == null ? void 0 : (_host_theme1 = host.theme) == null ? void 0 : (_host_theme_colorSchemes = _host_theme1.colorSchemes) == null ? void 0 : _host_theme_colorSchemes.light) != null ? _ref : null
107
+ colors: (_ref = theme == null ? void 0 : (_theme_colorSchemes = theme.colorSchemes) == null ? void 0 : _theme_colorSchemes.light) != null ? _ref : null
103
108
  })
104
109
  }, typeof radius === 'number' ? {
105
110
  buttonRadius: radius
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/host-email-tokens.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { sanitizeAuthorHtml } from '@aglyn/aglyn/app-utils/author-html'\nimport {\n hostTokenMerge,\n type HostTokenSource,\n} from '@aglyn/aglyn/app-utils/host-tokens'\nimport { composeHostComponentNodes } from '@aglyn/aglyn/app-utils/load-referenced-components'\nimport { buildSiteEmailChrome } from '@aglyn/aglyn/app-utils/site-email-chrome'\nimport {\n buildEmailPalette,\n emailPaletteBaseForHost,\n getTenantEmail,\n hostEmailOrigin,\n loadHostEmail,\n renderFramedTextEmail,\n renderHostEmail,\n renderLoadedHostEmail,\n type EmailChrome,\n type EmailTheme,\n type LoadedHostEmail,\n type RenderedHostEmail,\n} from '@aglyn/shared-util-email'\nimport { firebaseAdmin } from './firebase-admin'\n\n/**\n * `renderHostEmail` with `host.*` tokens resolved (AGL-1022).\n *\n * The whole point of host variables is that a publisher's email template can\n * say \"write to {{host.supportEmail}}\" and have the installing site fill it in.\n * That only happens if something resolves the tokens at SEND time, and the\n * email library cannot: `@aglyn/shared-util-email` is `scope:shared` and may\n * only depend on `scope:shared`, while the token registry is aglyn-scoped.\n *\n * So the join happens here, in the one lib that already sits above both. Three\n * alternatives were weighed and this is the one that cannot fail quietly:\n *\n * * A registration hook inside the email library would mean a provider that is\n * never registered resolves every token to nothing, silently, on a path whose\n * output has already been sent to a customer. This repo has shipped a\n * flawless security control with zero callers before; that is the shape.\n * * Threading the map through all eighteen call sites puts the burden on\n * whoever adds the nineteenth, and they will not know.\n * * Moving the registry into a `scope:shared` lib is the cleanest end state and\n * the right thing if a second shared consumer ever appears — but the only\n * candidate today is `shared/util/tools`, which is generic helpers, and\n * domain logic does not belong there.\n *\n * Costs one host read per send. Emails are not a hot path, and the read buys\n * the guarantee that a template's branding is right by construction.\n */\nexport async function renderHostEmailWithTokens(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n templateKey: string,\n merge: Record<string, string> = {},\n options: { origin?: string } = {},\n): Promise<RenderedHostEmail | null> {\n const host = await readHostTokenSource(firestore, hostId)\n return renderHostEmail(\n firestore as never,\n hostId,\n templateKey,\n // Host tokens first: an explicit caller value WINS. A sender passing\n // `host.businessName` is naming a specific thing on purpose (a white-label\n // send, a preview with sample data), and this is not the place to overrule\n // it.\n { ...hostTokenMerge(host), ...merge },\n // The HTML policy joins here for the same reason the token map does, and\n // it is the same reason: `renderEmailHtml` requires one, `scope:shared`\n // cannot import one, and this lib already sits above both. Supplying it\n // here rather than at each of the nine senders keeps their signatures\n // untouched and gives the tenth one the right policy without knowing it\n // needed to ask.\n //\n // The component graft joins on the same argument (AGL-3287): a header or\n // footer the site owner placed is a reference the shared renderer cannot\n // resolve, so every sender behind this function mails it expanded without\n // having to know it exists. Style overrides do not survive into mail —\n // they land in `sx`, which `renderEmailHtml` never reads — but property\n // values and attribute overrides do.\n //\n // And the site's header and footer (AGL-3370), which the built-in copy is\n // drawn inside; a design the owner published goes out as they built it.\n {\n ...options,\n sanitize: sanitizeAuthorHtml,\n compose: composeHostComponentNodes,\n chrome: siteChromeFor(host, templateKey),\n theme: siteThemeFor(host),\n },\n )\n}\n\n/**\n * The site's host document as the token and chrome readers see it, or null.\n *\n * A failed read must not stop the send. Every token then resolves to its\n * empty behaviour, which is the same outcome as a site that set nothing —\n * an email with a gap where the address would be, rather than no email.\n */\nasync function readHostTokenSource(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<SiteEmailHost | null> {\n try {\n const snapshot = await firestore.collection('hosts').doc(hostId).get()\n return snapshot.exists ? (snapshot.data() as SiteEmailHost) : null\n } catch {\n return null\n }\n}\n\n/** What a site email reads off the host document: its tokens and its theme. */\ntype SiteEmailHost = HostTokenSource & {\n theme?: {\n colorSchemes?: { light?: Record<string, unknown> | null }\n shape?: { borderRadius?: unknown }\n } | null\n}\n\n/**\n * The site's theme as its email draws it (AGL-3370): the palette the\n * published site renders with, light scheme, since an inbox has one, and its\n * corner radius for buttons. A site with no theme of its own resolves the\n * same base the site itself does.\n */\nfunction siteThemeFor(host: SiteEmailHost | null): EmailTheme {\n const radius = host?.theme?.shape?.borderRadius\n return {\n palette: buildEmailPalette({\n base: emailPaletteBaseForHost(host),\n colors: host?.theme?.colorSchemes?.light ?? null,\n }),\n ...(typeof radius === 'number' ? { buttonRadius: radius } : {}),\n }\n}\n\n/** The site's chrome for one kind of email, with that kind's footer reason. */\nfunction siteChromeFor(\n host: HostTokenSource | null,\n templateKey: string,\n): EmailChrome {\n return buildSiteEmailChrome({\n host,\n reason: getTenantEmail(templateKey)?.footerReason,\n })\n}\n\n/** A site email loaded once for a batch, with the site's tokens beside it. */\nexport interface LoadedHostEmailWithTokens {\n loaded: LoadedHostEmail\n hostMerge: Record<string, string>\n}\n\n/**\n * {@link loadHostEmail} for a batch (AGL-3370): the template, or the built-in\n * copy in the site's header and footer, read ONCE, with the site's `host.*`\n * tokens beside it for {@link renderLoadedHostEmailWithTokens}. `null` only\n * for a key with neither, so the batch keeps its own copy.\n */\nexport async function loadHostEmailWithTokens(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n templateKey: string,\n options: { origin?: string } = {},\n): Promise<LoadedHostEmailWithTokens | null> {\n const host = await readHostTokenSource(firestore, hostId)\n const loaded = await loadHostEmail(firestore as never, hostId, templateKey, {\n ...options,\n compose: composeHostComponentNodes,\n chrome: siteChromeFor(host, templateKey),\n theme: siteThemeFor(host),\n })\n return loaded ? { loaded, hostMerge: hostTokenMerge(host) } : null\n}\n\n/** One recipient's copy of a batch-loaded site email; no reads. */\nexport function renderLoadedHostEmailWithTokens(\n bundle: LoadedHostEmailWithTokens,\n merge: Record<string, string> = {},\n): RenderedHostEmail | null {\n return renderLoadedHostEmail(\n bundle.loaded,\n { ...bundle.hostMerge, ...merge },\n sanitizeAuthorHtml,\n )\n}\n\n/**\n * The site's header and footer for one kind of email, its theme, and the\n * origin and host a stored logo reference resolves against (AGL-3370): for a sender\n * that renders per recipient itself, such as a typed campaign.\n */\nexport async function siteEmailFrame(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n templateKey: string,\n): Promise<{\n chrome: EmailChrome\n theme: EmailTheme\n mediaOrigin?: string\n mediaHostId: string\n}> {\n const host = await readHostTokenSource(firestore, hostId)\n const origin = host\n ? hostEmailOrigin({ cname: host.cname, subdomain: host.subdomain })\n : undefined\n return {\n chrome: siteChromeFor(host, templateKey),\n theme: siteThemeFor(host),\n ...(origin ? { mediaOrigin: origin } : {}),\n mediaHostId: hostId,\n }\n}\n\n/**\n * A text-only site email in the site's header and footer (AGL-3370): a\n * workflow's email step, or a message a site sends in words nobody designed.\n * The text arrives resolved; the footer's reason is the kind's, by key.\n */\nexport async function renderSiteTextEmail(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n templateKey: string,\n message: { subject: string; text: string; preheader?: string },\n): Promise<{ subject: string; html: string; text: string }> {\n const frame = await siteEmailFrame(firestore, hostId, templateKey)\n const rendered = renderFramedTextEmail({ ...message, ...frame })\n return { subject: message.subject, html: rendered.html, text: rendered.text }\n}\n\nexport default renderHostEmailWithTokens\n"],"names":["sanitizeAuthorHtml","hostTokenMerge","composeHostComponentNodes","buildSiteEmailChrome","buildEmailPalette","emailPaletteBaseForHost","getTenantEmail","hostEmailOrigin","loadHostEmail","renderFramedTextEmail","renderHostEmail","renderLoadedHostEmail","renderHostEmailWithTokens","firestore","hostId","templateKey","merge","options","host","readHostTokenSource","sanitize","compose","chrome","siteChromeFor","theme","siteThemeFor","snapshot","collection","doc","get","exists","data","radius","shape","borderRadius","palette","base","colors","colorSchemes","light","buttonRadius","reason","footerReason","loadHostEmailWithTokens","loaded","hostMerge","renderLoadedHostEmailWithTokens","bundle","siteEmailFrame","origin","cname","subdomain","undefined","mediaOrigin","mediaHostId","renderSiteTextEmail","message","frame","rendered","subject","html","text"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,qCAAoC;AACvE,SACEC,cAAc,QAET,qCAAoC;AAC3C,SAASC,yBAAyB,QAAQ,oDAAmD;AAC7F,SAASC,oBAAoB,QAAQ,2CAA0C;AAC/E,SACEC,iBAAiB,EACjBC,uBAAuB,EACvBC,cAAc,EACdC,eAAe,EACfC,aAAa,EACbC,qBAAqB,EACrBC,eAAe,EACfC,qBAAqB,QAKhB,2BAA0B;AAGjC;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,eAAeC,0BACpBC,SAAsC,EACtCC,MAAc,EACdC,WAAmB,EACnBC,QAAgC,CAAC,CAAC,EAClCC,UAA+B,CAAC,CAAC;IAEjC,MAAMC,OAAO,MAAMC,oBAAoBN,WAAWC;IAClD,OAAOJ,gBACLG,WACAC,QACAC,aACA,qEAAqE;IACrE,2EAA2E;IAC3E,2EAA2E;IAC3E,MAAM;IACN,aAAKd,eAAeiB,OAAUF,QAC9B,yEAAyE;IACzE,wEAAwE;IACxE,wEAAwE;IACxE,sEAAsE;IACtE,wEAAwE;IACxE,iBAAiB;IACjB,EAAE;IACF,yEAAyE;IACzE,yEAAyE;IACzE,0EAA0E;IAC1E,uEAAuE;IACvE,wEAAwE;IACxE,qCAAqC;IACrC,EAAE;IACF,0EAA0E;IAC1E,wEAAwE;IACxE,aACKC;QACHG,UAAUpB;QACVqB,SAASnB;QACToB,QAAQC,cAAcL,MAAMH;QAC5BS,OAAOC,aAAaP;;AAG1B;AAEA;;;;;;CAMC,GACD,eAAeC,oBACbN,SAAsC,EACtCC,MAAc;IAEd,IAAI;QACF,MAAMY,WAAW,MAAMb,UAAUc,UAAU,CAAC,SAASC,GAAG,CAACd,QAAQe,GAAG;QACpE,OAAOH,SAASI,MAAM,GAAIJ,SAASK,IAAI,KAAuB;IAChE,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAUA;;;;;CAKC,GACD,SAASN,aAAaP,IAA0B;;QAC/BA,mBAAAA,aAIHA,0BAAAA;IAJZ,MAAMc,SAASd,yBAAAA,cAAAA,KAAMM,KAAK,sBAAXN,oBAAAA,YAAae,KAAK,qBAAlBf,kBAAoBgB,YAAY;IAC/C,OAAO;QACLC,SAAS/B,kBAAkB;YACzBgC,MAAM/B,wBAAwBa;YAC9BmB,MAAM,UAAEnB,yBAAAA,eAAAA,KAAMM,KAAK,sBAAXN,2BAAAA,aAAaoB,YAAY,qBAAzBpB,yBAA2BqB,KAAK,mBAAI;QAC9C;OACI,OAAOP,WAAW,WAAW;QAAEQ,cAAcR;IAAO,IAAI,CAAC;AAEjE;AAEA,6EAA6E,GAC7E,SAAST,cACPL,IAA4B,EAC5BH,WAAmB;QAITT;IAFV,OAAOH,qBAAqB;QAC1Be;QACAuB,MAAM,GAAEnC,kBAAAA,eAAeS,iCAAfT,gBAA6BoC,YAAY;IACnD;AACF;AAQA;;;;;CAKC,GACD,OAAO,eAAeC,wBACpB9B,SAAsC,EACtCC,MAAc,EACdC,WAAmB,EACnBE,UAA+B,CAAC,CAAC;IAEjC,MAAMC,OAAO,MAAMC,oBAAoBN,WAAWC;IAClD,MAAM8B,SAAS,MAAMpC,cAAcK,WAAoBC,QAAQC,aAAa,aACvEE;QACHI,SAASnB;QACToB,QAAQC,cAAcL,MAAMH;QAC5BS,OAAOC,aAAaP;;IAEtB,OAAO0B,SAAS;QAAEA;QAAQC,WAAW5C,eAAeiB;IAAM,IAAI;AAChE;AAEA,iEAAiE,GACjE,OAAO,SAAS4B,gCACdC,MAAiC,EACjC/B,QAAgC,CAAC,CAAC;IAElC,OAAOL,sBACLoC,OAAOH,MAAM,EACb,aAAKG,OAAOF,SAAS,EAAK7B,QAC1BhB;AAEJ;AAEA;;;;CAIC,GACD,OAAO,eAAegD,eACpBnC,SAAsC,EACtCC,MAAc,EACdC,WAAmB;IAOnB,MAAMG,OAAO,MAAMC,oBAAoBN,WAAWC;IAClD,MAAMmC,SAAS/B,OACXX,gBAAgB;QAAE2C,OAAOhC,KAAKgC,KAAK;QAAEC,WAAWjC,KAAKiC,SAAS;IAAC,KAC/DC;IACJ,OAAO;QACL9B,QAAQC,cAAcL,MAAMH;QAC5BS,OAAOC,aAAaP;OAChB+B,SAAS;QAAEI,aAAaJ;IAAO,IAAI,CAAC;QACxCK,aAAaxC;;AAEjB;AAEA;;;;CAIC,GACD,OAAO,eAAeyC,oBACpB1C,SAAsC,EACtCC,MAAc,EACdC,WAAmB,EACnByC,OAA8D;IAE9D,MAAMC,QAAQ,MAAMT,eAAenC,WAAWC,QAAQC;IACtD,MAAM2C,WAAWjD,sBAAsB,aAAK+C,SAAYC;IACxD,OAAO;QAAEE,SAASH,QAAQG,OAAO;QAAEC,MAAMF,SAASE,IAAI;QAAEC,MAAMH,SAASG,IAAI;IAAC;AAC9E;AAEA,eAAejD,0BAAyB"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/host-email-tokens.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n resolveSiteTheme,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport { sanitizeAuthorHtml } from '@aglyn/aglyn/app-utils/author-html'\nimport {\n hostTokenMerge,\n type HostTokenSource,\n} from '@aglyn/aglyn/app-utils/host-tokens'\nimport { composeHostComponentNodes } from '@aglyn/aglyn/app-utils/load-referenced-components'\nimport { buildSiteEmailChrome } from '@aglyn/aglyn/app-utils/site-email-chrome'\nimport {\n buildEmailPalette,\n emailPaletteBaseForHost,\n getTenantEmail,\n hostEmailOrigin,\n loadHostEmail,\n renderFramedTextEmail,\n renderHostEmail,\n renderLoadedHostEmail,\n type EmailChrome,\n type EmailTheme,\n type LoadedHostEmail,\n type RenderedHostEmail,\n} from '@aglyn/shared-util-email'\nimport { firebaseAdmin } from './firebase-admin'\n\n/**\n * `renderHostEmail` with `host.*` tokens resolved (AGL-1022).\n *\n * The whole point of host variables is that a publisher's email template can\n * say \"write to {{host.supportEmail}}\" and have the installing site fill it in.\n * That only happens if something resolves the tokens at SEND time, and the\n * email library cannot: `@aglyn/shared-util-email` is `scope:shared` and may\n * only depend on `scope:shared`, while the token registry is aglyn-scoped.\n *\n * So the join happens here, in the one lib that already sits above both. Three\n * alternatives were weighed and this is the one that cannot fail quietly:\n *\n * * A registration hook inside the email library would mean a provider that is\n * never registered resolves every token to nothing, silently, on a path whose\n * output has already been sent to a customer. This repo has shipped a\n * flawless security control with zero callers before; that is the shape.\n * * Threading the map through all eighteen call sites puts the burden on\n * whoever adds the nineteenth, and they will not know.\n * * Moving the registry into a `scope:shared` lib is the cleanest end state and\n * the right thing if a second shared consumer ever appears — but the only\n * candidate today is `shared/util/tools`, which is generic helpers, and\n * domain logic does not belong there.\n *\n * Costs one host read per send. Emails are not a hot path, and the read buys\n * the guarantee that a template's branding is right by construction.\n */\nexport async function renderHostEmailWithTokens(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n templateKey: string,\n merge: Record<string, string> = {},\n options: { origin?: string } = {},\n): Promise<RenderedHostEmail | null> {\n const host = await readHostTokenSource(firestore, hostId)\n return renderHostEmail(\n firestore as never,\n hostId,\n templateKey,\n // Host tokens first: an explicit caller value WINS. A sender passing\n // `host.businessName` is naming a specific thing on purpose (a white-label\n // send, a preview with sample data), and this is not the place to overrule\n // it.\n { ...hostTokenMerge(host), ...merge },\n // The HTML policy joins here for the same reason the token map does, and\n // it is the same reason: `renderEmailHtml` requires one, `scope:shared`\n // cannot import one, and this lib already sits above both. Supplying it\n // here rather than at each of the nine senders keeps their signatures\n // untouched and gives the tenth one the right policy without knowing it\n // needed to ask.\n //\n // The component graft joins on the same argument (AGL-3287): a header or\n // footer the site owner placed is a reference the shared renderer cannot\n // resolve, so every sender behind this function mails it expanded without\n // having to know it exists. Style overrides do not survive into mail —\n // they land in `sx`, which `renderEmailHtml` never reads — but property\n // values and attribute overrides do.\n //\n // And the site's header and footer (AGL-3370), which the built-in copy is\n // drawn inside; a design the owner published goes out as they built it.\n {\n ...options,\n sanitize: sanitizeAuthorHtml,\n compose: composeHostComponentNodes,\n chrome: siteChromeFor(host, templateKey),\n theme: siteThemeFor(host),\n },\n )\n}\n\n/**\n * The site's host document as the token and chrome readers see it, or null.\n *\n * A failed read must not stop the send. Every token then resolves to its\n * empty behaviour, which is the same outcome as a site that set nothing —\n * an email with a gap where the address would be, rather than no email.\n */\nasync function readHostTokenSource(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<SiteEmailHost | null> {\n try {\n const snapshot = await firestore.collection('hosts').doc(hostId).get()\n return snapshot.exists ? (snapshot.data() as SiteEmailHost) : null\n } catch {\n return null\n }\n}\n\n/** What a site email reads off the host document: its tokens and its theme. */\ntype SiteEmailHost = HostTokenSource & {\n theme?: {\n colorSchemes?: { light?: Record<string, unknown> | null }\n shape?: { borderRadius?: unknown }\n } | null\n themeOverride?: unknown\n}\n\n/**\n * The site's theme as its email draws it (AGL-3370): the palette the\n * published site renders with, light scheme, since an inbox has one, and its\n * corner radius for buttons. A site with no theme of its own resolves the\n * same base the site itself does.\n */\nfunction siteThemeFor(host: SiteEmailHost | null): EmailTheme {\n // Resolved, not read off `theme`: a site's edits are an override on the\n // theme it picked (AGL-3404), so the stored theme alone is the design\n // before anything the site changed.\n const theme = resolveSiteTheme(host as ThemeHostDocument | null)\n const radius = theme?.shape?.borderRadius\n return {\n palette: buildEmailPalette({\n base: emailPaletteBaseForHost(host),\n colors: (theme?.colorSchemes?.light ?? null) as Record<string, unknown> | null,\n }),\n ...(typeof radius === 'number' ? { buttonRadius: radius } : {}),\n }\n}\n\n/** The site's chrome for one kind of email, with that kind's footer reason. */\nfunction siteChromeFor(\n host: HostTokenSource | null,\n templateKey: string,\n): EmailChrome {\n return buildSiteEmailChrome({\n host,\n reason: getTenantEmail(templateKey)?.footerReason,\n })\n}\n\n/** A site email loaded once for a batch, with the site's tokens beside it. */\nexport interface LoadedHostEmailWithTokens {\n loaded: LoadedHostEmail\n hostMerge: Record<string, string>\n}\n\n/**\n * {@link loadHostEmail} for a batch (AGL-3370): the template, or the built-in\n * copy in the site's header and footer, read ONCE, with the site's `host.*`\n * tokens beside it for {@link renderLoadedHostEmailWithTokens}. `null` only\n * for a key with neither, so the batch keeps its own copy.\n */\nexport async function loadHostEmailWithTokens(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n templateKey: string,\n options: { origin?: string } = {},\n): Promise<LoadedHostEmailWithTokens | null> {\n const host = await readHostTokenSource(firestore, hostId)\n const loaded = await loadHostEmail(firestore as never, hostId, templateKey, {\n ...options,\n compose: composeHostComponentNodes,\n chrome: siteChromeFor(host, templateKey),\n theme: siteThemeFor(host),\n })\n return loaded ? { loaded, hostMerge: hostTokenMerge(host) } : null\n}\n\n/** One recipient's copy of a batch-loaded site email; no reads. */\nexport function renderLoadedHostEmailWithTokens(\n bundle: LoadedHostEmailWithTokens,\n merge: Record<string, string> = {},\n): RenderedHostEmail | null {\n return renderLoadedHostEmail(\n bundle.loaded,\n { ...bundle.hostMerge, ...merge },\n sanitizeAuthorHtml,\n )\n}\n\n/**\n * The site's header and footer for one kind of email, its theme, and the\n * origin and host a stored logo reference resolves against (AGL-3370): for a sender\n * that renders per recipient itself, such as a typed campaign.\n */\nexport async function siteEmailFrame(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n templateKey: string,\n): Promise<{\n chrome: EmailChrome\n theme: EmailTheme\n mediaOrigin?: string\n mediaHostId: string\n}> {\n const host = await readHostTokenSource(firestore, hostId)\n const origin = host\n ? hostEmailOrigin({ cname: host.cname, subdomain: host.subdomain })\n : undefined\n return {\n chrome: siteChromeFor(host, templateKey),\n theme: siteThemeFor(host),\n ...(origin ? { mediaOrigin: origin } : {}),\n mediaHostId: hostId,\n }\n}\n\n/**\n * A text-only site email in the site's header and footer (AGL-3370): a\n * workflow's email step, or a message a site sends in words nobody designed.\n * The text arrives resolved; the footer's reason is the kind's, by key.\n */\nexport async function renderSiteTextEmail(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n templateKey: string,\n message: { subject: string; text: string; preheader?: string },\n): Promise<{ subject: string; html: string; text: string }> {\n const frame = await siteEmailFrame(firestore, hostId, templateKey)\n const rendered = renderFramedTextEmail({ ...message, ...frame })\n return { subject: message.subject, html: rendered.html, text: rendered.text }\n}\n\nexport default renderHostEmailWithTokens\n"],"names":["resolveSiteTheme","sanitizeAuthorHtml","hostTokenMerge","composeHostComponentNodes","buildSiteEmailChrome","buildEmailPalette","emailPaletteBaseForHost","getTenantEmail","hostEmailOrigin","loadHostEmail","renderFramedTextEmail","renderHostEmail","renderLoadedHostEmail","renderHostEmailWithTokens","firestore","hostId","templateKey","merge","options","host","readHostTokenSource","sanitize","compose","chrome","siteChromeFor","theme","siteThemeFor","snapshot","collection","doc","get","exists","data","radius","shape","borderRadius","palette","base","colors","colorSchemes","light","buttonRadius","reason","footerReason","loadHostEmailWithTokens","loaded","hostMerge","renderLoadedHostEmailWithTokens","bundle","siteEmailFrame","origin","cname","subdomain","undefined","mediaOrigin","mediaHostId","renderSiteTextEmail","message","frame","rendered","subject","html","text"],"mappings":"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"}
@@ -0,0 +1,36 @@
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
+ import { type ThemeLibraryAction, type ThemeLibraryPlan } from '@aglyn/aglyn/app-utils/theme-library';
18
+ import type { DocumentReference, Transaction } from 'firebase-admin/firestore';
19
+ /**
20
+ * Plans a theme-library action against the host and its library as read
21
+ * INSIDE `tx`, and writes the plan in the same transaction (AGL-3404).
22
+ *
23
+ * One executor for every route that changes which theme a site runs — the
24
+ * console's library route and the marketplace install — so a switch cannot
25
+ * be filed one way by one of them and another way by the other.
26
+ *
27
+ * Reading in the transaction is the point: the console's copy of the host is
28
+ * a listener that can be serving a cache (AGL-1358), and a switch planned from
29
+ * a stale override would stash the wrong edits.
30
+ *
31
+ * Host fields are written with `update`, which replaces each named field
32
+ * wholesale. A deep merge would union two override patches and keep the
33
+ * tokens of the theme being replaced. Existing entries are updated the same
34
+ * way; new ones are created whole.
35
+ */
36
+ export declare function runThemeLibraryAction(tx: Transaction, hostRef: DocumentReference, action: ThemeLibraryAction, extra?: Record<string, unknown>): Promise<ThemeLibraryPlan>;
@@ -0,0 +1,79 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
18
+ import { planThemeLibraryAction, THEME_FIELD_DELETE, THEME_LIBRARY_COLLECTION } from "@aglyn/aglyn/app-utils/theme-library";
19
+ import firebaseAdmin from "./firebase-admin.js";
20
+ /**
21
+ * Plans a theme-library action against the host and its library as read
22
+ * INSIDE `tx`, and writes the plan in the same transaction (AGL-3404).
23
+ *
24
+ * One executor for every route that changes which theme a site runs — the
25
+ * console's library route and the marketplace install — so a switch cannot
26
+ * be filed one way by one of them and another way by the other.
27
+ *
28
+ * Reading in the transaction is the point: the console's copy of the host is
29
+ * a listener that can be serving a cache (AGL-1358), and a switch planned from
30
+ * a stale override would stash the wrong edits.
31
+ *
32
+ * Host fields are written with `update`, which replaces each named field
33
+ * wholesale. A deep merge would union two override patches and keep the
34
+ * tokens of the theme being replaced. Existing entries are updated the same
35
+ * way; new ones are created whole.
36
+ */ export async function runThemeLibraryAction(tx, hostRef, action, extra) {
37
+ var _hostSnapshot_data;
38
+ const library = hostRef.collection(THEME_LIBRARY_COLLECTION);
39
+ const [hostSnapshot, entriesSnapshot] = await Promise.all([
40
+ tx.get(hostRef),
41
+ tx.get(library)
42
+ ]);
43
+ const entries = {};
44
+ for (const doc of entriesSnapshot.docs){
45
+ entries[doc.id] = doc.data();
46
+ }
47
+ const plan = planThemeLibraryAction({
48
+ host: (_hostSnapshot_data = hostSnapshot.data()) != null ? _hostSnapshot_data : {},
49
+ entries,
50
+ action,
51
+ newId: createResourceUid
52
+ });
53
+ if (plan.ok === false) return plan;
54
+ const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
55
+ const stored = (fields)=>Object.fromEntries(Object.entries(fields).map(([key, value])=>[
56
+ key,
57
+ value === THEME_FIELD_DELETE ? firebaseAdmin.firestore.FieldValue.delete() : value
58
+ ]));
59
+ if (Object.keys(plan.host).length || extra) {
60
+ tx.update(hostRef, _extends({}, stored(plan.host), extra, {
61
+ updatedAt: now
62
+ }));
63
+ }
64
+ for (const write of plan.entries){
65
+ const ref = library.doc(write.id);
66
+ if (write.op === 'delete') tx.delete(ref);
67
+ else if (write.op === 'create') {
68
+ tx.set(ref, _extends({}, write.data, {
69
+ createdAt: now,
70
+ updatedAt: now
71
+ }));
72
+ } else tx.update(ref, _extends({}, stored(write.data), {
73
+ updatedAt: now
74
+ }));
75
+ }
76
+ return plan;
77
+ }
78
+
79
+ //# sourceMappingURL=theme-library-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/theme-library-write.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport {\n planThemeLibraryAction,\n THEME_FIELD_DELETE,\n THEME_LIBRARY_COLLECTION,\n type ThemeLibraryAction,\n type ThemeLibraryEntry,\n type ThemeLibraryPlan,\n} from '@aglyn/aglyn/app-utils/theme-library'\nimport type { DocumentReference, Transaction } from 'firebase-admin/firestore'\nimport firebaseAdmin from './firebase-admin'\n\n/**\n * Plans a theme-library action against the host and its library as read\n * INSIDE `tx`, and writes the plan in the same transaction (AGL-3404).\n *\n * One executor for every route that changes which theme a site runs — the\n * console's library route and the marketplace install — so a switch cannot\n * be filed one way by one of them and another way by the other.\n *\n * Reading in the transaction is the point: the console's copy of the host is\n * a listener that can be serving a cache (AGL-1358), and a switch planned from\n * a stale override would stash the wrong edits.\n *\n * Host fields are written with `update`, which replaces each named field\n * wholesale. A deep merge would union two override patches and keep the\n * tokens of the theme being replaced. Existing entries are updated the same\n * way; new ones are created whole.\n */\nexport async function runThemeLibraryAction(\n tx: Transaction,\n hostRef: DocumentReference,\n action: ThemeLibraryAction,\n extra?: Record<string, unknown>,\n): Promise<ThemeLibraryPlan> {\n const library = hostRef.collection(THEME_LIBRARY_COLLECTION)\n const [hostSnapshot, entriesSnapshot] = await Promise.all([\n tx.get(hostRef),\n tx.get(library),\n ])\n const entries: Record<string, ThemeLibraryEntry> = {}\n for (const doc of entriesSnapshot.docs) {\n entries[doc.id] = doc.data() as ThemeLibraryEntry\n }\n const plan = planThemeLibraryAction({\n host: hostSnapshot.data() ?? {},\n entries,\n action,\n newId: createResourceUid,\n })\n if (plan.ok === false) return plan\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const stored = (fields: Record<string, unknown>) =>\n Object.fromEntries(\n Object.entries(fields).map(([key, value]) => [\n key,\n value === THEME_FIELD_DELETE\n ? firebaseAdmin.firestore.FieldValue.delete()\n : value,\n ]),\n )\n\n if (Object.keys(plan.host).length || extra) {\n tx.update(hostRef, { ...stored(plan.host), ...extra, updatedAt: now })\n }\n for (const write of plan.entries) {\n const ref = library.doc(write.id)\n if (write.op === 'delete') tx.delete(ref)\n else if (write.op === 'create') {\n tx.set(ref, {\n ...(write.data as unknown as Record<string, unknown>),\n createdAt: now,\n updatedAt: now,\n })\n } else tx.update(ref, { ...stored(write.data), updatedAt: now })\n }\n return plan\n}\n"],"names":["createResourceUid","planThemeLibraryAction","THEME_FIELD_DELETE","THEME_LIBRARY_COLLECTION","firebaseAdmin","runThemeLibraryAction","tx","hostRef","action","extra","hostSnapshot","library","collection","entriesSnapshot","Promise","all","get","entries","doc","docs","id","data","plan","host","newId","ok","now","firestore","FieldValue","serverTimestamp","stored","fields","Object","fromEntries","map","key","value","delete","keys","length","update","updatedAt","write","ref","op","set","createdAt"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,6CAA4C;AAC9E,SACEC,sBAAsB,EACtBC,kBAAkB,EAClBC,wBAAwB,QAInB,uCAAsC;AAE7C,OAAOC,mBAAmB,sBAAkB;AAE5C;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,eAAeC,sBACpBC,EAAe,EACfC,OAA0B,EAC1BC,MAA0B,EAC1BC,KAA+B;QAYvBC;IAVR,MAAMC,UAAUJ,QAAQK,UAAU,CAACT;IACnC,MAAM,CAACO,cAAcG,gBAAgB,GAAG,MAAMC,QAAQC,GAAG,CAAC;QACxDT,GAAGU,GAAG,CAACT;QACPD,GAAGU,GAAG,CAACL;KACR;IACD,MAAMM,UAA6C,CAAC;IACpD,KAAK,MAAMC,OAAOL,gBAAgBM,IAAI,CAAE;QACtCF,OAAO,CAACC,IAAIE,EAAE,CAAC,GAAGF,IAAIG,IAAI;IAC5B;IACA,MAAMC,OAAOrB,uBAAuB;QAClCsB,IAAI,GAAEb,qBAAAA,aAAaW,IAAI,cAAjBX,qBAAuB,CAAC;QAC9BO;QACAT;QACAgB,OAAOxB;IACT;IACA,IAAIsB,KAAKG,EAAE,KAAK,OAAO,OAAOH;IAE9B,MAAMI,MAAMtB,cAAcuB,SAAS,CAACC,UAAU,CAACC,eAAe;IAC9D,MAAMC,SAAS,CAACC,SACdC,OAAOC,WAAW,CAChBD,OAAOf,OAAO,CAACc,QAAQG,GAAG,CAAC,CAAC,CAACC,KAAKC,MAAM,GAAK;gBAC3CD;gBACAC,UAAUlC,qBACNE,cAAcuB,SAAS,CAACC,UAAU,CAACS,MAAM,KACzCD;aACL;IAGL,IAAIJ,OAAOM,IAAI,CAAChB,KAAKC,IAAI,EAAEgB,MAAM,IAAI9B,OAAO;QAC1CH,GAAGkC,MAAM,CAACjC,SAAS,aAAKuB,OAAOR,KAAKC,IAAI,GAAMd;YAAOgC,WAAWf;;IAClE;IACA,KAAK,MAAMgB,SAASpB,KAAKL,OAAO,CAAE;QAChC,MAAM0B,MAAMhC,QAAQO,GAAG,CAACwB,MAAMtB,EAAE;QAChC,IAAIsB,MAAME,EAAE,KAAK,UAAUtC,GAAG+B,MAAM,CAACM;aAChC,IAAID,MAAME,EAAE,KAAK,UAAU;YAC9BtC,GAAGuC,GAAG,CAACF,KAAK,aACND,MAAMrB,IAAI;gBACdyB,WAAWpB;gBACXe,WAAWf;;QAEf,OAAOpB,GAAGkC,MAAM,CAACG,KAAK,aAAKb,OAAOY,MAAMrB,IAAI;YAAGoB,WAAWf;;IAC5D;IACA,OAAOJ;AACT"}