@aglyn/plugins-marketplace 1.0.0-beta.182 → 1.0.0-beta.184

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/plugins-marketplace",
3
- "version": "1.0.0-beta.182",
3
+ "version": "1.0.0-beta.184",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,19 +25,19 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.182",
29
- "@aglyn/aglyn-markdown-editor": "1.0.0-beta.182",
30
- "@aglyn/shared-data-enums": "1.0.0-beta.182",
31
- "@aglyn/shared-data-mdi": "1.0.0-beta.182",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.182",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.182",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.182",
35
- "@aglyn/shared-util-email": "1.0.0-beta.182",
36
- "@aglyn/shared-util-http": "1.0.0-beta.182",
37
- "@aglyn/shared-util-timestamp": "1.0.0-beta.182",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.182",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.182",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.182",
28
+ "@aglyn/aglyn": "1.0.0-beta.184",
29
+ "@aglyn/aglyn-markdown-editor": "1.0.0-beta.184",
30
+ "@aglyn/shared-data-enums": "1.0.0-beta.184",
31
+ "@aglyn/shared-data-mdi": "1.0.0-beta.184",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.184",
33
+ "@aglyn/shared-ui-next": "1.0.0-beta.184",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.184",
35
+ "@aglyn/shared-util-email": "1.0.0-beta.184",
36
+ "@aglyn/shared-util-http": "1.0.0-beta.184",
37
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.184",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.184",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.184",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.184",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -0,0 +1,53 @@
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 ComponentStoreLike } from '@aglyn/aglyn/app-utils/load-referenced-components';
18
+ /** Why an email with a dangling reusable block is not published. */
19
+ export declare const EMAIL_UNRESOLVED_BLOCK_REFUSAL: string;
20
+ /**
21
+ * An email design with the site's reusable blocks INLINED — what both email
22
+ * publishers hand the marketplace (AGL-3287).
23
+ *
24
+ * A shared header or footer is a `reusableInstance` naming a component on the
25
+ * SELLER'S site. A listing is installed into another org, which has none of
26
+ * those components, so a design that kept the reference would install with
27
+ * the blocks missing — and the sanitizer refuses the reference outright
28
+ * anyway, because a placement pointing into another tenant's documents is
29
+ * exactly what its allowlist exists to keep out. So each placement is
30
+ * replaced by the blocks it renders, from the component's PUBLISHED design,
31
+ * with the placement's own property values and attribute overrides applied:
32
+ * the listing is what the seller's own mail looks like, and depends on
33
+ * nothing the seller keeps.
34
+ *
35
+ * A placement that does not resolve — a deleted component, one never
36
+ * published — is REFUSED rather than dropped. It renders nothing in the
37
+ * seller's own mail, but a publish that silently removed part of the design
38
+ * would teach the seller nothing, which is the same argument the starter's
39
+ * inspection makes for refusing a URL instead of trimming it. Only placements
40
+ * the design actually draws count, from `rootId` down, which is the walk the
41
+ * sanitizer publishes.
42
+ */
43
+ export declare function inlineReusableBlocks<M extends Record<string, unknown>>(nodes: M, context: {
44
+ firestore: ComponentStoreLike;
45
+ hostId: string;
46
+ rootId: string;
47
+ }): Promise<{
48
+ ok: true;
49
+ nodes: M;
50
+ } | {
51
+ ok: false;
52
+ error: string;
53
+ }>;
@@ -0,0 +1,64 @@
1
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
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 { collectDescendantIds, REUSABLE_INSTANCE_COMPONENT_ID } from "@aglyn/aglyn/app-utils/compose-reusable-components";
18
+ import { composeHostComponentNodes } from "@aglyn/aglyn/app-utils/load-referenced-components";
19
+ /** Why an email with a dangling reusable block is not published. */ export const EMAIL_UNRESOLVED_BLOCK_REFUSAL = 'This email places a reusable block whose component is deleted or has ' + 'never been published. Publish the component or remove the block, then ' + 'publish the email again.';
20
+ /**
21
+ * An email design with the site's reusable blocks INLINED — what both email
22
+ * publishers hand the marketplace (AGL-3287).
23
+ *
24
+ * A shared header or footer is a `reusableInstance` naming a component on the
25
+ * SELLER'S site. A listing is installed into another org, which has none of
26
+ * those components, so a design that kept the reference would install with
27
+ * the blocks missing — and the sanitizer refuses the reference outright
28
+ * anyway, because a placement pointing into another tenant's documents is
29
+ * exactly what its allowlist exists to keep out. So each placement is
30
+ * replaced by the blocks it renders, from the component's PUBLISHED design,
31
+ * with the placement's own property values and attribute overrides applied:
32
+ * the listing is what the seller's own mail looks like, and depends on
33
+ * nothing the seller keeps.
34
+ *
35
+ * A placement that does not resolve — a deleted component, one never
36
+ * published — is REFUSED rather than dropped. It renders nothing in the
37
+ * seller's own mail, but a publish that silently removed part of the design
38
+ * would teach the seller nothing, which is the same argument the starter's
39
+ * inspection makes for refusing a URL instead of trimming it. Only placements
40
+ * the design actually draws count, from `rootId` down, which is the walk the
41
+ * sanitizer publishes.
42
+ */ export async function inlineReusableBlocks(nodes, context) {
43
+ const { rootId } = context, site = _object_without_properties_loose(context, [
44
+ "rootId"
45
+ ]);
46
+ const inlined = await composeHostComponentNodes(nodes, site);
47
+ const drawn = [
48
+ rootId,
49
+ ...collectDescendantIds(inlined, rootId)
50
+ ];
51
+ const dangling = drawn.some((id)=>{
52
+ var _inlined_id;
53
+ return ((_inlined_id = inlined[id]) == null ? void 0 : _inlined_id.componentId) === REUSABLE_INSTANCE_COMPONENT_ID;
54
+ });
55
+ return dangling ? {
56
+ ok: false,
57
+ error: EMAIL_UNRESOLVED_BLOCK_REFUSAL
58
+ } : {
59
+ ok: true,
60
+ nodes: inlined
61
+ };
62
+ }
63
+
64
+ //# sourceMappingURL=publish-email-blocks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-email-blocks.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 collectDescendantIds,\n REUSABLE_INSTANCE_COMPONENT_ID,\n} from '@aglyn/aglyn/app-utils/compose-reusable-components'\nimport {\n composeHostComponentNodes,\n type ComponentStoreLike,\n} from '@aglyn/aglyn/app-utils/load-referenced-components'\n\n/** Why an email with a dangling reusable block is not published. */\nexport const EMAIL_UNRESOLVED_BLOCK_REFUSAL =\n 'This email places a reusable block whose component is deleted or has ' +\n 'never been published. Publish the component or remove the block, then ' +\n 'publish the email again.'\n\n/**\n * An email design with the site's reusable blocks INLINED — what both email\n * publishers hand the marketplace (AGL-3287).\n *\n * A shared header or footer is a `reusableInstance` naming a component on the\n * SELLER'S site. A listing is installed into another org, which has none of\n * those components, so a design that kept the reference would install with\n * the blocks missing — and the sanitizer refuses the reference outright\n * anyway, because a placement pointing into another tenant's documents is\n * exactly what its allowlist exists to keep out. So each placement is\n * replaced by the blocks it renders, from the component's PUBLISHED design,\n * with the placement's own property values and attribute overrides applied:\n * the listing is what the seller's own mail looks like, and depends on\n * nothing the seller keeps.\n *\n * A placement that does not resolve — a deleted component, one never\n * published — is REFUSED rather than dropped. It renders nothing in the\n * seller's own mail, but a publish that silently removed part of the design\n * would teach the seller nothing, which is the same argument the starter's\n * inspection makes for refusing a URL instead of trimming it. Only placements\n * the design actually draws count, from `rootId` down, which is the walk the\n * sanitizer publishes.\n */\nexport async function inlineReusableBlocks<M extends Record<string, unknown>>(\n nodes: M,\n context: { firestore: ComponentStoreLike; hostId: string; rootId: string },\n): Promise<{ ok: true; nodes: M } | { ok: false; error: string }> {\n const { rootId, ...site } = context\n const inlined = await composeHostComponentNodes(nodes, site)\n const drawn = [rootId, ...collectDescendantIds(inlined as never, rootId)]\n const dangling = drawn.some(\n (id) =>\n (inlined[id] as { componentId?: unknown } | undefined)?.componentId ===\n REUSABLE_INSTANCE_COMPONENT_ID,\n )\n return dangling\n ? { ok: false, error: EMAIL_UNRESOLVED_BLOCK_REFUSAL }\n : { ok: true, nodes: inlined }\n}\n"],"names":["collectDescendantIds","REUSABLE_INSTANCE_COMPONENT_ID","composeHostComponentNodes","EMAIL_UNRESOLVED_BLOCK_REFUSAL","inlineReusableBlocks","nodes","context","rootId","site","inlined","drawn","dangling","some","id","componentId","ok","error"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,oBAAoB,EACpBC,8BAA8B,QACzB,qDAAoD;AAC3D,SACEC,yBAAyB,QAEpB,oDAAmD;AAE1D,kEAAkE,GAClE,OAAO,MAAMC,iCACX,0EACA,2EACA,2BAA0B;AAE5B;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,eAAeC,qBACpBC,KAAQ,EACRC,OAA0E;IAE1E,MAAM,EAAEC,MAAM,EAAW,GAAGD,SAATE,wCAASF;;;IAC5B,MAAMG,UAAU,MAAMP,0BAA0BG,OAAOG;IACvD,MAAME,QAAQ;QAACH;WAAWP,qBAAqBS,SAAkBF;KAAQ;IACzE,MAAMI,WAAWD,MAAME,IAAI,CACzB,CAACC;YACEJ;eAAD,EAACA,cAAAA,OAAO,CAACI,GAAG,qBAAZ,AAACJ,YAAuDK,WAAW,MACnEb;;IAEJ,OAAOU,WACH;QAAEI,IAAI;QAAOC,OAAOb;IAA+B,IACnD;QAAEY,IAAI;QAAMV,OAAOI;IAAQ;AACjC"}
@@ -15,6 +15,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { CANVAS_ROOT_ELEMENT_ID, checkEntitlement, createResourceUid } from "@aglyn/aglyn/server";
18
+ import { decodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
19
+ import { inlineReusableBlocks } from "./publish-email-blocks.js";
18
20
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
19
21
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
20
22
  import { emailStarterRefusal, inspectEmailStarter, MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, marketplacePriceRefusal, sanitizeMarketplaceDefinition } from "../model/index.js";
@@ -134,10 +136,33 @@ import { publishPreconditionRefusal } from "./publish-preconditions.js";
134
136
  });
135
137
  }
136
138
  const versionSnapshot = await screenRef.collection('versions').doc(String(activeVersionId)).get();
139
+ /*
140
+ * The site's reusable blocks INLINED before anything is judged (AGL-3287).
141
+ * A buyer's org has none of this site's components, so a placement has to
142
+ * travel as the blocks it renders — and inlining first is what puts a
143
+ * shared footer's links and images in front of the inspection below, as
144
+ * the send path would mail them. An undecodable payload is left as stored,
145
+ * so the inspection still refuses it by name.
146
+ */ const stored = versionSnapshot.get('nodes');
147
+ const readable = decodeStoredNodes(stored);
148
+ let design = stored;
149
+ if (readable && typeof readable === 'object' && !Array.isArray(readable)) {
150
+ const inlined = await inlineReusableBlocks(readable, {
151
+ firestore,
152
+ hostId,
153
+ rootId: CANVAS_ROOT_ELEMENT_ID
154
+ });
155
+ if (inlined.ok === false) {
156
+ return res.status(422).json({
157
+ error: inlined.error
158
+ });
159
+ }
160
+ design = inlined.nodes;
161
+ }
137
162
  // The inspection decodes, so it is also the guard against publishing a
138
163
  // node map nothing could read — it refuses rather than reporting a
139
164
  // compressed payload as a clean design.
140
- const inspection = inspectEmailStarter(versionSnapshot.get('nodes'));
165
+ const inspection = inspectEmailStarter(design);
141
166
  const violation = emailStarterRefusal(inspection);
142
167
  if (violation) {
143
168
  return res.status(422).json({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-email-starter.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 CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n emailStarterRefusal,\n inspectEmailStarter,\n MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST,\n marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\n\n/**\n * Publishes a site's campaign email design as a marketplace starter.\n *\n * The source is a `kind: 'email'` SCREEN — the document `createEmailScreen`\n * writes and the screen besigner edits — not an entry in the transactional\n * catalog. That is the whole difference from `publish-email-template`, and it\n * is why the two exist separately: a transactional design is a design FOR a\n * fixed key and installs onto that same key; a starter belongs to no key and\n * installs as a new email of the buyer's own.\n *\n * ## Two things this reads that the transactional publisher does not\n *\n * **The nodes are compressed.** A screen version is written through\n * `screenVersionConverter`, so `nodes` comes back from the Admin SDK as a\n * msgpack `Buffer`. `publish-email-template.ts` reads its `nodes` raw and says\n * why — its besigner saves with a bare `setDoc` and no converter — and copying\n * that line here would publish a Buffer: truthy, non-empty by `Object.keys`\n * (those are byte indices), and scanning as a design with no blocks in it.\n *\n * **The tree is inspected, not just sanitized.** `sanitizeMarketplaceDefinition`\n * DROPS an unsafe URL and publishes what is left, which is right for a page.\n * Here the URL rules are the product: a remote image in an email is a read\n * receipt for whoever hosts it, and quietly removing one teaches the publisher\n * nothing. So `inspectEmailStarter` runs first and REFUSES.\n */\nexport const publishEmailStarterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const screenId = String(req.body?.screenId ?? '')\n const displayName = String(req.body?.displayName ?? '').slice(0, 80)\n const description = String(req.body?.description ?? '').slice(0, 500)\n const category = String(req.body?.category ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(req.body?.priceUsd ?? 0)) || 0\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) return res.status(400).json({ error: priceRefusal })\n if (!hostId || !screenId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing hostId, screenId, or displayName' })\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n const orgForHost = await getOrgForHost(hostId)\n if (!checkEntitlement(orgForHost?.org ?? {}, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n if (!orgForHost?.orgId) {\n return res.status(409).json({ error: 'Site has no owning organization' })\n }\n const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId)\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'templates',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const screenRef = hostRef.collection('screens').doc(screenId)\n const screenSnapshot = await screenRef.get()\n const screen = screenSnapshot.data() as Record<string, unknown> | undefined\n if (!screen || screen['deletedAt']) {\n return res.status(404).json({ error: 'Unknown email' })\n }\n if (screen['kind'] !== 'email') {\n return res\n .status(422)\n .json({ error: 'That screen is a page, not an email' })\n }\n const activeVersionId = screen['versionId']\n if (!activeVersionId) {\n return res\n .status(404)\n .json({ error: 'This email has no saved design yet — design and save it first' })\n }\n const versionSnapshot = await screenRef\n .collection('versions')\n .doc(String(activeVersionId))\n .get()\n\n // The inspection decodes, so it is also the guard against publishing a\n // node map nothing could read — it refuses rather than reporting a\n // compressed payload as a clean design.\n const inspection = inspectEmailStarter(versionSnapshot.get('nodes'))\n const violation = emailStarterRefusal(inspection)\n if (violation) {\n return res.status(422).json({\n error: violation,\n violations: inspection.violations,\n })\n }\n if (!inspection.nodes || !Object.keys(inspection.nodes).length) {\n return res.status(404).json({ error: 'This email design is empty' })\n }\n // Runs after the inspection and over the same decoded map. It enforces the\n // component-id allowlist and trims node keys; the URL rules it also carries\n // can no longer fire, because a design that would have tripped them was\n // refused above rather than silently trimmed.\n const sanitized = sanitizeMarketplaceDefinition(\n { rootId: CANVAS_ROOT_ELEMENT_ID, nodes: inspection.nodes },\n { componentIds: MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // One listing per (publisher org, source screen). Re-publishing the same\n // email bumps its version rather than minting a second listing, which is\n // what makes \"update available\" mean anything to everyone who installed it.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceHostId', '==', hostId)\n .where('sourceScreenId', '==', screenId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'emailStarter',\n sourceHostId: hostId,\n sourceScreenId: screenId,\n displayName: displayName.trim(),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n deletedAt: null,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({\n rootId: sanitized.rootId,\n nodes: sanitized.nodes,\n // Copy is part of the design and travels with it.\n subject: String(screen['emailSubject'] ?? ''),\n preheader: String(screen['emailPreheader'] ?? ''),\n // The hosts this design would send a recipient to, recorded at publish\n // so the listing page can show them BEFORE somebody installs. A tenant\n // about to mail their own customers under their own name is entitled to\n // see the domains a stranger's template points them at, and no\n // allowlist can make that judgment for them.\n linkHosts: inspection.linkHosts,\n // Absent `reviewState` is the un-reviewed default, and it is written\n // NOWHERE here on purpose: a publisher who could stamp their own\n // version approved would make the field decorative. Staff write it.\n publishedAt: now,\n })\n\n return res\n .status(200)\n .json({ listingId: listingRef.id, version, linkHosts: inspection.linkHosts })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Email starter publish failed' })\n }\n}\n\nexport default publishEmailStarterHandler\n"],"names":["CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","firebaseAdmin","getOrgForHost","resolveOrgPermissions","emailStarterRefusal","inspectEmailStarter","MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","marketplacePriceRefusal","sanitizeMarketplaceDefinition","resolvePublisherProfile","publishPreconditionRefusal","publishEmailStarterHandler","req","res","method","status","json","error","hostId","String","body","screenId","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","length","undefined","hostSnapshot","existing","screen","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","orgForHost","org","orgId","publisher","refusal","sells","screenRef","screenSnapshot","data","activeVersionId","versionSnapshot","inspection","violation","violations","nodes","Object","keys","sanitized","rootId","componentIds","ok","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","set","profileId","artifactType","sourceHostId","sourceScreenId","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","subject","preheader","linkHosts","listingId","id","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-email-starter.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 CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { decodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { inlineReusableBlocks } from './publish-email-blocks'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n emailStarterRefusal,\n inspectEmailStarter,\n MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST,\n marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\n\n/**\n * Publishes a site's campaign email design as a marketplace starter.\n *\n * The source is a `kind: 'email'` SCREEN — the document `createEmailScreen`\n * writes and the screen besigner edits — not an entry in the transactional\n * catalog. That is the whole difference from `publish-email-template`, and it\n * is why the two exist separately: a transactional design is a design FOR a\n * fixed key and installs onto that same key; a starter belongs to no key and\n * installs as a new email of the buyer's own.\n *\n * ## Two things this reads that the transactional publisher does not\n *\n * **The nodes are compressed.** A screen version is written through\n * `screenVersionConverter`, so `nodes` comes back from the Admin SDK as a\n * msgpack `Buffer`. `publish-email-template.ts` reads its `nodes` raw and says\n * why — its besigner saves with a bare `setDoc` and no converter — and copying\n * that line here would publish a Buffer: truthy, non-empty by `Object.keys`\n * (those are byte indices), and scanning as a design with no blocks in it.\n *\n * **The tree is inspected, not just sanitized.** `sanitizeMarketplaceDefinition`\n * DROPS an unsafe URL and publishes what is left, which is right for a page.\n * Here the URL rules are the product: a remote image in an email is a read\n * receipt for whoever hosts it, and quietly removing one teaches the publisher\n * nothing. So `inspectEmailStarter` runs first and REFUSES.\n */\nexport const publishEmailStarterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const screenId = String(req.body?.screenId ?? '')\n const displayName = String(req.body?.displayName ?? '').slice(0, 80)\n const description = String(req.body?.description ?? '').slice(0, 500)\n const category = String(req.body?.category ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(req.body?.priceUsd ?? 0)) || 0\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) return res.status(400).json({ error: priceRefusal })\n if (!hostId || !screenId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing hostId, screenId, or displayName' })\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n const orgForHost = await getOrgForHost(hostId)\n if (!checkEntitlement(orgForHost?.org ?? {}, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n if (!orgForHost?.orgId) {\n return res.status(409).json({ error: 'Site has no owning organization' })\n }\n const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId)\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'templates',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const screenRef = hostRef.collection('screens').doc(screenId)\n const screenSnapshot = await screenRef.get()\n const screen = screenSnapshot.data() as Record<string, unknown> | undefined\n if (!screen || screen['deletedAt']) {\n return res.status(404).json({ error: 'Unknown email' })\n }\n if (screen['kind'] !== 'email') {\n return res\n .status(422)\n .json({ error: 'That screen is a page, not an email' })\n }\n const activeVersionId = screen['versionId']\n if (!activeVersionId) {\n return res\n .status(404)\n .json({ error: 'This email has no saved design yet — design and save it first' })\n }\n const versionSnapshot = await screenRef\n .collection('versions')\n .doc(String(activeVersionId))\n .get()\n\n /*\n * The site's reusable blocks INLINED before anything is judged (AGL-3287).\n * A buyer's org has none of this site's components, so a placement has to\n * travel as the blocks it renders — and inlining first is what puts a\n * shared footer's links and images in front of the inspection below, as\n * the send path would mail them. An undecodable payload is left as stored,\n * so the inspection still refuses it by name.\n */\n const stored = versionSnapshot.get('nodes')\n const readable = decodeStoredNodes<Record<string, unknown>>(stored)\n let design: unknown = stored\n if (readable && typeof readable === 'object' && !Array.isArray(readable)) {\n const inlined = await inlineReusableBlocks(readable, {\n firestore,\n hostId,\n rootId: CANVAS_ROOT_ELEMENT_ID,\n })\n if (inlined.ok === false) {\n return res.status(422).json({ error: inlined.error })\n }\n design = inlined.nodes\n }\n // The inspection decodes, so it is also the guard against publishing a\n // node map nothing could read — it refuses rather than reporting a\n // compressed payload as a clean design.\n const inspection = inspectEmailStarter(design)\n const violation = emailStarterRefusal(inspection)\n if (violation) {\n return res.status(422).json({\n error: violation,\n violations: inspection.violations,\n })\n }\n if (!inspection.nodes || !Object.keys(inspection.nodes).length) {\n return res.status(404).json({ error: 'This email design is empty' })\n }\n // Runs after the inspection and over the same decoded map. It enforces the\n // component-id allowlist and trims node keys; the URL rules it also carries\n // can no longer fire, because a design that would have tripped them was\n // refused above rather than silently trimmed.\n const sanitized = sanitizeMarketplaceDefinition(\n { rootId: CANVAS_ROOT_ELEMENT_ID, nodes: inspection.nodes },\n { componentIds: MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // One listing per (publisher org, source screen). Re-publishing the same\n // email bumps its version rather than minting a second listing, which is\n // what makes \"update available\" mean anything to everyone who installed it.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceHostId', '==', hostId)\n .where('sourceScreenId', '==', screenId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'emailStarter',\n sourceHostId: hostId,\n sourceScreenId: screenId,\n displayName: displayName.trim(),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n deletedAt: null,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({\n rootId: sanitized.rootId,\n nodes: sanitized.nodes,\n // Copy is part of the design and travels with it.\n subject: String(screen['emailSubject'] ?? ''),\n preheader: String(screen['emailPreheader'] ?? ''),\n // The hosts this design would send a recipient to, recorded at publish\n // so the listing page can show them BEFORE somebody installs. A tenant\n // about to mail their own customers under their own name is entitled to\n // see the domains a stranger's template points them at, and no\n // allowlist can make that judgment for them.\n linkHosts: inspection.linkHosts,\n // Absent `reviewState` is the un-reviewed default, and it is written\n // NOWHERE here on purpose: a publisher who could stamp their own\n // version approved would make the field decorative. Staff write it.\n publishedAt: now,\n })\n\n return res\n .status(200)\n .json({ listingId: listingRef.id, version, linkHosts: inspection.linkHosts })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Email starter publish failed' })\n }\n}\n\nexport default publishEmailStarterHandler\n"],"names":["CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","inlineReusableBlocks","firebaseAdmin","getOrgForHost","resolveOrgPermissions","emailStarterRefusal","inspectEmailStarter","MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","marketplacePriceRefusal","sanitizeMarketplaceDefinition","resolvePublisherProfile","publishPreconditionRefusal","publishEmailStarterHandler","req","res","method","status","json","error","hostId","String","body","screenId","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","length","undefined","hostSnapshot","existing","screen","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","orgForHost","org","orgId","publisher","refusal","sells","screenRef","screenSnapshot","data","activeVersionId","versionSnapshot","stored","readable","design","Array","isArray","inlined","rootId","ok","nodes","inspection","violation","violations","Object","keys","sanitized","componentIds","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","set","profileId","artifactType","sourceHostId","sourceScreenId","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","subject","preheader","linkHosts","listingId","id","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,sBAAsB,EACtBC,gBAAgB,EAChBC,iBAAiB,QACZ,sBAAqB;AAE5B,SAASC,iBAAiB,QAAQ,sCAAqC;AACvE,SAASC,oBAAoB,QAAQ,4BAAwB;AAC7D,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACvE,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SACEC,mBAAmB,EACnBC,mBAAmB,EACnBC,gDAAgD,EAChDC,uBAAuB,EACvBC,6BAA6B,QACxB,oBAAU;AACjB,SAASC,uBAAuB,QAAQ,yBAAqB;AAC7D,SAASC,0BAA0B,QAAQ,6BAAyB;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,MAAMC,6BAA+C,OAAOC,KAAKC;iDAkBzCD;QAdPA,WACEA,YACGA,YACAA,YACHA,YACWA;IARnC,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,SAASC,gBAAOP,YAAAA,IAAIQ,IAAI,qBAARR,UAAUM,MAAM,mBAAI;IAC1C,MAAMG,WAAWF,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUS,QAAQ,oBAAI;IAC9C,MAAMC,cAAcH,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUU,WAAW,oBAAI,IAAIC,KAAK,CAAC,GAAG;IACjE,MAAMC,cAAcL,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUY,WAAW,oBAAI,IAAID,KAAK,CAAC,GAAG;IACjE,MAAME,WAAWN,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUa,QAAQ,oBAAI,IAAIF,KAAK,CAAC,GAAG;IAC3D,MAAMG,WAAWC,KAAKC,KAAK,CAACC,iBAAOjB,aAAAA,IAAIQ,IAAI,qBAARR,WAAUc,QAAQ,oBAAI,OAAO;IAChE,MAAMI,eAAevB,wBAAwBmB;IAC7C,IAAII,cAAc,OAAOjB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAOa;IAAa;IACpE,IAAI,CAACZ,UAAU,CAACG,YAAY,CAACC,YAAYS,IAAI,IAAI;QAC/C,OAAOlB,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;YAAEC,OAAO;QAA2C;IAC9D;IAEA,MAAMe,gBAAgBb,QAAOP,6BAAAA,IAAIqB,OAAO,CAACD,aAAa,YAAzBpB,6BAA6B;IAC1D,MAAMsB,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcT,KAAK,CAAC,UAAUa,MAAM,IACpCC;IACJ,IAAI,CAACH,SAAS,OAAOrB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,IAAI;YAekBqB,0BA0GTC,qBA+BSC,sBACEA;QAxJtB,MAAMC,UAAU,MAAMxC,cAAcyC,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACV;QAC/D,MAAMW,aAAa,MAAM1C,sBAAsBsC,QAAQK,GAAG,EAAE;YAAE5B;QAAO;QACrE,IAAI,CAAC2B,WAAWE,WAAW,CAACC,oBAAoB,EAAE;YAChD,OAAOnC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OACE;YACJ;QACF;QACA,MAAMgC,YAAYhD,cAAcyC,GAAG,GAAGO,SAAS;QAC/C,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAAClC;QAClD,MAAMoB,eAAe,MAAMY,QAAQG,GAAG;QACtC,IAAI,CAACf,aAAagB,MAAM,EAAE;YACxB,OAAOzC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAe;QACtD;QACA,MAAMsC,aAAa,EAACjB,oBAAAA,aAAae,GAAG,CAAC,0BAAjBf,oBAAmC,CAAC,EAAE,CAACG,QAAQK,GAAG,CAAC;QACvE,IAAIS,eAAe,WAAWA,eAAe,UAAU;YACrD,OAAO1C,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA6B;QACpE;QAEA,MAAMuC,aAAa,MAAMtD,cAAcgB;QACvC,IAAI,CAACrB,0BAAiB2D,8BAAAA,WAAYC,GAAG,oBAAI,CAAC,GAAG,uBAAuB;YAClE,OAAO5C,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAO;YAAoD;QACvE;QACA,IAAI,EAACuC,8BAAAA,WAAYE,KAAK,GAAE;YACtB,OAAO7C,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAkC;QACzE;QACA,MAAM0C,YAAY,MAAMlD,wBAAwBwC,WAAWO,WAAWE,KAAK;QAC3E,MAAME,UAAUlD,2BAA2BiD,WAAW;YACpDjC;YACAmC,OAAO;QACT;QACA,IAAID,SAAS,OAAO/C,IAAIE,MAAM,CAAC6C,QAAQ7C,MAAM,EAAEC,IAAI,CAAC4C,QAAQxC,IAAI;QAEhE,MAAM0C,YAAYZ,QAAQC,UAAU,CAAC,WAAWC,GAAG,CAAC/B;QACpD,MAAM0C,iBAAiB,MAAMD,UAAUT,GAAG;QAC1C,MAAMb,SAASuB,eAAeC,IAAI;QAClC,IAAI,CAACxB,UAAUA,MAAM,CAAC,YAAY,EAAE;YAClC,OAAO3B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAgB;QACvD;QACA,IAAIuB,MAAM,CAAC,OAAO,KAAK,SAAS;YAC9B,OAAO3B,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAO;YAAsC;QACzD;QACA,MAAMgD,kBAAkBzB,MAAM,CAAC,YAAY;QAC3C,IAAI,CAACyB,iBAAiB;YACpB,OAAOpD,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAO;YAAgE;QACnF;QACA,MAAMiD,kBAAkB,MAAMJ,UAC3BX,UAAU,CAAC,YACXC,GAAG,CAACjC,OAAO8C,kBACXZ,GAAG;QAEN;;;;;;;KAOC,GACD,MAAMc,SAASD,gBAAgBb,GAAG,CAAC;QACnC,MAAMe,WAAWrE,kBAA2CoE;QAC5D,IAAIE,SAAkBF;QACtB,IAAIC,YAAY,OAAOA,aAAa,YAAY,CAACE,MAAMC,OAAO,CAACH,WAAW;YACxE,MAAMI,UAAU,MAAMxE,qBAAqBoE,UAAU;gBACnDnB;gBACA/B;gBACAuD,QAAQ7E;YACV;YACA,IAAI4E,QAAQE,EAAE,KAAK,OAAO;gBACxB,OAAO7D,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAAEC,OAAOuD,QAAQvD,KAAK;gBAAC;YACrD;YACAoD,SAASG,QAAQG,KAAK;QACxB;QACA,uEAAuE;QACvE,mEAAmE;QACnE,wCAAwC;QACxC,MAAMC,aAAavE,oBAAoBgE;QACvC,MAAMQ,YAAYzE,oBAAoBwE;QACtC,IAAIC,WAAW;YACb,OAAOhE,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO4D;gBACPC,YAAYF,WAAWE,UAAU;YACnC;QACF;QACA,IAAI,CAACF,WAAWD,KAAK,IAAI,CAACI,OAAOC,IAAI,CAACJ,WAAWD,KAAK,EAAEvC,MAAM,EAAE;YAC9D,OAAOvB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA6B;QACpE;QACA,2EAA2E;QAC3E,4EAA4E;QAC5E,wEAAwE;QACxE,8CAA8C;QAC9C,MAAMgE,YAAYzE,8BAChB;YAAEiE,QAAQ7E;YAAwB+E,OAAOC,WAAWD,KAAK;QAAC,GAC1D;YAAEO,cAAc5E;QAAiD;QAEnE,IAAI2E,UAAUP,EAAE,KAAK,OAAO;YAC1B,OAAO7D,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAOgE,UAAUhE,KAAK;YAAC;QACvD;QAEA,yEAAyE;QACzE,yEAAyE;QACzE,4EAA4E;QAC5E,MAAMsB,WAAW,MAAMU,UACpBE,UAAU,CAAC,uBACXgC,KAAK,CAAC,aAAa,MAAMxB,UAAUD,KAAK,EACxCyB,KAAK,CAAC,gBAAgB,MAAMjE,QAC5BiE,KAAK,CAAC,kBAAkB,MAAM9D,UAC9B+D,KAAK,CAAC,GACN/B,GAAG;QACN,MAAMgC,aAAa9C,SAAS+C,KAAK,GAC7BrC,UAAUE,UAAU,CAAC,uBAAuBC,GAAG,CAACtD,uBAChDyC,SAASgD,IAAI,CAAC,EAAE,CAACC,GAAG;QACxB,MAAMC,UAAUlD,SAAS+C,KAAK,GAC1B,IACAzD,QAAOU,sBAAAA,SAASgD,IAAI,CAAC,EAAE,CAAClC,GAAG,CAAC,4BAArBd,sBAAyC,KAAK;QACzD,MAAMmD,MAAMzF,cAAcgD,SAAS,CAAC0C,UAAU,CAACC,eAAe;QAE9D,MAAMP,WAAWQ,GAAG,CAClB;YACEC,WAAWnC,UAAUD,KAAK;YAC1BqC,cAAc;YACdC,cAAc9E;YACd+E,gBAAgB5E;YAChBC,aAAaA,YAAYS,IAAI;WACzBP,YAAYO,IAAI,MAAM;YAAEP,aAAaA,YAAYO,IAAI;QAAG,GACxDN,SAASM,IAAI,MAAM;YAAEN,UAAUA,SAASM,IAAI;QAAG;YACnDL;YACAwE,eAAeT;YACfU,WAAW;WACP5D,SAAS+C,KAAK,IAAI;YAAEc,WAAWV;QAAI;YACvCW,WAAWX;YACXY,gBAAgBrG,cAAcgD,SAAS,CAAC0C,UAAU,CAACY,UAAU,CAAC;gBAC5Dd;gBACAe,aAAavG,cAAcgD,SAAS,CAACwD,SAAS,CAACf,GAAG;YACpD;YAEF;YAAEgB,OAAO;QAAK;QAEhB,MAAMrB,WACHlC,UAAU,CAAC,YACXC,GAAG,CAACjC,OAAOsE,UACXI,GAAG,CAAC;YACHpB,QAAQQ,UAAUR,MAAM;YACxBE,OAAOM,UAAUN,KAAK;YACtB,kDAAkD;YAClDgC,SAASxF,QAAOqB,uBAAAA,MAAM,CAAC,eAAe,YAAtBA,uBAA0B;YAC1CoE,WAAWzF,QAAOqB,yBAAAA,MAAM,CAAC,iBAAiB,YAAxBA,yBAA4B;YAC9C,uEAAuE;YACvE,uEAAuE;YACvE,wEAAwE;YACxE,+DAA+D;YAC/D,6CAA6C;YAC7CqE,WAAWjC,WAAWiC,SAAS;YAC/B,qEAAqE;YACrE,iEAAiE;YACjE,oEAAoE;YACpEL,aAAad;QACf;QAEF,OAAO7E,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;YAAE8F,WAAWzB,WAAW0B,EAAE;YAAEtB;YAASoB,WAAWjC,WAAWiC,SAAS;QAAC;IAC/E,EAAE,OAAO5F,OAAO;QACd+F,QAAQ/F,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA+B;IACtE;AACF,EAAC;AAED,eAAeN,2BAA0B"}
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { checkEntitlement, createResourceUid, decodeStoredNodes } from "@aglyn/aglyn/server";
18
+ import { inlineReusableBlocks } from "./publish-email-blocks.js";
18
19
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
19
20
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
20
21
  import { EMAIL_NODE_ROOT_ID, getTenantEmail, isTenantEmailEditable, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-email";
@@ -135,16 +136,30 @@ import { publishPreconditionRefusal } from "./publish-preconditions.js";
135
136
  // `sanitizeMarketplaceDefinition` below would refuse an undecoded tree
136
137
  // rather than publish garbage, so this is the difference between the
137
138
  // feature working and a publish that always says the design is invalid.
138
- const nodes = decodeStoredNodes(versionSnapshot.get('nodes'));
139
- if (!nodes || !Object.keys(nodes).length) {
139
+ const stored = decodeStoredNodes(versionSnapshot.get('nodes'));
140
+ if (!stored || !Object.keys(stored).length) {
140
141
  return res.status(404).json({
141
142
  error: 'This email design is empty'
142
143
  });
143
144
  }
145
+ const rootId = (_versionSnapshot_get = versionSnapshot.get('rootId')) != null ? _versionSnapshot_get : EMAIL_NODE_ROOT_ID;
146
+ // The site's reusable blocks INLINED (AGL-3287): a listing is installed
147
+ // into another org, which has none of this site's components, and the
148
+ // sanitizer below refuses a component reference outright.
149
+ const inlined = await inlineReusableBlocks(stored, {
150
+ firestore,
151
+ hostId,
152
+ rootId
153
+ });
154
+ if (inlined.ok === false) {
155
+ return res.status(422).json({
156
+ error: inlined.error
157
+ });
158
+ }
159
+ const nodes = inlined.nodes;
144
160
  // Emails are built from `plugins-email` blocks only, so the page allowlist
145
161
  // is REPLACED rather than extended — see MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST
146
162
  // for why `emailHtml` is not on the list.
147
- const rootId = (_versionSnapshot_get = versionSnapshot.get('rootId')) != null ? _versionSnapshot_get : EMAIL_NODE_ROOT_ID;
148
163
  const sanitized = sanitizeMarketplaceDefinition({
149
164
  rootId,
150
165
  nodes
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-email-template.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 checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n EMAIL_NODE_ROOT_ID,\n getTenantEmail,\n isTenantEmailEditable,\n TENANT_EMAIL_COLLECTION,\n} from '@aglyn/shared-util-email'\nimport {\n MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST,\n marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\n\n/**\n * Publishes a site's designed transactional email to the marketplace\n * (AGL-657).\n *\n * An email design is a node tree like any other artifact, but it does NOT live\n * in the host's screen routing map — which is exactly why `publish-template`\n * could never capture one (it only sees `hostSnapshot.get('screens')`). The\n * tree lives at `hosts/{h}/emailTemplates/{key}/versions/{versionId}`, so this\n * reads it directly.\n *\n * `templateKey` is a fixed catalog key ('booking-confirmed', …), not a free\n * id: a published email means \"a design FOR this transactional email\", so the\n * key travels with the listing and the installer lands on the same key.\n */\nexport const publishEmailTemplateHandler: PluginApiHandler = async (\n req,\n res,\n) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const templateKey = String(req.body?.templateKey ?? '')\n const displayName = String(req.body?.displayName ?? '').slice(0, 80)\n const description = String(req.body?.description ?? '').slice(0, 500)\n const category = String(req.body?.category ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(req.body?.priceUsd ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343): a paid listing\n // under `MARKETPLACE_MIN_PRICE_USD` loses money on every sale, because\n // marketplace checkout is a destination charge whose Stripe fee is debited\n // from the platform's balance. Also enforced in `publishPreconditionRefusal`\n // below, so a door that drops this line is still covered.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\n }\n if (!hostId || !templateKey || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing hostId, templateKey, or displayName' })\n }\n const entry = getTenantEmail(templateKey)\n if (!entry || !isTenantEmailEditable(entry)) {\n return res.status(400).json({ error: 'Not a designable email template' })\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n const orgForHost = await getOrgForHost(hostId)\n if (!checkEntitlement(orgForHost?.org ?? {}, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n if (!orgForHost?.orgId) {\n return res.status(409).json({ error: 'Site has no owning organization' })\n }\n\n const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId)\n // Profile, payouts AND the publisher agreement, in one gate\n // (AGL-2282) — see `publishPreconditionRefusal`.\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'templates',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const templateRef = hostRef\n .collection(TENANT_EMAIL_COLLECTION)\n .doc(templateKey)\n const templateSnapshot = await templateRef.get()\n const activeVersionId = templateSnapshot.get('versionId')\n if (!templateSnapshot.exists || !activeVersionId) {\n return res.status(404).json({\n error: 'This email has no published design yet — design and save it first',\n })\n }\n const versionSnapshot = await templateRef\n .collection('versions')\n .doc(String(activeVersionId))\n .get()\n // Decoded, like the screen and layout publish paths (AGL-1395): the email\n // besigner now compresses through a converter as those do, and versions\n // written before it did are still plain maps, which come back unchanged.\n //\n // `sanitizeMarketplaceDefinition` below would refuse an undecoded tree\n // rather than publish garbage, so this is the difference between the\n // feature working and a publish that always says the design is invalid.\n const nodes = decodeStoredNodes<Record<string, any>>(\n versionSnapshot.get('nodes'),\n )\n if (!nodes || !Object.keys(nodes).length) {\n return res.status(404).json({ error: 'This email design is empty' })\n }\n // Emails are built from `plugins-email` blocks only, so the page allowlist\n // is REPLACED rather than extended — see MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST\n // for why `emailHtml` is not on the list.\n const rootId = versionSnapshot.get('rootId') ?? EMAIL_NODE_ROOT_ID\n const sanitized = sanitizeMarketplaceDefinition(\n { rootId, nodes },\n { componentIds: MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // One listing per (source site, template key).\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceHostId', '==', hostId)\n .where('emailTemplateKey', '==', templateKey)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'emailTemplate',\n sourceHostId: hostId,\n emailTemplateKey: templateKey,\n displayName: displayName.trim(),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n deletedAt: null,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({\n rootId: sanitized.rootId,\n nodes: sanitized.nodes,\n emailTemplateKey: templateKey,\n // The copy fields are part of the design, not the tree.\n subject: String(templateSnapshot.get('subject') ?? ''),\n preheader: String(templateSnapshot.get('preheader') ?? ''),\n publishedAt: now,\n })\n\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n\nexport default publishEmailTemplateHandler\n"],"names":["checkEntitlement","createResourceUid","decodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","EMAIL_NODE_ROOT_ID","getTenantEmail","isTenantEmailEditable","TENANT_EMAIL_COLLECTION","MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST","marketplacePriceRefusal","sanitizeMarketplaceDefinition","resolvePublisherProfile","publishPreconditionRefusal","publishEmailTemplateHandler","req","res","method","status","json","error","hostId","String","body","templateKey","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","entry","authorization","headers","idToken","startsWith","length","undefined","hostSnapshot","versionSnapshot","existing","templateSnapshot","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","orgForHost","org","orgId","publisher","refusal","sells","templateRef","activeVersionId","nodes","Object","keys","rootId","sanitized","componentIds","ok","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","set","profileId","artifactType","sourceHostId","emailTemplateKey","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","subject","preheader","listingId","id","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-email-template.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 checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { inlineReusableBlocks } from './publish-email-blocks'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n EMAIL_NODE_ROOT_ID,\n getTenantEmail,\n isTenantEmailEditable,\n TENANT_EMAIL_COLLECTION,\n} from '@aglyn/shared-util-email'\nimport {\n MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST,\n marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\n\n/**\n * Publishes a site's designed transactional email to the marketplace\n * (AGL-657).\n *\n * An email design is a node tree like any other artifact, but it does NOT live\n * in the host's screen routing map — which is exactly why `publish-template`\n * could never capture one (it only sees `hostSnapshot.get('screens')`). The\n * tree lives at `hosts/{h}/emailTemplates/{key}/versions/{versionId}`, so this\n * reads it directly.\n *\n * `templateKey` is a fixed catalog key ('booking-confirmed', …), not a free\n * id: a published email means \"a design FOR this transactional email\", so the\n * key travels with the listing and the installer lands on the same key.\n */\nexport const publishEmailTemplateHandler: PluginApiHandler = async (\n req,\n res,\n) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const templateKey = String(req.body?.templateKey ?? '')\n const displayName = String(req.body?.displayName ?? '').slice(0, 80)\n const description = String(req.body?.description ?? '').slice(0, 500)\n const category = String(req.body?.category ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(req.body?.priceUsd ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343): a paid listing\n // under `MARKETPLACE_MIN_PRICE_USD` loses money on every sale, because\n // marketplace checkout is a destination charge whose Stripe fee is debited\n // from the platform's balance. Also enforced in `publishPreconditionRefusal`\n // below, so a door that drops this line is still covered.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\n }\n if (!hostId || !templateKey || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing hostId, templateKey, or displayName' })\n }\n const entry = getTenantEmail(templateKey)\n if (!entry || !isTenantEmailEditable(entry)) {\n return res.status(400).json({ error: 'Not a designable email template' })\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n const orgForHost = await getOrgForHost(hostId)\n if (!checkEntitlement(orgForHost?.org ?? {}, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n if (!orgForHost?.orgId) {\n return res.status(409).json({ error: 'Site has no owning organization' })\n }\n\n const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId)\n // Profile, payouts AND the publisher agreement, in one gate\n // (AGL-2282) — see `publishPreconditionRefusal`.\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'templates',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const templateRef = hostRef\n .collection(TENANT_EMAIL_COLLECTION)\n .doc(templateKey)\n const templateSnapshot = await templateRef.get()\n const activeVersionId = templateSnapshot.get('versionId')\n if (!templateSnapshot.exists || !activeVersionId) {\n return res.status(404).json({\n error: 'This email has no published design yet — design and save it first',\n })\n }\n const versionSnapshot = await templateRef\n .collection('versions')\n .doc(String(activeVersionId))\n .get()\n // Decoded, like the screen and layout publish paths (AGL-1395): the email\n // besigner now compresses through a converter as those do, and versions\n // written before it did are still plain maps, which come back unchanged.\n //\n // `sanitizeMarketplaceDefinition` below would refuse an undecoded tree\n // rather than publish garbage, so this is the difference between the\n // feature working and a publish that always says the design is invalid.\n const stored = decodeStoredNodes<Record<string, any>>(\n versionSnapshot.get('nodes'),\n )\n if (!stored || !Object.keys(stored).length) {\n return res.status(404).json({ error: 'This email design is empty' })\n }\n const rootId = versionSnapshot.get('rootId') ?? EMAIL_NODE_ROOT_ID\n // The site's reusable blocks INLINED (AGL-3287): a listing is installed\n // into another org, which has none of this site's components, and the\n // sanitizer below refuses a component reference outright.\n const inlined = await inlineReusableBlocks(stored, {\n firestore,\n hostId,\n rootId,\n })\n if (inlined.ok === false) {\n return res.status(422).json({ error: inlined.error })\n }\n const nodes = inlined.nodes\n // Emails are built from `plugins-email` blocks only, so the page allowlist\n // is REPLACED rather than extended — see MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST\n // for why `emailHtml` is not on the list.\n const sanitized = sanitizeMarketplaceDefinition(\n { rootId, nodes },\n { componentIds: MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // One listing per (source site, template key).\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceHostId', '==', hostId)\n .where('emailTemplateKey', '==', templateKey)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'emailTemplate',\n sourceHostId: hostId,\n emailTemplateKey: templateKey,\n displayName: displayName.trim(),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n deletedAt: null,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({\n rootId: sanitized.rootId,\n nodes: sanitized.nodes,\n emailTemplateKey: templateKey,\n // The copy fields are part of the design, not the tree.\n subject: String(templateSnapshot.get('subject') ?? ''),\n preheader: String(templateSnapshot.get('preheader') ?? ''),\n publishedAt: now,\n })\n\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n\nexport default publishEmailTemplateHandler\n"],"names":["checkEntitlement","createResourceUid","decodeStoredNodes","inlineReusableBlocks","firebaseAdmin","getOrgForHost","resolveOrgPermissions","EMAIL_NODE_ROOT_ID","getTenantEmail","isTenantEmailEditable","TENANT_EMAIL_COLLECTION","MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST","marketplacePriceRefusal","sanitizeMarketplaceDefinition","resolvePublisherProfile","publishPreconditionRefusal","publishEmailTemplateHandler","req","res","method","status","json","error","hostId","String","body","templateKey","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","entry","authorization","headers","idToken","startsWith","length","undefined","hostSnapshot","versionSnapshot","existing","templateSnapshot","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","orgForHost","org","orgId","publisher","refusal","sells","templateRef","activeVersionId","stored","Object","keys","rootId","inlined","ok","nodes","sanitized","componentIds","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","set","profileId","artifactType","sourceHostId","emailTemplateKey","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","subject","preheader","listingId","id","console"],"mappings":"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"}