@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
@@ -20,6 +20,7 @@ import { firebaseAdmin, getOrgForUser, notifyStaff } from "@aglyn/tenant-data-ad
20
20
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
21
21
  import { canActAsPublisher, resolvePublisherProfile } from "./publisher-profile.js";
22
22
  import { publishPreconditionRefusal } from "./publish-preconditions.js";
23
+ import { listingSubmissionRefusal } from "./listing-screen.js";
23
24
  import { refreshListingQueryFields } from "./listing-query-fields.js";
24
25
  import { attestationLabels, missingAttestations, missingAttestationSubjects, PUBLISHER_ATTESTATION } from "../model/publisher-attestation.js";
25
26
  import { createHash } from "crypto";
@@ -55,6 +56,7 @@ const MAX_BUNDLE_BYTES = 5 * 1024 * 1024;
55
56
  error: verdict.error
56
57
  });
57
58
  try {
59
+ var _verdict_content, _listing_profileId, _listing_displayName, _listing_description, _ref1, _content_readme, _content_homepageUrl, _content_repositoryUrl;
58
60
  var _req_body2, _req_body3;
59
61
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
60
62
  const listingRef = firebaseAdmin.app().firestore().collection('marketplaceListings').doc(listingId);
@@ -81,6 +83,24 @@ const MAX_BUNDLE_BYTES = 5 * 1024 * 1024;
81
83
  const description = (_req_body2 = req.body) == null ? void 0 : _req_body2.description;
82
84
  const displayName = (_req_body3 = req.body) == null ? void 0 : _req_body3.displayName;
83
85
  const nextName = typeof displayName === 'string' ? displayName.trim().slice(0, 80) : '';
86
+ // An edit is a submission too (AGL-3365): the listing is already public,
87
+ // so what it is about to say is screened before it says it. The copy is
88
+ // read as it will stand after the merge, not only the changed fields.
89
+ const content = (_verdict_content = verdict.content) != null ? _verdict_content : {};
90
+ const screened = await listingSubmissionRefusal({
91
+ publisherOrgId: String((_listing_profileId = listing.profileId) != null ? _listing_profileId : ''),
92
+ content: {
93
+ displayName: nextName || String((_listing_displayName = listing.displayName) != null ? _listing_displayName : ''),
94
+ description: typeof description === 'string' ? description : String((_listing_description = listing.description) != null ? _listing_description : ''),
95
+ readme: (_ref1 = (_content_readme = content.readme) != null ? _content_readme : listing.readme) != null ? _ref1 : null,
96
+ urls: [
97
+ (_content_homepageUrl = content.homepageUrl) != null ? _content_homepageUrl : listing.homepageUrl,
98
+ (_content_repositoryUrl = content.repositoryUrl) != null ? _content_repositoryUrl : listing.repositoryUrl
99
+ ]
100
+ },
101
+ official: decoded['staff'] === true
102
+ });
103
+ if (screened) return res.status(screened.status).json(screened.body);
84
104
  await listingRef.set(_extends({}, verdict.content, typeof description === 'string' ? {
85
105
  description: description.slice(0, 500)
86
106
  } : {}, nextName ? _extends({
@@ -293,8 +313,8 @@ export const publishPluginHandler = async (req, res)=>{
293
313
  error: 'Unauthenticated'
294
314
  });
295
315
  try {
296
- var _ref6, _membership_orgId, _ref7, _ref8, _manifest_contributes;
297
- var _manifest_capabilities, _req_body4, _contentVerdict_content;
316
+ var _ref6, _membership_orgId, _ref7, _ref8, _manifest_contributes, _ref9;
317
+ var _manifest_capabilities, _contentVerdict_content, _contentVerdict_content1, _contentVerdict_content2, _req_body4, _contentVerdict_content3;
298
318
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
299
319
  // Act on the org the client is in (AGL-868), not a guessed first-org — a
300
320
  // member can belong to several, and the plugin publishes under one of them.
@@ -427,6 +447,24 @@ export const publishPluginHandler = async (req, res)=>{
427
447
  missingAttestationSubjects: missingSubjects.map((subject)=>subject.field)
428
448
  });
429
449
  }
450
+ // The phishing screen, before anything is stored or queued (AGL-3365).
451
+ const screened = await listingSubmissionRefusal({
452
+ publisherOrgId: publisher.orgId,
453
+ content: {
454
+ displayName,
455
+ publisherName: publisher.displayName,
456
+ description,
457
+ changelog,
458
+ readme: (_ref9 = (_contentVerdict_content = contentVerdict.content) == null ? void 0 : _contentVerdict_content.readme) != null ? _ref9 : null,
459
+ urls: [
460
+ (_contentVerdict_content1 = contentVerdict.content) == null ? void 0 : _contentVerdict_content1.homepageUrl,
461
+ (_contentVerdict_content2 = contentVerdict.content) == null ? void 0 : _contentVerdict_content2.repositoryUrl
462
+ ]
463
+ },
464
+ official: decoded['staff'] === true,
465
+ org
466
+ });
467
+ if (screened) return res.status(screened.status).json(screened.body);
430
468
  // Immutable content-addressed write — a new build is a new object, so a
431
469
  // consumer's pinned version can never be overwritten underneath it.
432
470
  const objectPath = pluginArtifactPath(listingRef.id, manifest.version, sha256);
@@ -488,7 +526,7 @@ export const publishPluginHandler = async (req, res)=>{
488
526
  manifest
489
527
  }, changelog.trim() && {
490
528
  changelog: changelog.trim()
491
- }, ((_contentVerdict_content = contentVerdict.content) == null ? void 0 : _contentVerdict_content.repositoryUrl) && {
529
+ }, ((_contentVerdict_content3 = contentVerdict.content) == null ? void 0 : _contentVerdict_content3.repositoryUrl) && {
492
530
  repositoryUrl: contentVerdict.content.repositoryUrl
493
531
  }, {
494
532
  publishedAt: now,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-plugin.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 checkPluginBundle,\n createResourceUid,\n MAX_PLUGIN_BUNDLE_BYTES,\n type PluginApiHandler,\n pluginArtifactPath,\n PLUGIN_VERIFIER_VERSION,\n validatePluginManifest,\n} from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n getOrgForUser,\n notifyStaff,\n} from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n canActAsPublisher,\n resolvePublisherProfile,\n} from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { refreshListingQueryFields } from './listing-query-fields'\nimport {\n attestationLabels,\n missingAttestations,\n missingAttestationSubjects,\n PUBLISHER_ATTESTATION,\n} from '../model/publisher-attestation'\nimport { createHash } from 'crypto'\nimport {\n marketplacePriceRefusal,\n missingPublicListingContent,\n validateListingContent,\n} from '../model/marketplace'\n\n// Base64 bundle bodies: a small JS plugin bundle, capped generously.\nconst MAX_BUNDLE_BYTES = 5 * 1024 * 1024\n\n/**\n * Publishes an executable plugin to the marketplace (AGL-45), per the AGL-43\n * artifact pipeline — relocated from the console app route into the\n * marketplace plugin (AGL-418); URL `/api/marketplace/publish-plugin` is\n * preserved through the dispatcher. Runs server-side so validation\n * (manifest schema, size caps) can't be bypassed: the bundle is\n * content-addressed (sha256) and written IMMUTABLY to the isolated\n * artifacts bucket, and the version entry records the hash the loader\n * verifies before executing. Env-gated on `PLUGIN_ARTIFACTS_BUCKET` —\n * 501 degrades like other platform infra. Requirements: publish\n * permission, marketplace profile, Pro plan; paid listings need completed\n * Stripe Connect onboarding.\n */\nconst updateListingContent: PluginApiHandler = async (req, res) => {\n const listingId = String(req.body?.listingId ?? '')\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\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 const verdict = validateListingContent(req.body ?? {})\n if (!verdict.ok) return res.status(400).json({ error: verdict.error })\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const listingRef = firebaseAdmin\n .app()\n .firestore()\n .collection('marketplaceListings')\n .doc(listingId)\n const listing = (await listingRef.get()).data()\n if (!listing || listing.deletedAt) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // Org-owned listings (AGL-652): a uid never equals an org id, so the\n // old identity comparison would lock publishers out of their own listing.\n const isPublisher = await canActAsPublisher(\n firebaseAdmin.app().firestore(),\n decoded.uid,\n listing.profileId,\n )\n if (!isPublisher && decoded['staff'] !== true) {\n return res.status(403).json({ error: 'Not your listing' })\n }\n // Name and description are the whole first impression of a listing in\n // browse, and until AGL-793 they were the only listing-content fields with\n // no edit path — a typo in either could only be corrected by publishing a\n // fake new version, which signals \"there's an update\" to every installer.\n // Same caps the publish routes apply. An empty name is ignored rather than\n // accepted: a nameless listing is unrecognisable in the catalogue.\n const description = req.body?.description\n const displayName = req.body?.displayName\n const nextName =\n typeof displayName === 'string' ? displayName.trim().slice(0, 80) : ''\n await listingRef.set(\n {\n ...verdict.content,\n ...(typeof description === 'string'\n ? { description: description.slice(0, 500) }\n : {}),\n ...(nextName\n ? { displayName: nextName, ...displayNameSearchFields(nextName) }\n : {}),\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n // A renamed listing is searched by its new name (AGL-3321).\n await refreshListingQueryFields(listingRef)\n return res.status(200).json({ ok: true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Listing update failed' })\n }\n}\n\n/**\n * Flip a listing between private and public (AGL-968/994).\n *\n * Deliberately NOT a re-review. Per-version `reviewState` and the listing's\n * `reviewStatus` are left exactly as they are: approval is a statement about\n * specific bytes (AGL-966), and who may install them was never part of that\n * statement. Sending an already-approved bundle back through the queue\n * because its audience widened would only teach publishers to start public.\n *\n * Going public does require what a public listing needs — see\n * {@link missingPublicListingContent}. Going private is unconditional: it\n * only ever removes reach.\n */\nconst setListingVisibility: PluginApiHandler = async (req, res) => {\n const listingId = String(req.body?.listingId ?? '')\n const visibility = req.body?.visibility === 'private' ? 'private' : 'public'\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\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 try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listing = (await listingRef.get()).data()\n if (!listing || listing.deletedAt) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n const isPublisher = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n if (!isPublisher && decoded['staff'] !== true) {\n return res.status(403).json({ error: 'Not your listing' })\n }\n if (visibility === 'public') {\n const missing = missingPublicListingContent(listing)\n if (missing.length) {\n return res.status(400).json({\n error:\n 'A marketplace listing needs ' +\n missing.join(', ') +\n '. Add them in Edit, then publish it.',\n missing,\n })\n }\n }\n await listingRef.set(\n {\n visibility,\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n // Private leaves browse and public joins it, through the audience the\n // browse query reads (AGL-3321).\n await refreshListingQueryFields(listingRef)\n return res.status(200).json({ ok: true, visibility })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Visibility change failed' })\n }\n}\n\n/**\n * Unpublish or republish a listing (AGL-3321).\n *\n * This was the one client write to a listing: the seller panel set\n * `deletedAt` directly. Browse now queries a listing's AUDIENCE, which\n * follows from `deletedAt` among other fields, and a client write cannot\n * re-derive it — so the toggle moved here, beside `set-visibility`, and the\n * rules deny `deletedAt` to clients. Publisher-or-staff, like the other two\n * listing actions; unlike them it reaches a listing that is already\n * unpublished, since republishing is half of what it does.\n */\nconst setListingPublished: PluginApiHandler = async (req, res) => {\n const listingId = String(req.body?.listingId ?? '')\n const published = req.body?.published === true\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\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 try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listing = (await listingRef.get()).data()\n if (!listing) return res.status(404).json({ error: 'Unknown listing' })\n const isPublisher = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n if (!isPublisher && decoded['staff'] !== true) {\n return res.status(403).json({ error: 'Not your listing' })\n }\n await listingRef.set(\n {\n deletedAt: published\n ? null\n : firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n await refreshListingQueryFields(listingRef)\n return res.status(200).json({ ok: true, published })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publishing change failed' })\n }\n}\n\nexport const publishPluginHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n // Content-only listing edits (AGL-430): publishers refresh the docs the\n // detail page renders (readme/screenshots/links/…) without shipping a\n // new bundle. Publisher-or-staff, validated exactly like publish.\n if (req.body?.action === 'update-listing') {\n return updateListingContent(req, res)\n }\n // Private ⇄ public (AGL-968/994), without shipping a new bundle.\n if (req.body?.action === 'set-visibility') {\n return setListingVisibility(req, res)\n }\n // Unpublish ⇄ republish (AGL-3321), which was a client write.\n if (req.body?.action === 'set-published') {\n return setListingPublished(req, res)\n }\n const body = req.body ?? {}\n const headers = req.headers as Partial<Record<string, string>>\n const displayName = String(body?.displayName ?? '').slice(0, 80)\n const description = String(body?.description ?? '').slice(0, 500)\n const category = String(body?.category ?? '').slice(0, 40)\n const changelog = String(body?.changelog ?? '').slice(0, 1000)\n const priceUsd = Math.round(Number(body?.priceUsd ?? 0)) || 0\n const bundleBase64 = String(body?.bundle ?? '')\n // Listing content (AGL-430): optional publisher docs for the detail page.\n const contentVerdict = validateListingContent(body ?? {})\n if (!contentVerdict.ok) {\n return res.status(400).json({ error: contentVerdict.error })\n }\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 (!displayName.trim() || !bundleBase64) {\n return res.status(400).json({ error: 'Missing displayName or bundle' })\n }\n\n const validation = validatePluginManifest(body?.manifest)\n if (validation.ok === false) {\n return res.status(422).json({ error: validation.error })\n }\n const manifest = validation.manifest\n\n const authorization = 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 // Act on the org the client is in (AGL-868), not a guessed first-org — a\n // member can belong to several, and the plugin publishes under one of them.\n const requestedOrgId = String(body?.orgId ?? '').trim() || undefined\n const membership = await resolveOrgPermissions(decoded.uid, {\n orgId: requestedOrgId,\n })\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 // Plan gate rides the acting org's doc (AGL-238/868).\n const orgForUser = await getOrgForUser(\n decoded.uid,\n membership.orgId ?? requestedOrgId,\n )\n const org = orgForUser?.org ?? {}\n if (!checkEntitlement(org, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n\n // Publish rate limit (AGL-437): a runaway or abusive publisher can't\n // flood the artifacts bucket/review queue — 20 publishes per UTC day.\n // Scoped to the publishing ORG (AGL-652): per-user it would multiply by\n // headcount, letting a big team flood the queue 20 publishes at a time.\n const dayKey = new Date().toISOString().slice(0, 10)\n if (!orgForUser?.orgId) {\n return res.status(409).json({ error: 'No publishing organization' })\n }\n const limiterRef = firestore\n .collection('publisherProfiles')\n .doc(orgForUser.orgId)\n .collection('meta')\n .doc('publishWindow')\n const allowed = await firestore.runTransaction(async (transaction) => {\n const window = (await transaction.get(limiterRef)).data() ?? {}\n const count = window.dayKey === dayKey ? Number(window.count ?? 0) : 0\n if (count >= 20) return false\n transaction.set(limiterRef, { dayKey, count: count + 1 })\n return true\n })\n if (!allowed) {\n return res\n .status(429)\n .json({ error: 'Daily publish limit reached — try again tomorrow' })\n }\n\n const publisher = await resolvePublisherProfile(firestore, orgForUser.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: 'plugins',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const bundle = Buffer.from(bundleBase64, 'base64')\n if (!bundle.length || bundle.length > MAX_BUNDLE_BYTES) {\n return res.status(413).json({ error: 'Bundle is empty or too large' })\n }\n // Static verification (AGL-426): the same checks as the local\n // `verify-plugin-bundle.mjs`, so a bundle that passes there publishes\n // here. Entry exports, self-containment, forbidden APIs, size.\n //\n // The manifest goes in too (AGL-964): the checker diffs the bundle's\n // network calls against the origins declared here, and a call to an\n // origin the manifest omits is refused at publish rather than silently\n // blocked by the CSP at runtime, where the publisher would meet it as a\n // mystery in production.\n //\n // And what register() registers is compared with the manifest's\n // `contributes` (AGL-3116), which a new version must carry whenever it\n // registers anything: the loaders place a plugin by that declaration\n // alone, so a registration it omits is a surface that never loads.\n const verification = checkPluginBundle(bundle.toString('utf8'), {\n maxBytes: MAX_PLUGIN_BUNDLE_BYTES,\n declaredNetwork: manifest.capabilities?.network ?? [],\n declaredContributions: manifest.contributes ?? null,\n requireContributions: true,\n })\n if (!verification.ok) {\n return res.status(422).json({\n error: 'Bundle failed verification',\n problems: verification.problems,\n })\n }\n const sha256 = createHash('sha256')\n .update(new Uint8Array(bundle))\n .digest('hex')\n\n // Isolated artifacts bucket (AGL-43 §3). Without it configured we\n // refuse rather than store executable code in the app bucket.\n const artifactsBucket = process.env.PLUGIN_ARTIFACTS_BUCKET\n if (!artifactsBucket) {\n return res.status(501).json({\n error:\n 'Plugin publishing is not configured (missing ' +\n 'PLUGIN_ARTIFACTS_BUCKET).',\n })\n }\n\n // One listing per publisher+plugin id: re-publishing bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('pluginId', '==', manifest.id)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n\n // Pre-submission attestation (AGL-969). Blocks the publish rather than\n // warning, because the whole point is that these answers exist BEFORE a\n // reviewer spends time on the bundle.\n //\n // Whether this is an update is decided here, from whether a listing\n // already exists — never from a client flag, which would make \"this is\n // my first version\" a way to skip the changelog item.\n const ticked = (\n Array.isArray(body?.attestation)\n ? body.attestation.map((id: unknown) => String(id))\n : []\n ).filter((id: string) =>\n PUBLISHER_ATTESTATION.some((item) => item.id === id),\n )\n const isUpdate = !existing.empty\n const missing = missingAttestations(ticked, isUpdate)\n if (missing.length) {\n return res.status(428).json({\n error:\n 'Confirm the pre-submission checklist before publishing: ' +\n attestationLabels(missing).join('; '),\n missingAttestations: missing,\n })\n }\n\n // An attestation needs something to be about (AGL-1076). `repository`\n // was confirmable in a form with no repository field, so listings\n // reached review carrying a signed claim that a URL we never collected\n // was public and matched — and the reviewer's first link went nowhere.\n //\n // Checked against the SUBMISSION, not the listing doc: a first publish\n // has no listing, and on an update the tick is a statement about what\n // this publish declares. `contentVerdict.content` is the normalized\n // value about to be stored, so the gate and the write cannot disagree.\n const missingSubjects = missingAttestationSubjects(\n contentVerdict.content as Record<string, unknown>,\n isUpdate,\n )\n if (missingSubjects.length) {\n return res.status(428).json({\n error:\n 'These are confirmed above but not filled in: ' +\n missingSubjects.map((subject) => subject.label).join('; '),\n missingAttestationSubjects: missingSubjects.map(\n (subject) => subject.field,\n ),\n })\n }\n\n // Immutable content-addressed write — a new build is a new object, so a\n // consumer's pinned version can never be overwritten underneath it.\n const objectPath = pluginArtifactPath(\n listingRef.id,\n manifest.version,\n sha256,\n )\n const bucket = firebaseAdmin.app().storage().bucket(artifactsBucket)\n const file = bucket.file(objectPath)\n const [alreadyStored] = await file.exists()\n if (!alreadyStored) {\n await file.save(bundle, {\n contentType: 'application/javascript',\n metadata: {\n cacheControl: 'public, max-age=31536000, immutable',\n },\n })\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n await listingRef.set(\n {\n type: 'plugin',\n artifactType: 'plugin',\n profileId: publisher.orgId,\n pluginId: manifest.id,\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 ...contentVerdict.content,\n latestVersion: manifest.version,\n // Mirror of `pluginVersions/{latestVersion}.reviewState`, for the\n // marketplace (AGL-1121). Browse reads listings directly and cannot\n // afford a version subdoc read per card, so the newest bytes' verdict\n // has to live here to be shown at all.\n //\n // Reset to 'pending' on EVERY publish, including a bump on a listing\n // that staff already verified. That is the whole point: the listing's\n // `reviewStatus` deliberately survives a version bump, so before this\n // the marketplace badged brand-new, unreviewed bytes as reviewed.\n latestVersionReviewState: 'pending',\n deletedAt: null,\n // Review queue (AGL-432): first publish enters as 'submitted';\n // version bumps keep whatever status staff granted — which is safe\n // now that installs resolve the newest APPROVED version rather than\n // `latestVersion` (AGL-966). A listed plugin stays listed on its\n // reviewed bytes while the new version waits in the queue.\n ...(existing.empty && { createdAt: now, reviewStatus: 'submitted' }),\n // Private plugins never reach the marketplace but take the same\n // review path (AGL-968). Only settable on first publish; changing\n // visibility later is a separate, deliberate action.\n ...(existing.empty && {\n visibility: req.body?.visibility === 'private' ? 'private' : 'public',\n }),\n updatedAt: now,\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 // Version snapshots are server-only; the loader reads sha256 + manifest\n // to verify integrity and stamp CSP before executing.\n await listingRef\n .collection('pluginVersions')\n .doc(manifest.version)\n .set({\n version: manifest.version,\n sha256,\n objectPath,\n manifest,\n ...(changelog.trim() && { changelog: changelog.trim() }),\n // The repository as declared FOR THESE BYTES (AGL-1076). The listing\n // carries whatever the latest publish said, and a publisher may move\n // or rename a repo between versions — a reviewer looking at v1.0.2\n // needs the link that was attested against v1.0.2's sha256, which is\n // the same reasoning that pins the attestation itself.\n ...(contentVerdict.content?.repositoryUrl && {\n repositoryUrl: contentVerdict.content.repositoryUrl,\n }),\n publishedAt: now,\n // What the publisher stated about these bytes (AGL-969). Same shape\n // as the staff `reviewChecklist` and pinned to the same sha256, so\n // a republish under this version string re-asks rather than\n // inheriting an attestation made about code that has since changed.\n //\n // `by` is the uid that submitted, not the org: takedown and removal\n // are defensible because a named person said this on a date, and an\n // org id names nobody.\n publisherAttestation: Object.fromEntries(\n ticked.map((id: string) => [id, { by: decoded.uid, at: now, sha256 }]),\n ),\n // Every version starts UNREVIEWED (AGL-966). This doc is written\n // with .set() and no merge, so a republish of the same version\n // string also resets it — new bytes, new review, by construction.\n reviewState: 'pending',\n // Keep the verdict we just computed (AGL-962). The bundle is\n // immutable and content-addressed, so this result holds for as long\n // as the checker does — the review page reads it instead of\n // re-downloading a megabyte per view. `verifierVersion` is what\n // makes that safe: bump PLUGIN_VERIFIER_VERSION and every stored\n // verdict is recomputed on next read.\n verification: {\n ok: verification.ok,\n problems: verification.problems,\n sha256,\n verifierVersion: PLUGIN_VERIFIER_VERSION,\n checkedAt: now,\n },\n })\n\n // Tell staff a version is waiting (AGL-970). Nothing announced a\n // submission before this: reviewers had to notice a new row by\n // visiting the queue, and an UPDATE to an already-listed plugin\n // produced no row at all. Persistent per-user notifications, so it\n // survives a refresh and stays until someone acts on it.\n await notifyStaff({\n type: 'marketplace.review',\n title: `${displayName.trim()} v${manifest.version} needs review`,\n body: existing.empty\n ? 'A new plugin was submitted to the marketplace.'\n : 'A new version was published. The previously approved version ' +\n 'keeps installing until this one is reviewed.',\n link: `/admin/plugin-reviews/${listingRef.id}`,\n }).catch(() => undefined)\n\n return res\n .status(200)\n .json({ listingId: listingRef.id, version: manifest.version, sha256 })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n"],"names":["displayNameSearchFields","checkEntitlement","checkPluginBundle","createResourceUid","MAX_PLUGIN_BUNDLE_BYTES","pluginArtifactPath","PLUGIN_VERIFIER_VERSION","validatePluginManifest","firebaseAdmin","getOrgForUser","notifyStaff","resolveOrgPermissions","canActAsPublisher","resolvePublisherProfile","publishPreconditionRefusal","refreshListingQueryFields","attestationLabels","missingAttestations","missingAttestationSubjects","PUBLISHER_ATTESTATION","createHash","marketplacePriceRefusal","missingPublicListingContent","validateListingContent","MAX_BUNDLE_BYTES","updateListingContent","req","res","listingId","String","body","status","json","error","authorization","headers","idToken","startsWith","slice","length","undefined","verdict","ok","decoded","app","auth","verifyIdToken","listingRef","firestore","collection","doc","listing","get","data","deletedAt","isPublisher","uid","profileId","description","displayName","nextName","trim","set","content","updatedAt","FieldValue","serverTimestamp","merge","console","setListingVisibility","visibility","missing","join","setListingPublished","published","publishPluginHandler","method","action","category","changelog","priceUsd","Math","round","Number","bundleBase64","bundle","contentVerdict","priceRefusal","validation","manifest","membership","requestedOrgId","orgId","permissions","publishToMarketplace","orgForUser","org","dayKey","Date","toISOString","limiterRef","allowed","runTransaction","transaction","window","count","publisher","refusal","sells","Buffer","from","verification","toString","maxBytes","declaredNetwork","capabilities","network","declaredContributions","contributes","requireContributions","problems","sha256","update","Uint8Array","digest","artifactsBucket","process","env","PLUGIN_ARTIFACTS_BUCKET","existing","where","id","limit","empty","docs","ref","ticked","Array","isArray","attestation","map","filter","some","item","isUpdate","missingSubjects","subject","label","field","objectPath","version","bucket","storage","file","alreadyStored","exists","save","contentType","metadata","cacheControl","now","type","artifactType","pluginId","latestVersion","latestVersionReviewState","createdAt","reviewStatus","repositoryUrl","publishedAt","publisherAttestation","Object","fromEntries","by","at","reviewState","verifierVersion","checkedAt","title","link","catch"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-plugin.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 checkPluginBundle,\n createResourceUid,\n MAX_PLUGIN_BUNDLE_BYTES,\n type PluginApiHandler,\n pluginArtifactPath,\n PLUGIN_VERIFIER_VERSION,\n validatePluginManifest,\n} from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n getOrgForUser,\n notifyStaff,\n} from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n canActAsPublisher,\n resolvePublisherProfile,\n} from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\nimport {\n attestationLabels,\n missingAttestations,\n missingAttestationSubjects,\n PUBLISHER_ATTESTATION,\n} from '../model/publisher-attestation'\nimport { createHash } from 'crypto'\nimport {\n marketplacePriceRefusal,\n missingPublicListingContent,\n validateListingContent,\n} from '../model/marketplace'\n\n// Base64 bundle bodies: a small JS plugin bundle, capped generously.\nconst MAX_BUNDLE_BYTES = 5 * 1024 * 1024\n\n/**\n * Publishes an executable plugin to the marketplace (AGL-45), per the AGL-43\n * artifact pipeline — relocated from the console app route into the\n * marketplace plugin (AGL-418); URL `/api/marketplace/publish-plugin` is\n * preserved through the dispatcher. Runs server-side so validation\n * (manifest schema, size caps) can't be bypassed: the bundle is\n * content-addressed (sha256) and written IMMUTABLY to the isolated\n * artifacts bucket, and the version entry records the hash the loader\n * verifies before executing. Env-gated on `PLUGIN_ARTIFACTS_BUCKET` —\n * 501 degrades like other platform infra. Requirements: publish\n * permission, marketplace profile, Pro plan; paid listings need completed\n * Stripe Connect onboarding.\n */\nconst updateListingContent: PluginApiHandler = async (req, res) => {\n const listingId = String(req.body?.listingId ?? '')\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\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 const verdict = validateListingContent(req.body ?? {})\n if (!verdict.ok) return res.status(400).json({ error: verdict.error })\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const listingRef = firebaseAdmin\n .app()\n .firestore()\n .collection('marketplaceListings')\n .doc(listingId)\n const listing = (await listingRef.get()).data()\n if (!listing || listing.deletedAt) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // Org-owned listings (AGL-652): a uid never equals an org id, so the\n // old identity comparison would lock publishers out of their own listing.\n const isPublisher = await canActAsPublisher(\n firebaseAdmin.app().firestore(),\n decoded.uid,\n listing.profileId,\n )\n if (!isPublisher && decoded['staff'] !== true) {\n return res.status(403).json({ error: 'Not your listing' })\n }\n // Name and description are the whole first impression of a listing in\n // browse, and until AGL-793 they were the only listing-content fields with\n // no edit path — a typo in either could only be corrected by publishing a\n // fake new version, which signals \"there's an update\" to every installer.\n // Same caps the publish routes apply. An empty name is ignored rather than\n // accepted: a nameless listing is unrecognisable in the catalogue.\n const description = req.body?.description\n const displayName = req.body?.displayName\n const nextName =\n typeof displayName === 'string' ? displayName.trim().slice(0, 80) : ''\n // An edit is a submission too (AGL-3365): the listing is already public,\n // so what it is about to say is screened before it says it. The copy is\n // read as it will stand after the merge, not only the changed fields.\n const content = verdict.content ?? {}\n const screened = await listingSubmissionRefusal({\n publisherOrgId: String(listing.profileId ?? ''),\n content: {\n displayName: nextName || String(listing.displayName ?? ''),\n description:\n typeof description === 'string' ? description : String(listing.description ?? ''),\n readme: content.readme ?? (listing.readme as string | undefined) ?? null,\n urls: [\n content.homepageUrl ?? listing.homepageUrl,\n content.repositoryUrl ?? listing.repositoryUrl,\n ],\n },\n official: decoded['staff'] === true,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n await listingRef.set(\n {\n ...verdict.content,\n ...(typeof description === 'string'\n ? { description: description.slice(0, 500) }\n : {}),\n ...(nextName\n ? { displayName: nextName, ...displayNameSearchFields(nextName) }\n : {}),\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n // A renamed listing is searched by its new name (AGL-3321).\n await refreshListingQueryFields(listingRef)\n return res.status(200).json({ ok: true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Listing update failed' })\n }\n}\n\n/**\n * Flip a listing between private and public (AGL-968/994).\n *\n * Deliberately NOT a re-review. Per-version `reviewState` and the listing's\n * `reviewStatus` are left exactly as they are: approval is a statement about\n * specific bytes (AGL-966), and who may install them was never part of that\n * statement. Sending an already-approved bundle back through the queue\n * because its audience widened would only teach publishers to start public.\n *\n * Going public does require what a public listing needs — see\n * {@link missingPublicListingContent}. Going private is unconditional: it\n * only ever removes reach.\n */\nconst setListingVisibility: PluginApiHandler = async (req, res) => {\n const listingId = String(req.body?.listingId ?? '')\n const visibility = req.body?.visibility === 'private' ? 'private' : 'public'\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\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 try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listing = (await listingRef.get()).data()\n if (!listing || listing.deletedAt) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n const isPublisher = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n if (!isPublisher && decoded['staff'] !== true) {\n return res.status(403).json({ error: 'Not your listing' })\n }\n if (visibility === 'public') {\n const missing = missingPublicListingContent(listing)\n if (missing.length) {\n return res.status(400).json({\n error:\n 'A marketplace listing needs ' +\n missing.join(', ') +\n '. Add them in Edit, then publish it.',\n missing,\n })\n }\n }\n await listingRef.set(\n {\n visibility,\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n // Private leaves browse and public joins it, through the audience the\n // browse query reads (AGL-3321).\n await refreshListingQueryFields(listingRef)\n return res.status(200).json({ ok: true, visibility })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Visibility change failed' })\n }\n}\n\n/**\n * Unpublish or republish a listing (AGL-3321).\n *\n * This was the one client write to a listing: the seller panel set\n * `deletedAt` directly. Browse now queries a listing's AUDIENCE, which\n * follows from `deletedAt` among other fields, and a client write cannot\n * re-derive it — so the toggle moved here, beside `set-visibility`, and the\n * rules deny `deletedAt` to clients. Publisher-or-staff, like the other two\n * listing actions; unlike them it reaches a listing that is already\n * unpublished, since republishing is half of what it does.\n */\nconst setListingPublished: PluginApiHandler = async (req, res) => {\n const listingId = String(req.body?.listingId ?? '')\n const published = req.body?.published === true\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\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 try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listing = (await listingRef.get()).data()\n if (!listing) return res.status(404).json({ error: 'Unknown listing' })\n const isPublisher = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n if (!isPublisher && decoded['staff'] !== true) {\n return res.status(403).json({ error: 'Not your listing' })\n }\n await listingRef.set(\n {\n deletedAt: published\n ? null\n : firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n await refreshListingQueryFields(listingRef)\n return res.status(200).json({ ok: true, published })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publishing change failed' })\n }\n}\n\nexport const publishPluginHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n // Content-only listing edits (AGL-430): publishers refresh the docs the\n // detail page renders (readme/screenshots/links/…) without shipping a\n // new bundle. Publisher-or-staff, validated exactly like publish.\n if (req.body?.action === 'update-listing') {\n return updateListingContent(req, res)\n }\n // Private ⇄ public (AGL-968/994), without shipping a new bundle.\n if (req.body?.action === 'set-visibility') {\n return setListingVisibility(req, res)\n }\n // Unpublish ⇄ republish (AGL-3321), which was a client write.\n if (req.body?.action === 'set-published') {\n return setListingPublished(req, res)\n }\n const body = req.body ?? {}\n const headers = req.headers as Partial<Record<string, string>>\n const displayName = String(body?.displayName ?? '').slice(0, 80)\n const description = String(body?.description ?? '').slice(0, 500)\n const category = String(body?.category ?? '').slice(0, 40)\n const changelog = String(body?.changelog ?? '').slice(0, 1000)\n const priceUsd = Math.round(Number(body?.priceUsd ?? 0)) || 0\n const bundleBase64 = String(body?.bundle ?? '')\n // Listing content (AGL-430): optional publisher docs for the detail page.\n const contentVerdict = validateListingContent(body ?? {})\n if (!contentVerdict.ok) {\n return res.status(400).json({ error: contentVerdict.error })\n }\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 (!displayName.trim() || !bundleBase64) {\n return res.status(400).json({ error: 'Missing displayName or bundle' })\n }\n\n const validation = validatePluginManifest(body?.manifest)\n if (validation.ok === false) {\n return res.status(422).json({ error: validation.error })\n }\n const manifest = validation.manifest\n\n const authorization = 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 // Act on the org the client is in (AGL-868), not a guessed first-org — a\n // member can belong to several, and the plugin publishes under one of them.\n const requestedOrgId = String(body?.orgId ?? '').trim() || undefined\n const membership = await resolveOrgPermissions(decoded.uid, {\n orgId: requestedOrgId,\n })\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 // Plan gate rides the acting org's doc (AGL-238/868).\n const orgForUser = await getOrgForUser(\n decoded.uid,\n membership.orgId ?? requestedOrgId,\n )\n const org = orgForUser?.org ?? {}\n if (!checkEntitlement(org, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n\n // Publish rate limit (AGL-437): a runaway or abusive publisher can't\n // flood the artifacts bucket/review queue — 20 publishes per UTC day.\n // Scoped to the publishing ORG (AGL-652): per-user it would multiply by\n // headcount, letting a big team flood the queue 20 publishes at a time.\n const dayKey = new Date().toISOString().slice(0, 10)\n if (!orgForUser?.orgId) {\n return res.status(409).json({ error: 'No publishing organization' })\n }\n const limiterRef = firestore\n .collection('publisherProfiles')\n .doc(orgForUser.orgId)\n .collection('meta')\n .doc('publishWindow')\n const allowed = await firestore.runTransaction(async (transaction) => {\n const window = (await transaction.get(limiterRef)).data() ?? {}\n const count = window.dayKey === dayKey ? Number(window.count ?? 0) : 0\n if (count >= 20) return false\n transaction.set(limiterRef, { dayKey, count: count + 1 })\n return true\n })\n if (!allowed) {\n return res\n .status(429)\n .json({ error: 'Daily publish limit reached — try again tomorrow' })\n }\n\n const publisher = await resolvePublisherProfile(firestore, orgForUser.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: 'plugins',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const bundle = Buffer.from(bundleBase64, 'base64')\n if (!bundle.length || bundle.length > MAX_BUNDLE_BYTES) {\n return res.status(413).json({ error: 'Bundle is empty or too large' })\n }\n // Static verification (AGL-426): the same checks as the local\n // `verify-plugin-bundle.mjs`, so a bundle that passes there publishes\n // here. Entry exports, self-containment, forbidden APIs, size.\n //\n // The manifest goes in too (AGL-964): the checker diffs the bundle's\n // network calls against the origins declared here, and a call to an\n // origin the manifest omits is refused at publish rather than silently\n // blocked by the CSP at runtime, where the publisher would meet it as a\n // mystery in production.\n //\n // And what register() registers is compared with the manifest's\n // `contributes` (AGL-3116), which a new version must carry whenever it\n // registers anything: the loaders place a plugin by that declaration\n // alone, so a registration it omits is a surface that never loads.\n const verification = checkPluginBundle(bundle.toString('utf8'), {\n maxBytes: MAX_PLUGIN_BUNDLE_BYTES,\n declaredNetwork: manifest.capabilities?.network ?? [],\n declaredContributions: manifest.contributes ?? null,\n requireContributions: true,\n })\n if (!verification.ok) {\n return res.status(422).json({\n error: 'Bundle failed verification',\n problems: verification.problems,\n })\n }\n const sha256 = createHash('sha256')\n .update(new Uint8Array(bundle))\n .digest('hex')\n\n // Isolated artifacts bucket (AGL-43 §3). Without it configured we\n // refuse rather than store executable code in the app bucket.\n const artifactsBucket = process.env.PLUGIN_ARTIFACTS_BUCKET\n if (!artifactsBucket) {\n return res.status(501).json({\n error:\n 'Plugin publishing is not configured (missing ' +\n 'PLUGIN_ARTIFACTS_BUCKET).',\n })\n }\n\n // One listing per publisher+plugin id: re-publishing bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('pluginId', '==', manifest.id)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n\n // Pre-submission attestation (AGL-969). Blocks the publish rather than\n // warning, because the whole point is that these answers exist BEFORE a\n // reviewer spends time on the bundle.\n //\n // Whether this is an update is decided here, from whether a listing\n // already exists — never from a client flag, which would make \"this is\n // my first version\" a way to skip the changelog item.\n const ticked = (\n Array.isArray(body?.attestation)\n ? body.attestation.map((id: unknown) => String(id))\n : []\n ).filter((id: string) =>\n PUBLISHER_ATTESTATION.some((item) => item.id === id),\n )\n const isUpdate = !existing.empty\n const missing = missingAttestations(ticked, isUpdate)\n if (missing.length) {\n return res.status(428).json({\n error:\n 'Confirm the pre-submission checklist before publishing: ' +\n attestationLabels(missing).join('; '),\n missingAttestations: missing,\n })\n }\n\n // An attestation needs something to be about (AGL-1076). `repository`\n // was confirmable in a form with no repository field, so listings\n // reached review carrying a signed claim that a URL we never collected\n // was public and matched — and the reviewer's first link went nowhere.\n //\n // Checked against the SUBMISSION, not the listing doc: a first publish\n // has no listing, and on an update the tick is a statement about what\n // this publish declares. `contentVerdict.content` is the normalized\n // value about to be stored, so the gate and the write cannot disagree.\n const missingSubjects = missingAttestationSubjects(\n contentVerdict.content as Record<string, unknown>,\n isUpdate,\n )\n if (missingSubjects.length) {\n return res.status(428).json({\n error:\n 'These are confirmed above but not filled in: ' +\n missingSubjects.map((subject) => subject.label).join('; '),\n missingAttestationSubjects: missingSubjects.map(\n (subject) => subject.field,\n ),\n })\n }\n\n // The phishing screen, before anything is stored or queued (AGL-3365).\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n changelog,\n readme: contentVerdict.content?.readme ?? null,\n urls: [contentVerdict.content?.homepageUrl, contentVerdict.content?.repositoryUrl],\n },\n official: decoded['staff'] === true,\n org,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n\n // Immutable content-addressed write — a new build is a new object, so a\n // consumer's pinned version can never be overwritten underneath it.\n const objectPath = pluginArtifactPath(\n listingRef.id,\n manifest.version,\n sha256,\n )\n const bucket = firebaseAdmin.app().storage().bucket(artifactsBucket)\n const file = bucket.file(objectPath)\n const [alreadyStored] = await file.exists()\n if (!alreadyStored) {\n await file.save(bundle, {\n contentType: 'application/javascript',\n metadata: {\n cacheControl: 'public, max-age=31536000, immutable',\n },\n })\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n await listingRef.set(\n {\n type: 'plugin',\n artifactType: 'plugin',\n profileId: publisher.orgId,\n pluginId: manifest.id,\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 ...contentVerdict.content,\n latestVersion: manifest.version,\n // Mirror of `pluginVersions/{latestVersion}.reviewState`, for the\n // marketplace (AGL-1121). Browse reads listings directly and cannot\n // afford a version subdoc read per card, so the newest bytes' verdict\n // has to live here to be shown at all.\n //\n // Reset to 'pending' on EVERY publish, including a bump on a listing\n // that staff already verified. That is the whole point: the listing's\n // `reviewStatus` deliberately survives a version bump, so before this\n // the marketplace badged brand-new, unreviewed bytes as reviewed.\n latestVersionReviewState: 'pending',\n deletedAt: null,\n // Review queue (AGL-432): first publish enters as 'submitted';\n // version bumps keep whatever status staff granted — which is safe\n // now that installs resolve the newest APPROVED version rather than\n // `latestVersion` (AGL-966). A listed plugin stays listed on its\n // reviewed bytes while the new version waits in the queue.\n ...(existing.empty && { createdAt: now, reviewStatus: 'submitted' }),\n // Private plugins never reach the marketplace but take the same\n // review path (AGL-968). Only settable on first publish; changing\n // visibility later is a separate, deliberate action.\n ...(existing.empty && {\n visibility: req.body?.visibility === 'private' ? 'private' : 'public',\n }),\n updatedAt: now,\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 // Version snapshots are server-only; the loader reads sha256 + manifest\n // to verify integrity and stamp CSP before executing.\n await listingRef\n .collection('pluginVersions')\n .doc(manifest.version)\n .set({\n version: manifest.version,\n sha256,\n objectPath,\n manifest,\n ...(changelog.trim() && { changelog: changelog.trim() }),\n // The repository as declared FOR THESE BYTES (AGL-1076). The listing\n // carries whatever the latest publish said, and a publisher may move\n // or rename a repo between versions — a reviewer looking at v1.0.2\n // needs the link that was attested against v1.0.2's sha256, which is\n // the same reasoning that pins the attestation itself.\n ...(contentVerdict.content?.repositoryUrl && {\n repositoryUrl: contentVerdict.content.repositoryUrl,\n }),\n publishedAt: now,\n // What the publisher stated about these bytes (AGL-969). Same shape\n // as the staff `reviewChecklist` and pinned to the same sha256, so\n // a republish under this version string re-asks rather than\n // inheriting an attestation made about code that has since changed.\n //\n // `by` is the uid that submitted, not the org: takedown and removal\n // are defensible because a named person said this on a date, and an\n // org id names nobody.\n publisherAttestation: Object.fromEntries(\n ticked.map((id: string) => [id, { by: decoded.uid, at: now, sha256 }]),\n ),\n // Every version starts UNREVIEWED (AGL-966). This doc is written\n // with .set() and no merge, so a republish of the same version\n // string also resets it — new bytes, new review, by construction.\n reviewState: 'pending',\n // Keep the verdict we just computed (AGL-962). The bundle is\n // immutable and content-addressed, so this result holds for as long\n // as the checker does — the review page reads it instead of\n // re-downloading a megabyte per view. `verifierVersion` is what\n // makes that safe: bump PLUGIN_VERIFIER_VERSION and every stored\n // verdict is recomputed on next read.\n verification: {\n ok: verification.ok,\n problems: verification.problems,\n sha256,\n verifierVersion: PLUGIN_VERIFIER_VERSION,\n checkedAt: now,\n },\n })\n\n // Tell staff a version is waiting (AGL-970). Nothing announced a\n // submission before this: reviewers had to notice a new row by\n // visiting the queue, and an UPDATE to an already-listed plugin\n // produced no row at all. Persistent per-user notifications, so it\n // survives a refresh and stays until someone acts on it.\n await notifyStaff({\n type: 'marketplace.review',\n title: `${displayName.trim()} v${manifest.version} needs review`,\n body: existing.empty\n ? 'A new plugin was submitted to the marketplace.'\n : 'A new version was published. The previously approved version ' +\n 'keeps installing until this one is reviewed.',\n link: `/admin/plugin-reviews/${listingRef.id}`,\n }).catch(() => undefined)\n\n return res\n .status(200)\n .json({ listingId: listingRef.id, version: manifest.version, sha256 })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n"],"names":["displayNameSearchFields","checkEntitlement","checkPluginBundle","createResourceUid","MAX_PLUGIN_BUNDLE_BYTES","pluginArtifactPath","PLUGIN_VERIFIER_VERSION","validatePluginManifest","firebaseAdmin","getOrgForUser","notifyStaff","resolveOrgPermissions","canActAsPublisher","resolvePublisherProfile","publishPreconditionRefusal","listingSubmissionRefusal","refreshListingQueryFields","attestationLabels","missingAttestations","missingAttestationSubjects","PUBLISHER_ATTESTATION","createHash","marketplacePriceRefusal","missingPublicListingContent","validateListingContent","MAX_BUNDLE_BYTES","updateListingContent","req","res","listingId","String","body","status","json","error","authorization","headers","idToken","startsWith","slice","length","undefined","verdict","ok","listing","content","decoded","app","auth","verifyIdToken","listingRef","firestore","collection","doc","get","data","deletedAt","isPublisher","uid","profileId","description","displayName","nextName","trim","screened","publisherOrgId","readme","urls","homepageUrl","repositoryUrl","official","set","updatedAt","FieldValue","serverTimestamp","merge","console","setListingVisibility","visibility","missing","join","setListingPublished","published","publishPluginHandler","method","action","category","changelog","priceUsd","Math","round","Number","bundleBase64","bundle","contentVerdict","priceRefusal","validation","manifest","membership","requestedOrgId","orgId","permissions","publishToMarketplace","orgForUser","org","dayKey","Date","toISOString","limiterRef","allowed","runTransaction","transaction","window","count","publisher","refusal","sells","Buffer","from","verification","toString","maxBytes","declaredNetwork","capabilities","network","declaredContributions","contributes","requireContributions","problems","sha256","update","Uint8Array","digest","artifactsBucket","process","env","PLUGIN_ARTIFACTS_BUCKET","existing","where","id","limit","empty","docs","ref","ticked","Array","isArray","attestation","map","filter","some","item","isUpdate","missingSubjects","subject","label","field","publisherName","objectPath","version","bucket","storage","file","alreadyStored","exists","save","contentType","metadata","cacheControl","now","type","artifactType","pluginId","latestVersion","latestVersionReviewState","createdAt","reviewStatus","publishedAt","publisherAttestation","Object","fromEntries","by","at","reviewState","verifierVersion","checkedAt","title","link","catch"],"mappings":"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"}
@@ -21,6 +21,7 @@ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
21
21
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
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
  const MAX_TEMPLATE_SCREENS = 25;
26
27
  /**
@@ -66,7 +67,7 @@ const MAX_TEMPLATE_SCREENS = 25;
66
67
  error: 'Unauthenticated'
67
68
  });
68
69
  try {
69
- var _hostSnapshot_get, _ref5, _hostSnapshot_get1, _existing_docs__get, _hostSnapshot_get2;
70
+ var _hostSnapshot_get, _ref5, _hostSnapshot_get1, _existing_docs__get, _orgForHost_org, _hostSnapshot_get2;
70
71
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
71
72
  const membership = await resolveOrgPermissions(decoded.uid, {
72
73
  hostId
@@ -167,6 +168,19 @@ const MAX_TEMPLATE_SCREENS = 25;
167
168
  const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
168
169
  const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
169
170
  const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
171
+ // The phishing screen, before anything is listed (AGL-3365).
172
+ const screened = await listingSubmissionRefusal({
173
+ publisherOrgId: publisher.orgId,
174
+ content: {
175
+ displayName,
176
+ publisherName: publisher.displayName,
177
+ description,
178
+ pageNodes: screens
179
+ },
180
+ official: decoded['staff'] === true,
181
+ org: (_orgForHost_org = orgForHost.org) != null ? _orgForHost_org : null
182
+ });
183
+ if (screened) return res.status(screened.status).json(screened.body);
170
184
  await listingRef.set(_extends({
171
185
  // `kind` retained alongside artifactType so listings published by
172
186
  // an older build still resolve (AGL-654).
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-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 CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport {\n marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { refreshListingQueryFields } from './listing-query-fields'\n\nconst MAX_TEMPLATE_SCREENS = 25\n\n/**\n * Publishes a host as a site template (AGL-137): captures every published\n * screen (nodes run through the same marketplace sanitizer as component\n * publishing — the id allowlist and size caps apply per screen) plus the\n * theme into a `marketplaceListings` doc with `kind: 'template'`. Same\n * gates as component publishing: host admin, marketplace profile,\n * `marketplaceSelling` for plan-gated workspaces, Stripe onboarding for paid\n * listings. One template per source host; re-publishing bumps the version.\n */\nexport const publishTemplateHandler: 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 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 || !displayName.trim()) {\n return res.status(400).json({ error: 'Missing hostId 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: '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 // Plan gate rides the owning org's doc (AGL-238).\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 // Publishing is ORG-ONLY (AGL-652).\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 // Capture: every screen in the routing map (published) with its\n // published version, sanitized like any marketplace definition.\n const routingMap = (hostSnapshot.get('screens') ?? {}) as Record<\n string,\n string\n >\n const screenIds = Object.keys(routingMap).slice(0, MAX_TEMPLATE_SCREENS)\n if (!screenIds.length) {\n return res\n .status(422)\n .json({ error: 'Publish at least one screen before templating' })\n }\n const screens: any[] = []\n for (const screenId of screenIds) {\n const screenSnapshot = await hostRef\n .collection('screens')\n .doc(screenId)\n .get()\n const screen = screenSnapshot.data() as any\n if (!screen || screen.deletedAt || !screen.versionId) continue\n const versionSnapshot = await hostRef\n .collection('screens')\n .doc(screenId)\n .collection('versions')\n .doc(String(screen.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, that Buffer is truthy, so the guard below let it\n // straight through to the sanitizer — which found no `_@_` on it and\n // told the author their screen had no root node. Every besigner-edited\n // screen failed here, blaming content that was fine.\n const nodes = decodeStoredNodes(versionSnapshot.get('nodes'))\n if (!nodes) continue\n const sanitized = sanitizeMarketplaceDefinition({\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes,\n })\n if (sanitized.ok === false) {\n return res.status(422).json({\n error: `Screen \"${screen.displayName ?? screenId}\": ${sanitized.error}`,\n })\n }\n screens.push({\n displayName: screen.displayName ?? 'Screen',\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n slug: routingMap[screenId],\n nodes: sanitized.nodes,\n })\n }\n if (!screens.length) {\n return res\n .status(422)\n .json({ error: 'No publishable screens with versions found' })\n }\n\n // One template listing per source host; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceHostId', '==', hostId)\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 // `kind` retained alongside artifactType so listings published by\n // an older build still resolve (AGL-654).\n kind: 'template',\n artifactType: 'template',\n profileId: publisher.orgId,\n sourceHostId: hostId,\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 screenCount: screens.length,\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 template: {\n screens,\n theme: hostSnapshot.get('theme') ?? null,\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: 'Template publish failed' })\n }\n}\n"],"names":["displayNameSearchFields","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","marketplacePriceRefusal","sanitizeMarketplaceDefinition","firebaseAdmin","getOrgForHost","resolveOrgPermissions","resolvePublisherProfile","publishPreconditionRefusal","refreshListingQueryFields","MAX_TEMPLATE_SCREENS","publishTemplateHandler","req","res","method","status","json","error","hostId","String","body","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","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","routingMap","screenIds","Object","keys","screens","screenId","screen","screenSnapshot","data","deletedAt","versionId","versionSnapshot","nodes","sanitized","rootId","ok","push","seo","slug","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","set","kind","artifactType","profileId","sourceHostId","screenCount","latestVersion","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","template","theme","listingId","id","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-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 CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport {\n marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\n\nconst MAX_TEMPLATE_SCREENS = 25\n\n/**\n * Publishes a host as a site template (AGL-137): captures every published\n * screen (nodes run through the same marketplace sanitizer as component\n * publishing — the id allowlist and size caps apply per screen) plus the\n * theme into a `marketplaceListings` doc with `kind: 'template'`. Same\n * gates as component publishing: host admin, marketplace profile,\n * `marketplaceSelling` for plan-gated workspaces, Stripe onboarding for paid\n * listings. One template per source host; re-publishing bumps the version.\n */\nexport const publishTemplateHandler: 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 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 || !displayName.trim()) {\n return res.status(400).json({ error: 'Missing hostId 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: '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 // Plan gate rides the owning org's doc (AGL-238).\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 // Publishing is ORG-ONLY (AGL-652).\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 // Capture: every screen in the routing map (published) with its\n // published version, sanitized like any marketplace definition.\n const routingMap = (hostSnapshot.get('screens') ?? {}) as Record<\n string,\n string\n >\n const screenIds = Object.keys(routingMap).slice(0, MAX_TEMPLATE_SCREENS)\n if (!screenIds.length) {\n return res\n .status(422)\n .json({ error: 'Publish at least one screen before templating' })\n }\n const screens: any[] = []\n for (const screenId of screenIds) {\n const screenSnapshot = await hostRef\n .collection('screens')\n .doc(screenId)\n .get()\n const screen = screenSnapshot.data() as any\n if (!screen || screen.deletedAt || !screen.versionId) continue\n const versionSnapshot = await hostRef\n .collection('screens')\n .doc(screenId)\n .collection('versions')\n .doc(String(screen.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, that Buffer is truthy, so the guard below let it\n // straight through to the sanitizer — which found no `_@_` on it and\n // told the author their screen had no root node. Every besigner-edited\n // screen failed here, blaming content that was fine.\n const nodes = decodeStoredNodes(versionSnapshot.get('nodes'))\n if (!nodes) continue\n const sanitized = sanitizeMarketplaceDefinition({\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes,\n })\n if (sanitized.ok === false) {\n return res.status(422).json({\n error: `Screen \"${screen.displayName ?? screenId}\": ${sanitized.error}`,\n })\n }\n screens.push({\n displayName: screen.displayName ?? 'Screen',\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n slug: routingMap[screenId],\n nodes: sanitized.nodes,\n })\n }\n if (!screens.length) {\n return res\n .status(422)\n .json({ error: 'No publishable screens with versions found' })\n }\n\n // One template listing per source host; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceHostId', '==', hostId)\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: screens,\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 // `kind` retained alongside artifactType so listings published by\n // an older build still resolve (AGL-654).\n kind: 'template',\n artifactType: 'template',\n profileId: publisher.orgId,\n sourceHostId: hostId,\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 screenCount: screens.length,\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 template: {\n screens,\n theme: hostSnapshot.get('theme') ?? null,\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: 'Template publish failed' })\n }\n}\n"],"names":["displayNameSearchFields","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","marketplacePriceRefusal","sanitizeMarketplaceDefinition","firebaseAdmin","getOrgForHost","resolveOrgPermissions","resolvePublisherProfile","publishPreconditionRefusal","listingSubmissionRefusal","refreshListingQueryFields","MAX_TEMPLATE_SCREENS","publishTemplateHandler","req","res","method","status","json","error","hostId","String","body","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","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","routingMap","screenIds","Object","keys","screens","screenId","screen","screenSnapshot","data","deletedAt","versionId","versionSnapshot","nodes","sanitized","rootId","ok","push","seo","slug","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","pageNodes","official","set","kind","artifactType","profileId","sourceHostId","screenCount","latestVersion","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","template","theme","listingId","id","console"],"mappings":"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"}
@@ -22,6 +22,7 @@ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
22
22
  import { marketplacePriceRefusal } from "../model/marketplace.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
  /**
27
28
  * Publishes a site's theme to the marketplace (AGL-1020).
@@ -75,7 +76,7 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
75
76
  error: 'Unauthenticated'
76
77
  });
77
78
  try {
78
- var _hostSnapshot_get, _ref5, _hostSnapshot_get1, _existing_docs__get;
79
+ var _hostSnapshot_get, _ref5, _hostSnapshot_get1, _existing_docs__get, _orgForHost_org;
79
80
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
80
81
  const membership = await resolveOrgPermissions(decoded.uid, {
81
82
  hostId
@@ -140,6 +141,18 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
140
141
  const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
141
142
  const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
142
143
  const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
144
+ // The phishing screen, before anything is listed (AGL-3365).
145
+ const screened = await listingSubmissionRefusal({
146
+ publisherOrgId: publisher.orgId,
147
+ content: {
148
+ displayName,
149
+ publisherName: publisher.displayName,
150
+ description
151
+ },
152
+ official: decoded['staff'] === true,
153
+ org: (_orgForHost_org = orgForHost.org) != null ? _orgForHost_org : null
154
+ });
155
+ if (screened) return res.status(screened.status).json(screened.body);
143
156
  await listingRef.set(_extends({
144
157
  artifactType: 'theme',
145
158
  profileId: publisher.orgId,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-theme.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 {\n themeArtifactContent,\n validateThemeForPublish,\n} from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal } from '../model/marketplace'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { refreshListingQueryFields } from './listing-query-fields'\n\n/**\n * Publishes a site's theme to the marketplace (AGL-1020).\n *\n * A theme is the one artifact that is pure data — no nodes, no code, no\n * bundle — so it needs no sanitizer allowlist the way a component tree does.\n * What it needs instead is validation of the two things that make an installed\n * theme unusable, and both are refused here rather than caught in review:\n * a missing colour scheme, and text nobody can read. See\n * `validateThemeForPublish` for why those two and not others.\n *\n * The theme is stored as a field on the host document, not as its own\n * collection, so unlike every other publish route there is no artifact id to\n * pick — the site IS the selection. That also makes the listing key\n * `sourceThemeHostId`, so re-publishing the same site's theme bumps its version\n * rather than opening a second listing.\n *\n * Gates mirror `publish-layout` exactly: org permission, host role,\n * `marketplaceSelling`, a publisher profile, and payouts before anything sells.\n */\nexport const publishThemeHandler: 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 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 || !displayName.trim()) {\n return res.status(400).json({ error: 'Missing hostId 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: 'themes',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n // The SITE'S OWN theme, before any resolution against a marketplace base or\n // the site's overrides. Publishing the resolved view would bake this site's\n // private customisations into an artifact other people install.\n const theme = themeArtifactContent((hostSnapshot.get('theme') ?? {}) as any)\n const validation = validateThemeForPublish(theme)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n // The whole list, so the dialog can show every problem at once rather\n // than making the publisher fix them one refusal at a time.\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n\n // One listing per source site's theme; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceThemeHostId', '==', hostId)\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: 'theme',\n profileId: publisher.orgId,\n sourceHostId: hostId,\n sourceThemeHostId: hostId,\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 theme,\n publishedAt: now,\n })\n\n return res.status(200).json({\n listingId: listingRef.id,\n version,\n // Warnings do not block, but publishing past one silently would make the\n // pre-flight pointless — the dialog reports what shipped anyway.\n warnings: validation.warnings,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Theme publish failed' })\n }\n}\n\nexport default publishThemeHandler\n"],"names":["displayNameSearchFields","checkEntitlement","createResourceUid","themeArtifactContent","validateThemeForPublish","firebaseAdmin","getOrgForHost","resolveOrgPermissions","marketplacePriceRefusal","resolvePublisherProfile","publishPreconditionRefusal","refreshListingQueryFields","publishThemeHandler","req","res","method","status","json","error","hostId","String","body","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","theme","validation","ok","errors","message","warnings","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","set","artifactType","profileId","sourceHostId","sourceThemeHostId","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","listingId","id","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-theme.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 {\n themeArtifactContent,\n validateThemeForPublish,\n} from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal } from '../model/marketplace'\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 theme to the marketplace (AGL-1020).\n *\n * A theme is the one artifact that is pure data — no nodes, no code, no\n * bundle — so it needs no sanitizer allowlist the way a component tree does.\n * What it needs instead is validation of the two things that make an installed\n * theme unusable, and both are refused here rather than caught in review:\n * a missing colour scheme, and text nobody can read. See\n * `validateThemeForPublish` for why those two and not others.\n *\n * The theme is stored as a field on the host document, not as its own\n * collection, so unlike every other publish route there is no artifact id to\n * pick — the site IS the selection. That also makes the listing key\n * `sourceThemeHostId`, so re-publishing the same site's theme bumps its version\n * rather than opening a second listing.\n *\n * Gates mirror `publish-layout` exactly: org permission, host role,\n * `marketplaceSelling`, a publisher profile, and payouts before anything sells.\n */\nexport const publishThemeHandler: 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 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 || !displayName.trim()) {\n return res.status(400).json({ error: 'Missing hostId 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: 'themes',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n // The SITE'S OWN theme, before any resolution against a marketplace base or\n // the site's overrides. Publishing the resolved view would bake this site's\n // private customisations into an artifact other people install.\n const theme = themeArtifactContent((hostSnapshot.get('theme') ?? {}) as any)\n const validation = validateThemeForPublish(theme)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n // The whole list, so the dialog can show every problem at once rather\n // than making the publisher fix them one refusal at a time.\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n\n // One listing per source site's theme; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceThemeHostId', '==', hostId)\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 org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n\n await listingRef.set(\n {\n artifactType: 'theme',\n profileId: publisher.orgId,\n sourceHostId: hostId,\n sourceThemeHostId: hostId,\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 theme,\n publishedAt: now,\n })\n\n return res.status(200).json({\n listingId: listingRef.id,\n version,\n // Warnings do not block, but publishing past one silently would make the\n // pre-flight pointless — the dialog reports what shipped anyway.\n warnings: validation.warnings,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Theme publish failed' })\n }\n}\n\nexport default publishThemeHandler\n"],"names":["displayNameSearchFields","checkEntitlement","createResourceUid","themeArtifactContent","validateThemeForPublish","firebaseAdmin","getOrgForHost","resolveOrgPermissions","marketplacePriceRefusal","resolvePublisherProfile","publishPreconditionRefusal","listingSubmissionRefusal","refreshListingQueryFields","publishThemeHandler","req","res","method","status","json","error","hostId","String","body","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","theme","validation","ok","errors","message","warnings","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","official","set","artifactType","profileId","sourceHostId","sourceThemeHostId","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","listingId","id","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,uBAAuB,QAAQ,qCAAoC;AAC5E,SAASC,gBAAgB,EAAEC,iBAAiB,QAAQ,sBAAqB;AAEzE,SACEC,oBAAoB,EACpBC,uBAAuB,QAClB,2CAA0C;AACjD,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACvE,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SAASC,uBAAuB,QAAQ,0BAAsB;AAC9D,SAASC,uBAAuB,QAAQ,yBAAqB;AAC7D,SAASC,0BAA0B,QAAQ,6BAAyB;AACpE,SAASC,wBAAwB,QAAQ,sBAAkB;AAC3D,SAASC,yBAAyB,QAAQ,4BAAwB;AAElE;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,MAAMC,sBAAwC,OAAOC,KAAKC;0CAoBlCD;QAhBPA,WACKA,YACAA,YACHA,YACuBA;IAP/C,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,SAASC,gBAAOP,YAAAA,IAAIQ,IAAI,qBAARR,UAAUM,MAAM,mBAAI;IAC1C,MAAMG,cAAcF,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUS,WAAW,oBAAI;IACpD,MAAMC,cAAcH,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUU,WAAW,oBAAI;IACpD,MAAMC,WAAWJ,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUW,QAAQ,oBAAI;IAC9C,MAAMC,WAAWC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACC,iBAAOhB,aAAAA,IAAIQ,IAAI,qBAARR,WAAUY,QAAQ,oBAAI;IACrE,IAAI,CAACN,UAAU,CAACG,YAAYQ,IAAI,IAAI;QAClC,OAAOhB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAgC;IACvE;IACA,4EAA4E;IAC5E,8EAA8E;IAC9E,8EAA8E;IAC9E,yEAAyE;IACzE,MAAMa,eAAexB,wBAAwBkB;IAC7C,IAAIM,cAAc;QAChB,OAAOjB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAOa;QAAa;IACpD;IACA,MAAMC,gBAAgBZ,QAAOP,6BAAAA,IAAIoB,OAAO,CAACD,aAAa,YAAzBnB,6BAA6B;IAC1D,MAAMqB,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS,OAAOpB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,IAAI;YAgBkBqB,0BA4BgBA,oBAwBzBC,qBAYJC;QA/EP,MAAMC,UAAU,MAAMtC,cAAcuC,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACX;QAC/D,MAAMY,aAAa,MAAMxC,sBAAsBoC,QAAQK,GAAG,EAAE;YAAE5B;QAAO;QACrE,IAAI,CAAC2B,WAAWE,WAAW,CAACC,oBAAoB,EAAE;YAChD,OAAOnC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OACE;YACJ;QACF;QACA,MAAMgC,YAAY9C,cAAcuC,GAAG,GAAGO,SAAS;QAE/C,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAAClC;QAClD,MAAMoB,eAAe,MAAMY,QAAQG,GAAG;QACtC,IAAI,CAACf,aAAagB,MAAM,EAAE;YACxB,OAAOzC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAe;QACtD;QACA,MAAMsC,aAAa,EAACjB,oBAAAA,aAAae,GAAG,CAAC,0BAAjBf,oBAAmC,CAAC,EAAE,CAACG,QAAQK,GAAG,CAAC;QACvE,IAAIS,eAAe,WAAWA,eAAe,UAAU;YACrD,OAAO1C,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA6B;QACpE;QAEA,MAAMuB,aAAa,MAAMpC,cAAcc;QACvC,MAAMsC,eAAMhB,8BAAAA,WAAYgB,GAAG,oBAAI,CAAC;QAChC,IAAI,CAACzD,iBAAiByD,KAAY,uBAAuB;YACvD,OAAO3C,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO;YACT;QACF;QACA,IAAI,EAACuB,8BAAAA,WAAYiB,KAAK,GAAE;YACtB,OAAO5C,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAkC;QACzE;QAEA,MAAMyC,YAAY,MAAMnD,wBAAwB0C,WAAWT,WAAWiB,KAAK;QAC3E,4DAA4D;QAC5D,iDAAiD;QACjD,MAAME,UAAUnD,2BAA2BkD,WAAW;YACpDlC;YACAoC,OAAO;QACT;QACA,IAAID,SAAS,OAAO9C,IAAIE,MAAM,CAAC4C,QAAQ5C,MAAM,EAAEC,IAAI,CAAC2C,QAAQvC,IAAI;QAEhE,4EAA4E;QAC5E,4EAA4E;QAC5E,gEAAgE;QAChE,MAAMyC,QAAQ5D,sBAAsBqC,qBAAAA,aAAae,GAAG,CAAC,oBAAjBf,qBAA6B,CAAC;QAClE,MAAMwB,aAAa5D,wBAAwB2D;QAC3C,IAAI,CAACC,WAAWC,EAAE,EAAE;;gBAETD;YADT,OAAOjD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,KAAK,YAAE6C,sBAAAA,WAAWE,MAAM,CAAC,EAAE,qBAApBF,oBAAsBG,OAAO,oBAAI;gBACxC,sEAAsE;gBACtE,4DAA4D;gBAC5DD,QAAQF,WAAWE,MAAM;gBACzBE,UAAUJ,WAAWI,QAAQ;YAC/B;QACF;QAEA,qEAAqE;QACrE,MAAM3B,WAAW,MAAMU,UACpBE,UAAU,CAAC,uBACXgB,KAAK,CAAC,aAAa,MAAMT,UAAUD,KAAK,EACxCU,KAAK,CAAC,qBAAqB,MAAMjD,QACjCkD,KAAK,CAAC,GACNf,GAAG;QACN,MAAMgB,aAAa9B,SAAS+B,KAAK,GAC7BrB,UAAUE,UAAU,CAAC,uBAAuBC,GAAG,CAACpD,uBAChDuC,SAASgC,IAAI,CAAC,EAAE,CAACC,GAAG;QACxB,MAAMC,UAAUlC,SAAS+B,KAAK,GAC1B,IACA1C,QAAOW,sBAAAA,SAASgC,IAAI,CAAC,EAAE,CAAClB,GAAG,CAAC,4BAArBd,sBAAyC,KAAK;QACzD,MAAMmC,MAAMvE,cAAc8C,SAAS,CAAC0B,UAAU,CAACC,eAAe;QAE9D,6DAA6D;QAC7D,MAAMC,WAAW,MAAMpE,yBAAyB;YAC9CqE,gBAAgBpB,UAAUD,KAAK;YAC/BsB,SAAS;gBACP1D;gBACA2D,eAAetB,UAAUrC,WAAW;gBACpCC;YACF;YACA2D,UAAUxC,OAAO,CAAC,QAAQ,KAAK;YAC/Be,GAAG,GAAEhB,kBAAAA,WAAWgB,GAAG,YAAdhB,kBAAkB;QACzB;QACA,IAAIqC,UAAU,OAAOhE,IAAIE,MAAM,CAAC8D,SAAS9D,MAAM,EAAEC,IAAI,CAAC6D,SAASzD,IAAI;QAEnE,MAAMiD,WAAWa,GAAG,CAClB;YACEC,cAAc;YACdC,WAAW1B,UAAUD,KAAK;YAC1B4B,cAAcnE;YACdoE,mBAAmBpE;YACnBG,aAAaA,YAAYQ,IAAI;WAG1B/B,wBAAwBuB,YAAYQ,IAAI,KACvCP,YAAYO,IAAI,MAAM;YAAEP,aAAaA,YAAYO,IAAI;QAAG,GACxDN,SAASM,IAAI,MAAM;YAAEN,UAAUA,SAASM,IAAI;QAAG;YACnDL;YACA+D,eAAed;YACfe,WAAW;WACPjD,SAAS+B,KAAK,IAAI;YAAEmB,WAAWf;QAAI;YACvCgB,WAAWhB;YACXiB,gBAAgBxF,cAAc8C,SAAS,CAAC0B,UAAU,CAACiB,UAAU,CAAC;gBAC5DnB;gBACAoB,aAAa1F,cAAc8C,SAAS,CAAC6C,SAAS,CAACpB,GAAG;YACpD;YAEF;YAAEqB,OAAO;QAAK;QAEhB,gFAAgF;QAChF,MAAMrF,0BAA0B2D;QAChC,MAAMA,WAAWlB,UAAU,CAAC,YAAYC,GAAG,CAACjC,OAAOsD,UAAUS,GAAG,CAAC;YAC/DrB;YACAgC,aAAanB;QACf;QAEA,OAAO7D,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAC1BgF,WAAW3B,WAAW4B,EAAE;YACxBxB;YACA,yEAAyE;YACzE,iEAAiE;YACjEP,UAAUJ,WAAWI,QAAQ;QAC/B;IACF,EAAE,OAAOjD,OAAO;QACdiF,QAAQjF,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAuB;IAC9D;AACF,EAAC;AAED,eAAeN,oBAAmB"}
@@ -22,6 +22,7 @@ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
22
22
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
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
  /**
27
28
  * Publishes a host reusable component to the marketplace (AGL-44). Runs
@@ -66,7 +67,7 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
66
67
  error: 'Unauthenticated'
67
68
  });
68
69
  try {
69
- var _hostSnapshot_get, _ref6, _decodeStoredNodes, _existing_docs__get;
70
+ var _hostSnapshot_get, _ref6, _decodeStoredNodes, _existing_docs__get, _orgForHost_org;
70
71
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
71
72
  // Org-role permission gate (AGL-238).
72
73
  const membership = await resolveOrgPermissions(decoded.uid, {
@@ -157,6 +158,19 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
157
158
  const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
158
159
  const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
159
160
  const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
161
+ // The phishing screen, before anything is listed (AGL-3365).
162
+ const screened = await listingSubmissionRefusal({
163
+ publisherOrgId: publisher.orgId,
164
+ content: {
165
+ displayName,
166
+ publisherName: publisher.displayName,
167
+ description,
168
+ pageNodes: sanitized.nodes
169
+ },
170
+ official: decoded['staff'] === true,
171
+ org: (_orgForHost_org = orgForHost.org) != null ? _orgForHost_org : null
172
+ });
173
+ if (screened) return res.status(screened.status).json(screened.body);
160
174
  await listingRef.set(_extends({
161
175
  profileId: publisher.orgId,
162
176
  artifactType: 'component',