@aglyn/plugins-marketplace 1.0.0-beta.201 → 1.0.0-beta.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/package.json +14 -14
  2. package/src/lib/model/publisher-risk.d.ts +52 -0
  3. package/src/lib/model/publisher-risk.js +74 -0
  4. package/src/lib/model/publisher-risk.js.map +1 -0
  5. package/src/lib/server/billing-webhook.js +72 -4
  6. package/src/lib/server/billing-webhook.js.map +1 -1
  7. package/src/lib/server/checkout.js +18 -1
  8. package/src/lib/server/checkout.js.map +1 -1
  9. package/src/lib/server/connect.js +15 -1
  10. package/src/lib/server/connect.js.map +1 -1
  11. package/src/lib/server/install-dataset-schema.js +3 -1
  12. package/src/lib/server/install-dataset-schema.js.map +1 -1
  13. package/src/lib/server/install-email-starter.js +3 -1
  14. package/src/lib/server/install-email-starter.js.map +1 -1
  15. package/src/lib/server/install-email-template.js +3 -1
  16. package/src/lib/server/install-email-template.js.map +1 -1
  17. package/src/lib/server/install-layout.js +3 -1
  18. package/src/lib/server/install-layout.js.map +1 -1
  19. package/src/lib/server/install-plugin.js +3 -1
  20. package/src/lib/server/install-plugin.js.map +1 -1
  21. package/src/lib/server/install-template.js +3 -1
  22. package/src/lib/server/install-template.js.map +1 -1
  23. package/src/lib/server/install-theme.js +3 -1
  24. package/src/lib/server/install-theme.js.map +1 -1
  25. package/src/lib/server/install.js +3 -1
  26. package/src/lib/server/install.js.map +1 -1
  27. package/src/lib/server/listing-screen.d.ts +83 -0
  28. package/src/lib/server/listing-screen.js +244 -0
  29. package/src/lib/server/listing-screen.js.map +1 -0
  30. package/src/lib/server/publish-dataset-schema.js +12 -0
  31. package/src/lib/server/publish-dataset-schema.js.map +1 -1
  32. package/src/lib/server/publish-email-starter.js +15 -1
  33. package/src/lib/server/publish-email-starter.js.map +1 -1
  34. package/src/lib/server/publish-email-template.js +15 -1
  35. package/src/lib/server/publish-email-template.js.map +1 -1
  36. package/src/lib/server/publish-layout.js +15 -1
  37. package/src/lib/server/publish-layout.js.map +1 -1
  38. package/src/lib/server/publish-plugin.js +41 -3
  39. package/src/lib/server/publish-plugin.js.map +1 -1
  40. package/src/lib/server/publish-template.js +15 -1
  41. package/src/lib/server/publish-template.js.map +1 -1
  42. package/src/lib/server/publish-theme.js +14 -1
  43. package/src/lib/server/publish-theme.js.map +1 -1
  44. package/src/lib/server/publish.js +15 -1
  45. package/src/lib/server/publish.js.map +1 -1
  46. package/src/lib/server/publisher-profile-save.js +36 -0
  47. package/src/lib/server/publisher-profile-save.js.map +1 -1
  48. package/src/lib/server/publisher-review-email.js +15 -5
  49. package/src/lib/server/publisher-review-email.js.map +1 -1
  50. package/src/lib/server/reviews.d.ts +8 -0
  51. package/src/lib/server/reviews.js +43 -3
  52. package/src/lib/server/reviews.js.map +1 -1
  53. package/src/lib/server/sale-risk.d.ts +105 -0
  54. package/src/lib/server/sale-risk.js +315 -0
  55. package/src/lib/server/sale-risk.js.map +1 -0
  56. package/src/lib/server/update-artifact.js +3 -1
  57. package/src/lib/server/update-artifact.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/listing-screen.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * THE PHISHING SCREEN, AT MARKETPLACE SUBMISSION (AGL-3365).\n *\n * A listing is content a stranger reads and trusts — the marketplace is the\n * platform vouching for it — so it goes through the same screen as a\n * tenant's email and published page, with the same brand list, the same\n * lookalike rule and the same tiers (`signalsThatHold`), and it is held\n * BEFORE it is published rather than found afterwards:\n *\n * - its title, description, readme, changelog and links are read as a\n * message (`screenOutboundEmail`), with the publisher's display name as\n * the sender — a listing \"from\" PayPal is mail from PayPal;\n * - the nodes a template, layout, component or email template carries are\n * read as the page they will become (`screenHostedPage`), so a phishing\n * template never reaches a buyer's site through the catalog.\n *\n * What holds files the SAME abuse-queue row a held email or page files\n * (`fileOutboundHold`, `heldSend.kind: 'listing'`), and the submission is\n * refused with its reference. Staff dismiss the row to release it — the\n * publisher submits again and it goes through, since the row is keyed on the\n * publisher and the signals — or mark it actioned to reject it.\n *\n * Two NAME rules refuse outright rather than hold, because nothing is lost by\n * asking an honest publisher to rename:\n *\n * - a listing title or publisher name that claims to SPEAK FOR a brand\n * (`brandClaimedByName`: \"PayPal Support\", \"Official Aglyn Plugins\");\n * - a publisher display name or handle that wears the platform's own name at\n * all. A workspace owns any other brand by carrying its name; the\n * platform's is owned only by the platform's staff.\n *\n * Pure first: a clean submission costs no read.\n *=========================================*/\n\nimport { screenHostedPage } from '@aglyn/shared-util-email/hosted-page-screen'\nimport { ABUSE_REPORT_COLLECTION } from '@aglyn/aglyn/app-utils/abuse-report'\nimport {\n brandClaimedByName,\n brandForSubdomainLabel,\n phishingSignalTier,\n platformPhishingBrand,\n type PhishingScreenSignal,\n screenOutboundEmail,\n signalsThatHold,\n} from '@aglyn/shared-util-email/outbound-phishing-screen'\nimport { firebaseAdmin, notifyOrgAdmins } from '@aglyn/tenant-data-admin'\nimport { orgAgeDays } from '@aglyn/tenant-data-admin/server/org-age'\nimport {\n canonicalJson,\n fileOutboundHold,\n flaggedHostOf,\n heldOutboundReference,\n heldOutboundReviewId,\n outboundContentHash,\n} from '@aglyn/tenant-data-admin/server/outbound-send-review'\nimport type { PublishRefusal } from './publish-preconditions'\n\n/** What a submission says, as a stranger will read it. */\nexport interface ListingSubmissionContent {\n displayName?: string | null\n /** The publisher's display name, read as the sender. */\n publisherName?: string | null\n description?: string | null\n readme?: string | null\n changelog?: string | null\n /** Homepage, repository, support links. */\n urls?: readonly (string | null | undefined)[]\n /** Nodes that will be rendered as a page (template, layout, component). */\n pageNodes?: unknown\n /** Nodes that will be sent as an email (email template, starter). */\n emailNodes?: unknown\n}\n\nexport interface ListingScreenInput extends ListingSubmissionContent {\n /** The publishing workspace's own names. */\n ownNames?: readonly (string | null | undefined)[]\n /** The actor is the platform's staff — the one owner of its brand. */\n official?: boolean\n}\n\nexport interface ListingScreenFindings {\n /** A name rule the submission breaks — refused, never held. */\n impersonation: string | null\n /** Every signal the screen found, before the tiers. */\n signals: PhishingScreenSignal[]\n}\n\n/** Names that mention the platform are not \"own\" unless the actor is its staff. */\nfunction ownNamesFor(input: ListingScreenInput): string[] {\n const platform = platformPhishingBrand()\n return (input.ownNames ?? [])\n .filter((name): name is string => typeof name === 'string' && name.length > 0)\n .filter((name) => input.official === true || !platform.mention.test(name))\n}\n\n/**\n * The refusal for a publisher NAME or HANDLE, or null (AGL-3365): it wears\n * the platform's own name, or claims to speak for any brand, or — the handle\n * being a URL segment, like a subdomain — has a brand lookalike's shape.\n */\nexport function publisherIdentityImpersonation(input: {\n displayName?: string | null\n handle?: string | null\n official?: boolean\n}): string | null {\n if (input.official === true) return null\n const platform = platformPhishingBrand()\n const name = String(input.displayName ?? '')\n const handle = String(input.handle ?? '').toLowerCase()\n if (platform.mention.test(name) || platform.hostTokens.some((token) => handle.includes(token))) {\n return (\n `A publisher name or handle cannot use \"${platform.label}\" — the marketplace would ` +\n `present it as ${platform.label}'s own. Choose a name of your own.`\n )\n }\n const claimed = brandClaimedByName(name)\n if (claimed) {\n return `\"${name}\" reads as ${claimed.label} itself. Choose a name of your own.`\n }\n const lookalike = handle ? brandForSubdomainLabel(handle) : null\n if (lookalike) {\n return `The handle \"${handle}\" is shaped like ${lookalike.label}'s. Choose a handle of your own.`\n }\n return null\n}\n\n/** Screen one submission. Pure. */\nexport function screenListingSubmission(input: ListingScreenInput): ListingScreenFindings {\n const title = String(input.displayName ?? '')\n const claimed = input.official === true ? null : brandClaimedByName(title)\n const impersonation = claimed\n ? `A listing titled \"${title}\" reads as ${claimed.label} itself. Name what it does instead ` +\n `(for example \"… for ${claimed.label}\").`\n : null\n const ownNames = ownNamesFor(input)\n const email = screenOutboundEmail({\n subject: title,\n fromName: input.publisherName ?? null,\n bodies: [\n input.description,\n input.readme,\n input.changelog,\n ...(input.urls ?? []),\n input.emailNodes === undefined ? null : JSON.stringify(input.emailNodes),\n ],\n ownNames,\n })\n const page =\n input.pageNodes === undefined\n ? { signals: [] as PhishingScreenSignal[] }\n : screenHostedPage({ nodes: input.pageNodes, ownNames })\n const seen = new Set<string>()\n const signals = [...email.signals, ...page.signals].filter((signal) => {\n const key = canonicalJson(signal)\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n return { impersonation, signals }\n}\n\n/**\n * Tell the publisher's owners and admins a submission is held (AGL-3365). The\n * outcome and the way forward only — which rule held it stays with staff.\n * Staff hear from `fileOutboundHold`, the same alert a held email or page\n * raises. Never throws.\n */\nexport async function notifyPublisherSubmissionHeld(\n publisherOrgId: string,\n held: { title: string; reference: string },\n): Promise<void> {\n await notifyOrgAdmins(publisherOrgId, {\n type: 'marketplace.review',\n title: 'A marketplace submission is held for review',\n body:\n `\"${held.title}\" was not listed yet: it is waiting for a review by our team. If it is ` +\n 'released, submit it again and it will go through. To ask about it, contact support ' +\n `with reference ${held.reference}.`,\n link: '/manage/marketplace',\n }).catch(() => undefined)\n}\n\n/** The review row a publisher's held submission files, keyed on the publisher and the signals. */\nexport function listingReviewId(\n publisherOrgId: string,\n signals: readonly PhishingScreenSignal[],\n): string {\n const key = signals\n .map((signal) => canonicalJson(signal))\n .sort()\n .join('\\n')\n return heldOutboundReviewId(\n `publisherProfiles/${publisherOrgId}/submissions`,\n outboundContentHash([key]),\n )\n}\n\n/**\n * The whole gate for a marketplace submission: the refusal to send, or null\n * when it may be published. Every publish door and the listing edit call it\n * with what they are about to store (the sweep in\n * `listing-screen-doors.spec.ts` keeps it that way).\n */\nexport async function listingSubmissionRefusal(input: {\n publisherOrgId: string\n content: ListingSubmissionContent\n official?: boolean\n /** The publishing org's document, when the caller already holds it. */\n org?: Record<string, unknown> | null\n nowMs?: number\n}): Promise<PublishRefusal | null> {\n const first = screenListingSubmission({ ...input.content, official: input.official })\n if (first.impersonation) {\n return { status: 422, body: { error: first.impersonation, code: 'impersonation' } }\n }\n if (!first.signals.length) return null\n\n const nowMs = input.nowMs ?? Date.now()\n let reviewId = ''\n try {\n const org =\n input.org !== undefined\n ? input.org\n : ((\n await firebaseAdmin.app().firestore().collection('orgs').doc(input.publisherOrgId).get()\n ).data() ?? null)\n const text = (value: unknown) => (typeof value === 'string' ? value : '')\n // Again, now knowing whose submission it is: a brand in the workspace's\n // own name is its own (the platform's excepted — see `ownNamesFor`).\n const { signals } = screenListingSubmission({\n ...input.content,\n official: input.official,\n ownNames: [text(org?.['name'])],\n })\n const ageDays = orgAgeDays((org as { createdAt?: unknown } | null)?.createdAt, nowMs)\n const holding = signalsThatHold(signals, { ageDays })\n if (!holding.length) return null\n reviewId = listingReviewId(input.publisherOrgId, holding)\n const title = String(input.content.displayName ?? '').slice(0, 300) || 'a listing'\n // Whether this hold is new, so the publisher is told once, not on every\n // resubmission while it waits.\n const firstHold = !(\n await firebaseAdmin.app().firestore().collection(ABUSE_REPORT_COLLECTION).doc(reviewId).get()\n ).exists\n const state = await fileOutboundHold({\n reviewId,\n heldSend: {\n kind: 'listing',\n path: `publisherProfiles/${input.publisherOrgId}`,\n hostId: '',\n orgId: input.publisherOrgId,\n contentHash: outboundContentHash([holding]),\n subject: title,\n fromName: input.content.publisherName ?? null,\n signals: holding,\n state: 'held',\n heldAtMs: nowMs,\n ageDays,\n },\n headline:\n `Held a marketplace submission (\"${title}\") from publisher workspace ` +\n `${input.publisherOrgId}, ${ageDays ?? '?'} day(s) old. Nothing was published.`,\n alertTitle: 'Marketplace submission held for review — possible phishing',\n alertBody: `A marketplace submission (\"${title}\") was held before it was listed.`,\n url: null,\n reportedHostname: flaggedHostOf(holding),\n })\n if (state === 'released') return null\n const reference = heldOutboundReference(reviewId)\n if (state === 'held' && firstHold) {\n await notifyPublisherSubmissionHeld(input.publisherOrgId, { title, reference })\n }\n return {\n status: 409,\n body: {\n // The outcome and what to do — never which rule held it.\n error:\n state === 'rejected'\n ? `This submission was reviewed and cannot be published. Reference ${reference}.`\n : 'This submission is held for a review before it can be published. Nothing was ' +\n 'listed. Our team will look at it; if it is released, submit it again and it will ' +\n `go through. To ask about it, contact support with reference ${reference}.`,\n code: state === 'rejected' ? 'rejected' : 'held-for-review',\n reference,\n },\n }\n } catch (error) {\n // Closed only for what holds whoever the publisher is, the page screen's\n // posture: a soft signal needs the workspace's age to hold at all.\n if (!first.signals.some((signal) => phishingSignalTier(signal) === 'strong')) {\n console.error('[listing-screen] a submission could not be reviewed — allowing it', error)\n return null\n }\n console.error('[listing-screen] a flagged submission could not be reviewed — refusing it', error)\n return {\n status: 503,\n body: {\n error: 'This submission could not be reviewed just now — try again in a minute.',\n ...(reviewId ? { reference: heldOutboundReference(reviewId) } : {}),\n },\n }\n }\n}\n"],"names":["screenHostedPage","ABUSE_REPORT_COLLECTION","brandClaimedByName","brandForSubdomainLabel","phishingSignalTier","platformPhishingBrand","screenOutboundEmail","signalsThatHold","firebaseAdmin","notifyOrgAdmins","orgAgeDays","canonicalJson","fileOutboundHold","flaggedHostOf","heldOutboundReference","heldOutboundReviewId","outboundContentHash","ownNamesFor","input","platform","ownNames","filter","name","length","official","mention","test","publisherIdentityImpersonation","String","displayName","handle","toLowerCase","hostTokens","some","token","includes","label","claimed","lookalike","screenListingSubmission","title","impersonation","email","subject","fromName","publisherName","bodies","description","readme","changelog","urls","emailNodes","undefined","JSON","stringify","page","pageNodes","signals","nodes","seen","Set","signal","key","has","add","notifyPublisherSubmissionHeld","publisherOrgId","held","type","body","reference","link","catch","listingReviewId","map","sort","join","listingSubmissionRefusal","first","content","status","error","code","nowMs","Date","now","reviewId","org","app","firestore","collection","doc","get","data","text","value","ageDays","createdAt","holding","slice","firstHold","exists","state","heldSend","kind","path","hostId","orgId","contentHash","heldAtMs","headline","alertTitle","alertBody","url","reportedHostname","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAgC2C,GAE3C,SAASA,gBAAgB,QAAQ,8CAA6C;AAC9E,SAASC,uBAAuB,QAAQ,sCAAqC;AAC7E,SACEC,kBAAkB,EAClBC,sBAAsB,EACtBC,kBAAkB,EAClBC,qBAAqB,EAErBC,mBAAmB,EACnBC,eAAe,QACV,oDAAmD;AAC1D,SAASC,aAAa,EAAEC,eAAe,QAAQ,2BAA0B;AACzE,SAASC,UAAU,QAAQ,0CAAyC;AACpE,SACEC,aAAa,EACbC,gBAAgB,EAChBC,aAAa,EACbC,qBAAqB,EACrBC,oBAAoB,EACpBC,mBAAmB,QACd,uDAAsD;AAiC7D,iFAAiF,GACjF,SAASC,YAAYC,KAAyB;QAEpCA;IADR,MAAMC,WAAWd;IACjB,OAAO,EAACa,kBAAAA,MAAME,QAAQ,YAAdF,kBAAkB,EAAE,EACzBG,MAAM,CAAC,CAACC,OAAyB,OAAOA,SAAS,YAAYA,KAAKC,MAAM,GAAG,GAC3EF,MAAM,CAAC,CAACC,OAASJ,MAAMM,QAAQ,KAAK,QAAQ,CAACL,SAASM,OAAO,CAACC,IAAI,CAACJ;AACxE;AAEA;;;;CAIC,GACD,OAAO,SAASK,+BAA+BT,KAI9C;QAGqBA,oBACEA;IAHtB,IAAIA,MAAMM,QAAQ,KAAK,MAAM,OAAO;IACpC,MAAML,WAAWd;IACjB,MAAMiB,OAAOM,QAAOV,qBAAAA,MAAMW,WAAW,YAAjBX,qBAAqB;IACzC,MAAMY,SAASF,QAAOV,gBAAAA,MAAMY,MAAM,YAAZZ,gBAAgB,IAAIa,WAAW;IACrD,IAAIZ,SAASM,OAAO,CAACC,IAAI,CAACJ,SAASH,SAASa,UAAU,CAACC,IAAI,CAAC,CAACC,QAAUJ,OAAOK,QAAQ,CAACD,SAAS;QAC9F,OACE,CAAC,uCAAuC,EAAEf,SAASiB,KAAK,CAAC,0BAA0B,CAAC,GACpF,CAAC,cAAc,EAAEjB,SAASiB,KAAK,CAAC,kCAAkC,CAAC;IAEvE;IACA,MAAMC,UAAUnC,mBAAmBoB;IACnC,IAAIe,SAAS;QACX,OAAO,CAAC,CAAC,EAAEf,KAAK,WAAW,EAAEe,QAAQD,KAAK,CAAC,mCAAmC,CAAC;IACjF;IACA,MAAME,YAAYR,SAAS3B,uBAAuB2B,UAAU;IAC5D,IAAIQ,WAAW;QACb,OAAO,CAAC,YAAY,EAAER,OAAO,iBAAiB,EAAEQ,UAAUF,KAAK,CAAC,gCAAgC,CAAC;IACnG;IACA,OAAO;AACT;AAEA,iCAAiC,GACjC,OAAO,SAASG,wBAAwBrB,KAAyB;QAC1CA,oBASTA,sBAKJA;IAdR,MAAMsB,QAAQZ,QAAOV,qBAAAA,MAAMW,WAAW,YAAjBX,qBAAqB;IAC1C,MAAMmB,UAAUnB,MAAMM,QAAQ,KAAK,OAAO,OAAOtB,mBAAmBsC;IACpE,MAAMC,gBAAgBJ,UAClB,CAAC,kBAAkB,EAAEG,MAAM,WAAW,EAAEH,QAAQD,KAAK,CAAC,mCAAmC,CAAC,GAC1F,CAAC,oBAAoB,EAAEC,QAAQD,KAAK,CAAC,GAAG,CAAC,GACzC;IACJ,MAAMhB,WAAWH,YAAYC;IAC7B,MAAMwB,QAAQpC,oBAAoB;QAChCqC,SAASH;QACTI,QAAQ,GAAE1B,uBAAAA,MAAM2B,aAAa,YAAnB3B,uBAAuB;QACjC4B,QAAQ;YACN5B,MAAM6B,WAAW;YACjB7B,MAAM8B,MAAM;YACZ9B,MAAM+B,SAAS;gBACX/B,cAAAA,MAAMgC,IAAI,YAAVhC,cAAc,EAAE;YACpBA,MAAMiC,UAAU,KAAKC,YAAY,OAAOC,KAAKC,SAAS,CAACpC,MAAMiC,UAAU;SACxE;QACD/B;IACF;IACA,MAAMmC,OACJrC,MAAMsC,SAAS,KAAKJ,YAChB;QAAEK,SAAS,EAAE;IAA2B,IACxCzD,iBAAiB;QAAE0D,OAAOxC,MAAMsC,SAAS;QAAEpC;IAAS;IAC1D,MAAMuC,OAAO,IAAIC;IACjB,MAAMH,UAAU;WAAIf,MAAMe,OAAO;WAAKF,KAAKE,OAAO;KAAC,CAACpC,MAAM,CAAC,CAACwC;QAC1D,MAAMC,MAAMnD,cAAckD;QAC1B,IAAIF,KAAKI,GAAG,CAACD,MAAM,OAAO;QAC1BH,KAAKK,GAAG,CAACF;QACT,OAAO;IACT;IACA,OAAO;QAAErB;QAAegB;IAAQ;AAClC;AAEA;;;;;CAKC,GACD,OAAO,eAAeQ,8BACpBC,cAAsB,EACtBC,IAA0C;IAE1C,MAAM1D,gBAAgByD,gBAAgB;QACpCE,MAAM;QACN5B,OAAO;QACP6B,MACE,CAAC,CAAC,EAAEF,KAAK3B,KAAK,CAAC,uEAAuE,CAAC,GACvF,wFACA,CAAC,eAAe,EAAE2B,KAAKG,SAAS,CAAC,CAAC,CAAC;QACrCC,MAAM;IACR,GAAGC,KAAK,CAAC,IAAMpB;AACjB;AAEA,gGAAgG,GAChG,OAAO,SAASqB,gBACdP,cAAsB,EACtBT,OAAwC;IAExC,MAAMK,MAAML,QACTiB,GAAG,CAAC,CAACb,SAAWlD,cAAckD,SAC9Bc,IAAI,GACJC,IAAI,CAAC;IACR,OAAO7D,qBACL,CAAC,kBAAkB,EAAEmD,eAAe,YAAY,CAAC,EACjDlD,oBAAoB;QAAC8C;KAAI;AAE7B;AAEA;;;;;CAKC,GACD,OAAO,eAAee,yBAAyB3D,KAO9C;QAOeA;IANd,MAAM4D,QAAQvC,wBAAwB,aAAKrB,MAAM6D,OAAO;QAAEvD,UAAUN,MAAMM,QAAQ;;IAClF,IAAIsD,MAAMrC,aAAa,EAAE;QACvB,OAAO;YAAEuC,QAAQ;YAAKX,MAAM;gBAAEY,OAAOH,MAAMrC,aAAa;gBAAEyC,MAAM;YAAgB;QAAE;IACpF;IACA,IAAI,CAACJ,MAAMrB,OAAO,CAAClC,MAAM,EAAE,OAAO;IAElC,MAAM4D,SAAQjE,eAAAA,MAAMiE,KAAK,YAAXjE,eAAekE,KAAKC,GAAG;IACrC,IAAIC,WAAW;IACf,IAAI;YAIK,OAecpE,4BAePA;QAjCd,MAAMqE,MACJrE,MAAMqE,GAAG,KAAKnC,YACVlC,MAAMqE,GAAG,IACR,QAAA,AACC,CAAA,MAAM/E,cAAcgF,GAAG,GAAGC,SAAS,GAAGC,UAAU,CAAC,QAAQC,GAAG,CAACzE,MAAMgD,cAAc,EAAE0B,GAAG,EAAC,EACvFC,IAAI,cAFL,QAEW;QAClB,MAAMC,OAAO,CAACC,QAAoB,OAAOA,UAAU,WAAWA,QAAQ;QACtE,wEAAwE;QACxE,qEAAqE;QACrE,MAAM,EAAEtC,OAAO,EAAE,GAAGlB,wBAAwB,aACvCrB,MAAM6D,OAAO;YAChBvD,UAAUN,MAAMM,QAAQ;YACxBJ,UAAU;gBAAC0E,KAAKP,uBAAAA,GAAK,CAAC,OAAO;aAAE;;QAEjC,MAAMS,UAAUtF,WAAY6E,uBAAD,AAACA,IAAwCU,SAAS,EAAEd;QAC/E,MAAMe,UAAU3F,gBAAgBkD,SAAS;YAAEuC;QAAQ;QACnD,IAAI,CAACE,QAAQ3E,MAAM,EAAE,OAAO;QAC5B+D,WAAWb,gBAAgBvD,MAAMgD,cAAc,EAAEgC;QACjD,MAAM1D,QAAQZ,QAAOV,6BAAAA,MAAM6D,OAAO,CAAClD,WAAW,YAAzBX,6BAA6B,IAAIiF,KAAK,CAAC,GAAG,QAAQ;QACvE,wEAAwE;QACxE,+BAA+B;QAC/B,MAAMC,YAAY,CAAC,AACjB,CAAA,MAAM5F,cAAcgF,GAAG,GAAGC,SAAS,GAAGC,UAAU,CAACzF,yBAAyB0F,GAAG,CAACL,UAAUM,GAAG,EAAC,EAC5FS,MAAM;QACR,MAAMC,QAAQ,MAAM1F,iBAAiB;YACnC0E;YACAiB,UAAU;gBACRC,MAAM;gBACNC,MAAM,CAAC,kBAAkB,EAAEvF,MAAMgD,cAAc,EAAE;gBACjDwC,QAAQ;gBACRC,OAAOzF,MAAMgD,cAAc;gBAC3B0C,aAAa5F,oBAAoB;oBAACkF;iBAAQ;gBAC1CvD,SAASH;gBACTI,QAAQ,GAAE1B,+BAAAA,MAAM6D,OAAO,CAAClC,aAAa,YAA3B3B,+BAA+B;gBACzCuC,SAASyC;gBACTI,OAAO;gBACPO,UAAU1B;gBACVa;YACF;YACAc,UACE,CAAC,gCAAgC,EAAEtE,MAAM,4BAA4B,CAAC,GACtE,GAAGtB,MAAMgD,cAAc,CAAC,EAAE,EAAE8B,kBAAAA,UAAW,IAAI,mCAAmC,CAAC;YACjFe,YAAY;YACZC,WAAW,CAAC,2BAA2B,EAAExE,MAAM,iCAAiC,CAAC;YACjFyE,KAAK;YACLC,kBAAkBrG,cAAcqF;QAClC;QACA,IAAII,UAAU,YAAY,OAAO;QACjC,MAAMhC,YAAYxD,sBAAsBwE;QACxC,IAAIgB,UAAU,UAAUF,WAAW;YACjC,MAAMnC,8BAA8B/C,MAAMgD,cAAc,EAAE;gBAAE1B;gBAAO8B;YAAU;QAC/E;QACA,OAAO;YACLU,QAAQ;YACRX,MAAM;gBACJ,yDAAyD;gBACzDY,OACEqB,UAAU,aACN,CAAC,gEAAgE,EAAEhC,UAAU,CAAC,CAAC,GAC/E,kFACA,sFACA,CAAC,4DAA4D,EAAEA,UAAU,CAAC,CAAC;gBACjFY,MAAMoB,UAAU,aAAa,aAAa;gBAC1ChC;YACF;QACF;IACF,EAAE,OAAOW,OAAO;QACd,yEAAyE;QACzE,mEAAmE;QACnE,IAAI,CAACH,MAAMrB,OAAO,CAACxB,IAAI,CAAC,CAAC4B,SAAWzD,mBAAmByD,YAAY,WAAW;YAC5EsD,QAAQlC,KAAK,CAAC,qEAAqEA;YACnF,OAAO;QACT;QACAkC,QAAQlC,KAAK,CAAC,6EAA6EA;QAC3F,OAAO;YACLD,QAAQ;YACRX,MAAM;gBACJY,OAAO;eACHK,WAAW;gBAAEhB,WAAWxD,sBAAsBwE;YAAU,IAAI,CAAC;QAErE;IACF;AACF"}
@@ -21,6 +21,7 @@ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
21
21
  import { marketplacePriceRefusal, sanitizeDatasetSchema } from "../model/index.js";
22
22
  import { resolvePublisherProfile } from "./publisher-profile.js";
23
23
  import { publishPreconditionRefusal } from "./publish-preconditions.js";
24
+ import { listingSubmissionRefusal } from "./listing-screen.js";
24
25
  import { refreshListingQueryFields } from "./listing-query-fields.js";
25
26
  /**
26
27
  * Publishes an org dataset's SCHEMA to the marketplace (AGL-657).
@@ -159,6 +160,17 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
159
160
  const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
160
161
  const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
161
162
  const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
163
+ // The phishing screen, before anything is listed (AGL-3365).
164
+ const screened = await listingSubmissionRefusal({
165
+ publisherOrgId: publisher.orgId,
166
+ content: {
167
+ displayName,
168
+ publisherName: publisher.displayName,
169
+ description
170
+ },
171
+ official: decoded['staff'] === true
172
+ });
173
+ if (screened) return res.status(screened.status).json(screened.body);
162
174
  await listingRef.set(_extends({
163
175
  profileId: publisher.orgId,
164
176
  artifactType: 'datasetSchema',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-dataset-schema.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 { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport { checkEntitlement, createResourceUid } from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal, sanitizeDatasetSchema } from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { refreshListingQueryFields } from './listing-query-fields'\n\n/**\n * Publishes an org dataset's SCHEMA to the marketplace (AGL-657).\n *\n * Unlike every other publish route this one takes an `orgId` rather than a\n * `hostId`: datasets live at `orgs/{orgId}/datasets/{id}` and are org-shared\n * (AGL-237), so there is no source site to derive the org from. The role gate\n * goes through `resolveOrgPermissions` directly for the same reason.\n *\n * Records NEVER travel — `sanitizeDatasetSchema` reads the model and this\n * handler never touches the `records` subcollection. A dataset's rows are the\n * org's customer data; only the shape is publishable.\n */\nexport const publishDatasetSchemaHandler: 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 orgId = String(req.body?.orgId ?? '')\n const datasetId = String(req.body?.datasetId ?? '')\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 (!orgId || !datasetId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing orgId, datasetId, 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, { orgId })\n if (membership.orgId !== orgId || !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 orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown organization' })\n }\n if (!checkEntitlement(orgSnapshot.data() as any, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n\n const publisher = await resolvePublisherProfile(firestore, 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: 'schemas',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const datasetSnapshot = await orgRef\n .collection('datasets')\n .doc(datasetId)\n .get()\n const dataset = datasetSnapshot.data() as any\n if (!datasetSnapshot.exists || dataset?.deletedAt) {\n return res.status(404).json({ error: 'Unknown dataset' })\n }\n // v1 datasets carry a flat `fields: string[]` with no model; publish the\n // derived shape so unmigrated datasets are publishable too (AGL-102 shim,\n // inlined rather than importing core's deriveModelFromFields to keep the\n // sanitizer's input one plain shape).\n const model = dataset?.model?.fields\n ? dataset.model\n : {\n fields: Object.fromEntries(\n (Array.isArray(dataset?.fields) ? dataset.fields : []).map(\n (name: unknown) => [String(name), { name: String(name), type: 'text' }],\n ),\n ),\n order: (Array.isArray(dataset?.fields) ? dataset.fields : []).map(String),\n }\n\n const sanitized = sanitizeDatasetSchema(model)\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // Reference fields name a dataset id that means nothing outside this org,\n // so resolve each target's display name now — the installer relinks by\n // that label (see resolveInstalledDatasetSchema).\n const referenced = new Set(\n Object.values(sanitized.schema.fields)\n .map((field) => field.reference?.datasetId)\n .filter((id): id is string => Boolean(id)),\n )\n if (referenced.size) {\n const labels = new Map<string, string>()\n await Promise.all(\n [...referenced].map(async (id) => {\n const snapshot = await orgRef.collection('datasets').doc(id).get()\n const label = snapshot.get('displayName')\n if (label) labels.set(id, String(label))\n }),\n )\n for (const field of Object.values(sanitized.schema.fields)) {\n const target = field.reference?.datasetId\n if (target && labels.has(target)) {\n field.reference = {\n ...field.reference,\n datasetLabel: labels.get(target),\n }\n }\n }\n }\n\n // One listing per source dataset: re-publish bumps latestVersion.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceDatasetId', '==', datasetId)\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: 'datasetSchema',\n sourceDatasetId: datasetId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321): the template gallery's\n // marketplace shelf asks `nameTokens` on its query.\n ...displayNameSearchFields(displayName.trim()),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n fieldCount: sanitized.schema.order.length,\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 // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({ datasetSchema: sanitized.schema, publishedAt: now })\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 publishDatasetSchemaHandler\n"],"names":["displayNameSearchFields","checkEntitlement","createResourceUid","firebaseAdmin","resolveOrgPermissions","marketplacePriceRefusal","sanitizeDatasetSchema","resolvePublisherProfile","publishPreconditionRefusal","refreshListingQueryFields","publishDatasetSchemaHandler","req","res","method","status","json","error","orgId","String","body","datasetId","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","length","undefined","existing","dataset","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","orgRef","collection","doc","orgSnapshot","get","exists","data","publisher","refusal","sells","datasetSnapshot","deletedAt","model","fields","Object","fromEntries","Array","isArray","map","name","type","order","sanitized","ok","referenced","Set","values","schema","field","reference","filter","id","Boolean","size","labels","Map","Promise","all","snapshot","label","set","target","has","datasetLabel","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","profileId","artifactType","sourceDatasetId","latestVersion","fieldCount","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","datasetSchema","listingId","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-dataset-schema.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 { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport { checkEntitlement, createResourceUid } from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal, sanitizeDatasetSchema } from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\n\n/**\n * Publishes an org dataset's SCHEMA to the marketplace (AGL-657).\n *\n * Unlike every other publish route this one takes an `orgId` rather than a\n * `hostId`: datasets live at `orgs/{orgId}/datasets/{id}` and are org-shared\n * (AGL-237), so there is no source site to derive the org from. The role gate\n * goes through `resolveOrgPermissions` directly for the same reason.\n *\n * Records NEVER travel — `sanitizeDatasetSchema` reads the model and this\n * handler never touches the `records` subcollection. A dataset's rows are the\n * org's customer data; only the shape is publishable.\n */\nexport const publishDatasetSchemaHandler: 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 orgId = String(req.body?.orgId ?? '')\n const datasetId = String(req.body?.datasetId ?? '')\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 (!orgId || !datasetId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing orgId, datasetId, 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, { orgId })\n if (membership.orgId !== orgId || !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 orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown organization' })\n }\n if (!checkEntitlement(orgSnapshot.data() as any, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n\n const publisher = await resolvePublisherProfile(firestore, 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: 'schemas',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const datasetSnapshot = await orgRef\n .collection('datasets')\n .doc(datasetId)\n .get()\n const dataset = datasetSnapshot.data() as any\n if (!datasetSnapshot.exists || dataset?.deletedAt) {\n return res.status(404).json({ error: 'Unknown dataset' })\n }\n // v1 datasets carry a flat `fields: string[]` with no model; publish the\n // derived shape so unmigrated datasets are publishable too (AGL-102 shim,\n // inlined rather than importing core's deriveModelFromFields to keep the\n // sanitizer's input one plain shape).\n const model = dataset?.model?.fields\n ? dataset.model\n : {\n fields: Object.fromEntries(\n (Array.isArray(dataset?.fields) ? dataset.fields : []).map(\n (name: unknown) => [String(name), { name: String(name), type: 'text' }],\n ),\n ),\n order: (Array.isArray(dataset?.fields) ? dataset.fields : []).map(String),\n }\n\n const sanitized = sanitizeDatasetSchema(model)\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // Reference fields name a dataset id that means nothing outside this org,\n // so resolve each target's display name now — the installer relinks by\n // that label (see resolveInstalledDatasetSchema).\n const referenced = new Set(\n Object.values(sanitized.schema.fields)\n .map((field) => field.reference?.datasetId)\n .filter((id): id is string => Boolean(id)),\n )\n if (referenced.size) {\n const labels = new Map<string, string>()\n await Promise.all(\n [...referenced].map(async (id) => {\n const snapshot = await orgRef.collection('datasets').doc(id).get()\n const label = snapshot.get('displayName')\n if (label) labels.set(id, String(label))\n }),\n )\n for (const field of Object.values(sanitized.schema.fields)) {\n const target = field.reference?.datasetId\n if (target && labels.has(target)) {\n field.reference = {\n ...field.reference,\n datasetLabel: labels.get(target),\n }\n }\n }\n }\n\n // One listing per source dataset: re-publish bumps latestVersion.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceDatasetId', '==', datasetId)\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 // The phishing screen, before anything is listed (AGL-3365).\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n },\n official: decoded['staff'] === true,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'datasetSchema',\n sourceDatasetId: datasetId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321): the template gallery's\n // marketplace shelf asks `nameTokens` on its query.\n ...displayNameSearchFields(displayName.trim()),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n fieldCount: sanitized.schema.order.length,\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 // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({ datasetSchema: sanitized.schema, publishedAt: now })\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 publishDatasetSchemaHandler\n"],"names":["displayNameSearchFields","checkEntitlement","createResourceUid","firebaseAdmin","resolveOrgPermissions","marketplacePriceRefusal","sanitizeDatasetSchema","resolvePublisherProfile","publishPreconditionRefusal","listingSubmissionRefusal","refreshListingQueryFields","publishDatasetSchemaHandler","req","res","method","status","json","error","orgId","String","body","datasetId","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","length","undefined","existing","dataset","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","orgRef","collection","doc","orgSnapshot","get","exists","data","publisher","refusal","sells","datasetSnapshot","deletedAt","model","fields","Object","fromEntries","Array","isArray","map","name","type","order","sanitized","ok","referenced","Set","values","schema","field","reference","filter","id","Boolean","size","labels","Map","Promise","all","snapshot","label","set","target","has","datasetLabel","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","official","profileId","artifactType","sourceDatasetId","latestVersion","fieldCount","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","datasetSchema","listingId","console"],"mappings":"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"}
@@ -23,6 +23,7 @@ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
23
23
  import { emailStarterRefusal, inspectEmailStarter, MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, marketplacePriceRefusal, sanitizeMarketplaceDefinition } from "../model/index.js";
24
24
  import { resolvePublisherProfile } from "./publisher-profile.js";
25
25
  import { publishPreconditionRefusal } from "./publish-preconditions.js";
26
+ import { listingSubmissionRefusal } from "./listing-screen.js";
26
27
  import { refreshListingQueryFields } from "./listing-query-fields.js";
27
28
  /**
28
29
  * Publishes a site's campaign email design as a marketplace starter.
@@ -77,7 +78,7 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
77
78
  error: 'Unauthenticated'
78
79
  });
79
80
  try {
80
- var _hostSnapshot_get, _ref6, _existing_docs__get, _screen_emailSubject, _screen_emailPreheader;
81
+ var _hostSnapshot_get, _ref6, _existing_docs__get, _orgForHost_org, _screen_emailSubject, _screen_emailPreheader;
81
82
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
82
83
  const membership = await resolveOrgPermissions(decoded.uid, {
83
84
  hostId
@@ -199,6 +200,19 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
199
200
  const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
200
201
  const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
201
202
  const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
203
+ // The phishing screen, before anything is listed (AGL-3365).
204
+ const screened = await listingSubmissionRefusal({
205
+ publisherOrgId: publisher.orgId,
206
+ content: {
207
+ displayName,
208
+ publisherName: publisher.displayName,
209
+ description,
210
+ emailNodes: sanitized.nodes
211
+ },
212
+ official: decoded['staff'] === true,
213
+ org: (_orgForHost_org = orgForHost.org) != null ? _orgForHost_org : null
214
+ });
215
+ if (screened) return res.status(screened.status).json(screened.body);
202
216
  await listingRef.set(_extends({
203
217
  profileId: publisher.orgId,
204
218
  artifactType: 'emailStarter',
@@ -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 { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\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'\nimport { refreshListingQueryFields } from './listing-query-fields'\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 // The keys a listing is searched by (AGL-3321): the template gallery's\n // marketplace shelf asks `nameTokens` on its query.\n ...displayNameSearchFields(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 // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\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":["displayNameSearchFields","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","inlineReusableBlocks","firebaseAdmin","getOrgForHost","resolveOrgPermissions","emailStarterRefusal","inspectEmailStarter","MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","marketplacePriceRefusal","sanitizeMarketplaceDefinition","resolvePublisherProfile","publishPreconditionRefusal","refreshListingQueryFields","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":"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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 { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\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'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\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 // The phishing screen, before anything is listed (AGL-3365).\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n emailNodes: sanitized.nodes,\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'emailStarter',\n sourceHostId: hostId,\n sourceScreenId: screenId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321): the template gallery's\n // marketplace shelf asks `nameTokens` on its query.\n ...displayNameSearchFields(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 // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\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":["displayNameSearchFields","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","inlineReusableBlocks","firebaseAdmin","getOrgForHost","resolveOrgPermissions","emailStarterRefusal","inspectEmailStarter","MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","marketplacePriceRefusal","sanitizeMarketplaceDefinition","resolvePublisherProfile","publishPreconditionRefusal","listingSubmissionRefusal","refreshListingQueryFields","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","orgForHost","screen","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","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","screened","publisherOrgId","content","publisherName","emailNodes","official","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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"}
@@ -23,6 +23,7 @@ import { EMAIL_NODE_ROOT_ID, getTenantEmail, isTenantEmailEditable, TENANT_EMAIL
23
23
  import { MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST, marketplacePriceRefusal, sanitizeMarketplaceDefinition } from "../model/index.js";
24
24
  import { resolvePublisherProfile } from "./publisher-profile.js";
25
25
  import { publishPreconditionRefusal } from "./publish-preconditions.js";
26
+ import { listingSubmissionRefusal } from "./listing-screen.js";
26
27
  import { refreshListingQueryFields } from "./listing-query-fields.js";
27
28
  /**
28
29
  * Publishes a site's designed transactional email to the marketplace
@@ -79,7 +80,7 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
79
80
  error: 'Unauthenticated'
80
81
  });
81
82
  try {
82
- var _hostSnapshot_get, _ref6, _versionSnapshot_get, _existing_docs__get, _templateSnapshot_get, _templateSnapshot_get1;
83
+ var _hostSnapshot_get, _ref6, _versionSnapshot_get, _existing_docs__get, _orgForHost_org, _templateSnapshot_get, _templateSnapshot_get1;
83
84
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
84
85
  const membership = await resolveOrgPermissions(decoded.uid, {
85
86
  hostId
@@ -178,6 +179,19 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
178
179
  const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
179
180
  const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
180
181
  const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
182
+ // The phishing screen, before anything is listed (AGL-3365).
183
+ const screened = await listingSubmissionRefusal({
184
+ publisherOrgId: publisher.orgId,
185
+ content: {
186
+ displayName,
187
+ publisherName: publisher.displayName,
188
+ description,
189
+ emailNodes: sanitized.nodes
190
+ },
191
+ official: decoded['staff'] === true,
192
+ org: (_orgForHost_org = orgForHost.org) != null ? _orgForHost_org : null
193
+ });
194
+ if (screened) return res.status(screened.status).json(screened.body);
181
195
  await listingRef.set(_extends({
182
196
  profileId: publisher.orgId,
183
197
  artifactType: 'emailTemplate',
@@ -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 { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\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'\nimport { refreshListingQueryFields } from './listing-query-fields'\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 // The keys a listing is searched by (AGL-3321): the template gallery's\n // marketplace shelf asks `nameTokens` on its query.\n ...displayNameSearchFields(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 // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\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":["displayNameSearchFields","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","refreshListingQueryFields","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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"}
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 { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\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'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\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 // The phishing screen, before anything is listed (AGL-3365).\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n emailNodes: sanitized.nodes,\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'emailTemplate',\n sourceHostId: hostId,\n emailTemplateKey: templateKey,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321): the template gallery's\n // marketplace shelf asks `nameTokens` on its query.\n ...displayNameSearchFields(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 // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\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":["displayNameSearchFields","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","listingSubmissionRefusal","refreshListingQueryFields","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","orgForHost","templateSnapshot","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","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","screened","publisherOrgId","content","publisherName","emailNodes","official","set","profileId","artifactType","sourceHostId","emailTemplateKey","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","subject","preheader","listingId","id","console"],"mappings":"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"}
@@ -22,6 +22,7 @@ import { marketplacePriceRefusal, sanitizeMarketplaceDefinition } from "../model
22
22
  import { sanitizeMarketplaceProps } from "../model/marketplace-props.js";
23
23
  import { resolvePublisherProfile } from "./publisher-profile.js";
24
24
  import { publishPreconditionRefusal } from "./publish-preconditions.js";
25
+ import { listingSubmissionRefusal } from "./listing-screen.js";
25
26
  import { refreshListingQueryFields } from "./listing-query-fields.js";
26
27
  /** The node marking where a bound screen's content grafts in. */ const LAYOUT_SLOT_COMPONENT_ID = 'layoutSlot';
27
28
  /**
@@ -72,7 +73,7 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
72
73
  error: 'Unauthenticated'
73
74
  });
74
75
  try {
75
- var _hostSnapshot_get, _ref6, _existing_docs__get;
76
+ var _hostSnapshot_get, _ref6, _existing_docs__get, _orgForHost_org;
76
77
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
77
78
  const membership = await resolveOrgPermissions(decoded.uid, {
78
79
  hostId
@@ -194,6 +195,19 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
194
195
  const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
195
196
  const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
196
197
  const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
198
+ // The phishing screen, before anything is listed (AGL-3365).
199
+ const screened = await listingSubmissionRefusal({
200
+ publisherOrgId: publisher.orgId,
201
+ content: {
202
+ displayName,
203
+ publisherName: publisher.displayName,
204
+ description,
205
+ pageNodes: sanitized.nodes
206
+ },
207
+ official: decoded['staff'] === true,
208
+ org: (_orgForHost_org = orgForHost.org) != null ? _orgForHost_org : null
209
+ });
210
+ if (screened) return res.status(screened.status).json(screened.body);
197
211
  await listingRef.set(_extends({
198
212
  artifactType: 'layout',
199
213
  profileId: publisher.orgId,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-layout.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 { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n CANVAS_ROOT_ELEMENT_ID,\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 marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model/marketplace'\nimport { sanitizeMarketplaceProps } from '../model/marketplace-props'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { refreshListingQueryFields } from './listing-query-fields'\n\n/** The node marking where a bound screen's content grafts in. */\nconst LAYOUT_SLOT_COMPONENT_ID = 'layoutSlot'\n\n/**\n * Publishes a shared layout to the marketplace (AGL-671).\n *\n * Layouts were the one design asset that could not be shared at all:\n * `publish-template` captures screens and theme only, and `layoutSlot` is\n * excluded from the marketplace allowlist as \"layout chrome\". Both are\n * addressed here — the slot is permitted for this artifact type\n * specifically, since a layout without one is chrome with nowhere to put\n * the page.\n *\n * Gates mirror template publishing exactly: org permission, host role,\n * `marketplaceSelling`, an org publisher profile, and payouts configured\n * before anything can be sold.\n */\nexport const publishLayoutHandler: 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 layoutId = String(req.body?.layoutId ?? '')\n const displayName = String(req.body?.displayName ?? '')\n const description = String(req.body?.description ?? '')\n const category = String(req.body?.category ?? '')\n const priceUsd = Math.max(0, Math.round(Number(req.body?.priceUsd ?? 0)))\n if (!hostId || !layoutId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing hostId, layoutId or displayName' })\n }\n // The price floor and ceiling, from ONE validator (AGL-2343). This door had\n // NO price validation at all: it clamped to zero and stored whatever came in,\n // so both a $1 listing that loses money on every sale and a $250,000 one were\n // publishable here. Also enforced in `publishPreconditionRefusal` below.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\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\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 const org = orgForHost?.org ?? {}\n if (!checkEntitlement(org as any, 'marketplaceSelling')) {\n return res.status(403).json({\n error: 'Publishing to the marketplace requires a Pro plan',\n })\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: 'layouts',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const layoutRef = hostRef.collection('layouts').doc(layoutId)\n const layoutSnapshot = await layoutRef.get()\n if (!layoutSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown layout' })\n }\n const versionId = layoutSnapshot.get('versionId')\n if (!versionId) {\n return res.status(422).json({\n error: 'Publish this layout on your site first — there is no ' +\n 'published version to share',\n })\n }\n const versionSnapshot = await layoutRef\n .collection('versions')\n .doc(String(versionId))\n .get()\n // `nodes` is stored in more than one form and the besigner writes the\n // COMPRESSED one, which the Admin SDK materialises as a Node `Buffer`\n // (AGL-1395). Read raw, this was worse than the template path: the length\n // check passed on the BYTE COUNT, and the root search below walked byte\n // INDICES — byte 0 is a number, a number has no `parentId`, so `rootId`\n // resolved to `'0'` and the sanitizer built a wrapper around nothing.\n // Every besigner-edited layout was refused as empty or slotless.\n const nodes = decodeStoredNodes<Record<string, any>>(\n versionSnapshot.get('nodes'),\n )\n if (!nodes || !Object.keys(nodes).length) {\n return res.status(422).json({ error: 'Layout version has no content' })\n }\n\n // The layout properties its tree binds to (AGL-2933), from the same\n // version. A default the sanitizer refuses is cleared and its property\n // kept — see `sanitizeMarketplaceProps`.\n const props = sanitizeMarketplaceProps(versionSnapshot.get('props'))\n if (props.ok === false) {\n return res.status(422).json({ error: props.error })\n }\n\n // Layout versions have no explicit rootId. The besigner canvas roots them\n // at the same wrapper screens use, so prefer it outright; the parentless\n // search stays as the fallback for anything authored before that, and\n // matches how the renderer walks them. Preferring the wrapper also means a\n // top-level node that relies on child arrays alone — legal, see\n // `compose-layout-nodes` — can no longer win the search over the real root.\n const rootId: string =\n (nodes[CANVAS_ROOT_ELEMENT_ID] && CANVAS_ROOT_ELEMENT_ID) ||\n Object.keys(nodes).find((id) => !nodes[id]?.parentId) ||\n Object.keys(nodes)[0] ||\n ''\n const sanitized = sanitizeMarketplaceDefinition(\n { rootId, nodes },\n {\n extraComponentIds: [LAYOUT_SLOT_COMPONENT_ID],\n // A link or image bound whole to a Link or Image property travels\n // bound (AGL-2933).\n declaredProps: props.props,\n },\n )\n // `=== false` rather than `!sanitized.ok`: the compiler does not narrow\n // the union on the negated form here, and publish-template hit the same\n // thing first.\n if (sanitized.ok === false) {\n return res.status(400).json({ error: sanitized.error })\n }\n // A layout whose slot did not survive would render as chrome with the\n // page content missing — better to refuse than to publish that.\n const hasSlot = Object.values(sanitized.nodes).some(\n (node: any) => node.componentId === LAYOUT_SLOT_COMPONENT_ID,\n )\n if (!hasSlot) {\n return res.status(422).json({\n error:\n 'This layout has no content slot, so pages using it would have ' +\n 'nowhere to render',\n })\n }\n\n // One listing per source layout; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceLayoutId', '==', layoutId)\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 artifactType: 'layout',\n profileId: publisher.orgId,\n sourceHostId: hostId,\n sourceLayoutId: layoutId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321): the template gallery's\n // marketplace shelf asks `nameTokens` on its query.\n ...displayNameSearchFields(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 // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\n await listingRef.collection('versions').doc(String(version)).set({\n // `props` is always a list, even an empty one: install and update read a\n // version with none as one published before properties were carried.\n layout: {\n rootId: sanitized.rootId,\n nodes: sanitized.nodes,\n props: props.props,\n },\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: 'Layout publish failed' })\n }\n}\n\nexport default publishLayoutHandler\n"],"names":["displayNameSearchFields","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","marketplacePriceRefusal","sanitizeMarketplaceDefinition","sanitizeMarketplaceProps","resolvePublisherProfile","publishPreconditionRefusal","refreshListingQueryFields","LAYOUT_SLOT_COMPONENT_ID","publishLayoutHandler","req","res","method","status","json","error","hostId","String","body","layoutId","displayName","description","category","priceUsd","Math","max","round","Number","trim","priceRefusal","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","existing","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","orgForHost","org","orgId","publisher","refusal","sells","layoutRef","layoutSnapshot","versionId","versionSnapshot","nodes","Object","keys","props","ok","rootId","find","id","parentId","sanitized","extraComponentIds","declaredProps","hasSlot","values","some","node","componentId","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","set","artifactType","profileId","sourceHostId","sourceLayoutId","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","layout","listingId","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-layout.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 { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n CANVAS_ROOT_ELEMENT_ID,\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 marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model/marketplace'\nimport { sanitizeMarketplaceProps } from '../model/marketplace-props'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\n\n/** The node marking where a bound screen's content grafts in. */\nconst LAYOUT_SLOT_COMPONENT_ID = 'layoutSlot'\n\n/**\n * Publishes a shared layout to the marketplace (AGL-671).\n *\n * Layouts were the one design asset that could not be shared at all:\n * `publish-template` captures screens and theme only, and `layoutSlot` is\n * excluded from the marketplace allowlist as \"layout chrome\". Both are\n * addressed here — the slot is permitted for this artifact type\n * specifically, since a layout without one is chrome with nowhere to put\n * the page.\n *\n * Gates mirror template publishing exactly: org permission, host role,\n * `marketplaceSelling`, an org publisher profile, and payouts configured\n * before anything can be sold.\n */\nexport const publishLayoutHandler: 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 layoutId = String(req.body?.layoutId ?? '')\n const displayName = String(req.body?.displayName ?? '')\n const description = String(req.body?.description ?? '')\n const category = String(req.body?.category ?? '')\n const priceUsd = Math.max(0, Math.round(Number(req.body?.priceUsd ?? 0)))\n if (!hostId || !layoutId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing hostId, layoutId or displayName' })\n }\n // The price floor and ceiling, from ONE validator (AGL-2343). This door had\n // NO price validation at all: it clamped to zero and stored whatever came in,\n // so both a $1 listing that loses money on every sale and a $250,000 one were\n // publishable here. Also enforced in `publishPreconditionRefusal` below.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\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\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 const org = orgForHost?.org ?? {}\n if (!checkEntitlement(org as any, 'marketplaceSelling')) {\n return res.status(403).json({\n error: 'Publishing to the marketplace requires a Pro plan',\n })\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: 'layouts',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const layoutRef = hostRef.collection('layouts').doc(layoutId)\n const layoutSnapshot = await layoutRef.get()\n if (!layoutSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown layout' })\n }\n const versionId = layoutSnapshot.get('versionId')\n if (!versionId) {\n return res.status(422).json({\n error: 'Publish this layout on your site first — there is no ' +\n 'published version to share',\n })\n }\n const versionSnapshot = await layoutRef\n .collection('versions')\n .doc(String(versionId))\n .get()\n // `nodes` is stored in more than one form and the besigner writes the\n // COMPRESSED one, which the Admin SDK materialises as a Node `Buffer`\n // (AGL-1395). Read raw, this was worse than the template path: the length\n // check passed on the BYTE COUNT, and the root search below walked byte\n // INDICES — byte 0 is a number, a number has no `parentId`, so `rootId`\n // resolved to `'0'` and the sanitizer built a wrapper around nothing.\n // Every besigner-edited layout was refused as empty or slotless.\n const nodes = decodeStoredNodes<Record<string, any>>(\n versionSnapshot.get('nodes'),\n )\n if (!nodes || !Object.keys(nodes).length) {\n return res.status(422).json({ error: 'Layout version has no content' })\n }\n\n // The layout properties its tree binds to (AGL-2933), from the same\n // version. A default the sanitizer refuses is cleared and its property\n // kept — see `sanitizeMarketplaceProps`.\n const props = sanitizeMarketplaceProps(versionSnapshot.get('props'))\n if (props.ok === false) {\n return res.status(422).json({ error: props.error })\n }\n\n // Layout versions have no explicit rootId. The besigner canvas roots them\n // at the same wrapper screens use, so prefer it outright; the parentless\n // search stays as the fallback for anything authored before that, and\n // matches how the renderer walks them. Preferring the wrapper also means a\n // top-level node that relies on child arrays alone — legal, see\n // `compose-layout-nodes` — can no longer win the search over the real root.\n const rootId: string =\n (nodes[CANVAS_ROOT_ELEMENT_ID] && CANVAS_ROOT_ELEMENT_ID) ||\n Object.keys(nodes).find((id) => !nodes[id]?.parentId) ||\n Object.keys(nodes)[0] ||\n ''\n const sanitized = sanitizeMarketplaceDefinition(\n { rootId, nodes },\n {\n extraComponentIds: [LAYOUT_SLOT_COMPONENT_ID],\n // A link or image bound whole to a Link or Image property travels\n // bound (AGL-2933).\n declaredProps: props.props,\n },\n )\n // `=== false` rather than `!sanitized.ok`: the compiler does not narrow\n // the union on the negated form here, and publish-template hit the same\n // thing first.\n if (sanitized.ok === false) {\n return res.status(400).json({ error: sanitized.error })\n }\n // A layout whose slot did not survive would render as chrome with the\n // page content missing — better to refuse than to publish that.\n const hasSlot = Object.values(sanitized.nodes).some(\n (node: any) => node.componentId === LAYOUT_SLOT_COMPONENT_ID,\n )\n if (!hasSlot) {\n return res.status(422).json({\n error:\n 'This layout has no content slot, so pages using it would have ' +\n 'nowhere to render',\n })\n }\n\n // One listing per source layout; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceLayoutId', '==', layoutId)\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 // The phishing screen, before anything is listed (AGL-3365).\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n pageNodes: sanitized.nodes,\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n\n await listingRef.set(\n {\n artifactType: 'layout',\n profileId: publisher.orgId,\n sourceHostId: hostId,\n sourceLayoutId: layoutId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321): the template gallery's\n // marketplace shelf asks `nameTokens` on its query.\n ...displayNameSearchFields(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 // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\n await listingRef.collection('versions').doc(String(version)).set({\n // `props` is always a list, even an empty one: install and update read a\n // version with none as one published before properties were carried.\n layout: {\n rootId: sanitized.rootId,\n nodes: sanitized.nodes,\n props: props.props,\n },\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: 'Layout publish failed' })\n }\n}\n\nexport default publishLayoutHandler\n"],"names":["displayNameSearchFields","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","marketplacePriceRefusal","sanitizeMarketplaceDefinition","sanitizeMarketplaceProps","resolvePublisherProfile","publishPreconditionRefusal","listingSubmissionRefusal","refreshListingQueryFields","LAYOUT_SLOT_COMPONENT_ID","publishLayoutHandler","req","res","method","status","json","error","hostId","String","body","layoutId","displayName","description","category","priceUsd","Math","max","round","Number","trim","priceRefusal","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","existing","orgForHost","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","org","orgId","publisher","refusal","sells","layoutRef","layoutSnapshot","versionId","versionSnapshot","nodes","Object","keys","props","ok","rootId","find","id","parentId","sanitized","extraComponentIds","declaredProps","hasSlot","values","some","node","componentId","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","pageNodes","official","set","artifactType","profileId","sourceHostId","sourceLayoutId","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","layout","listingId","console"],"mappings":"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"}