@aglyn/plugins-marketplace 1.0.0-beta.285 → 1.0.0-beta.288

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 (49) hide show
  1. package/package.json +14 -14
  2. package/src/lib/declarations.d.ts +26 -0
  3. package/src/lib/declarations.js +37 -0
  4. package/src/lib/declarations.js.map +1 -0
  5. package/src/lib/declarations.server.d.ts +2 -1
  6. package/src/lib/declarations.server.js +18 -1
  7. package/src/lib/declarations.server.js.map +1 -1
  8. package/src/lib/model/marketplace-site-permissions.d.ts +26 -0
  9. package/src/lib/model/marketplace-site-permissions.js +62 -0
  10. package/src/lib/model/marketplace-site-permissions.js.map +1 -0
  11. package/src/lib/model/marketplace.d.ts +24 -0
  12. package/src/lib/model/marketplace.js.map +1 -1
  13. package/src/lib/model/site-starter-listing.d.ts +49 -0
  14. package/src/lib/model/site-starter-listing.js +75 -0
  15. package/src/lib/model/site-starter-listing.js.map +1 -0
  16. package/src/lib/server/install-email-starter.js +7 -4
  17. package/src/lib/server/install-email-starter.js.map +1 -1
  18. package/src/lib/server/install-email-template.js +8 -5
  19. package/src/lib/server/install-email-template.js.map +1 -1
  20. package/src/lib/server/install-layout.js +7 -4
  21. package/src/lib/server/install-layout.js.map +1 -1
  22. package/src/lib/server/install-plugin.js +8 -8
  23. package/src/lib/server/install-plugin.js.map +1 -1
  24. package/src/lib/server/install-template.js +7 -4
  25. package/src/lib/server/install-template.js.map +1 -1
  26. package/src/lib/server/install-theme.js +10 -7
  27. package/src/lib/server/install-theme.js.map +1 -1
  28. package/src/lib/server/install.js +7 -4
  29. package/src/lib/server/install.js.map +1 -1
  30. package/src/lib/server/publish-email-starter.js +8 -5
  31. package/src/lib/server/publish-email-starter.js.map +1 -1
  32. package/src/lib/server/publish-email-template.js +8 -5
  33. package/src/lib/server/publish-email-template.js.map +1 -1
  34. package/src/lib/server/publish-layout.js +8 -5
  35. package/src/lib/server/publish-layout.js.map +1 -1
  36. package/src/lib/server/publish-starter.d.ts +37 -0
  37. package/src/lib/server/publish-starter.js +501 -0
  38. package/src/lib/server/publish-starter.js.map +1 -0
  39. package/src/lib/server/publish-template.js +9 -6
  40. package/src/lib/server/publish-template.js.map +1 -1
  41. package/src/lib/server/publish-theme.js +8 -5
  42. package/src/lib/server/publish-theme.js.map +1 -1
  43. package/src/lib/server/publish.js +8 -5
  44. package/src/lib/server/publish.js.map +1 -1
  45. package/src/lib/server/site-starter-source.d.ts +19 -0
  46. package/src/lib/server/site-starter-source.js +189 -0
  47. package/src/lib/server/site-starter-source.js.map +1 -0
  48. package/src/lib/server.js +3 -0
  49. package/src/lib/server.js.map +1 -1
@@ -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 { resolveSiteTheme } from '@aglyn/aglyn/app-utils/site-theme'\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 page 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: `Page \"${screen.displayName ?? screenId}\": ${sanitized.error}`,\n })\n }\n screens.push({\n displayName: screen.displayName ?? 'Page',\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 pages 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 // The theme the site shows, edits included (AGL-3404: every edit is\n // an override on the picked theme). A template carries it rather than\n // applies it, so there is no publisher's copy here to keep apart.\n theme: resolveSiteTheme(hostSnapshot.data() ?? {}) ?? 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":["resolveSiteTheme","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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"}
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 { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { resolveSiteTheme } from '@aglyn/aglyn/app-utils/site-theme'\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 // \"Publish a site’s items to the marketplace\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.sitePublish', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.sitePublish') })\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 page 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: `Page \"${screen.displayName ?? screenId}\": ${sanitized.error}`,\n })\n }\n screens.push({\n displayName: screen.displayName ?? 'Page',\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 pages 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 // The theme the site shows, edits included (AGL-3404: every edit is\n // an override on the picked theme). A template carries it rather than\n // applies it, so there is no publisher's copy here to keep apart.\n theme: resolveSiteTheme(hostSnapshot.data() ?? {}) ?? 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":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","resolveSiteTheme","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","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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"}
@@ -14,7 +14,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { displayNameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
17
+ */ import { holdsStampedSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
18
+ import { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from "../model/marketplace-site-permissions.js";
19
+ import { displayNameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
18
20
  import { checkEntitlement, createResourceUid } from "@aglyn/aglyn/server";
19
21
  import { hostThemeSource, resolveSiteTheme, themeArtifactContent, validateThemeForPublish } from "@aglyn/aglyn/app-utils/site-theme";
20
22
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
@@ -76,7 +78,7 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
76
78
  error: 'Unauthenticated'
77
79
  });
78
80
  try {
79
- var _hostSnapshot_get, _ref5, _hostSnapshot_data, _host_theme, _resolveSiteTheme, _existing_docs__get, _orgForHost_org;
81
+ var _ref5, _hostSnapshot_data, _host_theme, _resolveSiteTheme, _existing_docs__get, _orgForHost_org;
80
82
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
81
83
  const membership = await resolveOrgPermissions(decoded.uid, {
82
84
  hostId
@@ -94,10 +96,11 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
94
96
  error: 'Unknown site'
95
97
  });
96
98
  }
97
- const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
98
- if (memberRole !== 'admin' && memberRole !== 'editor') {
99
+ // "Publish a site’s items to the marketplace" (AGL-4047): the site's stamp, else
100
+ // the preset of the member's site role, the Admin and Editor this asked for.
101
+ if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.sitePublish', MARKETPLACE_SITE_ROLE_PRESET)) {
99
102
  return res.status(403).json({
100
- error: 'Not a site admin or editor'
103
+ error: marketplaceSitePermissionRefusal('marketplace.sitePublish')
101
104
  });
102
105
  }
103
106
  const orgForHost = await getOrgForHost(hostId);
@@ -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 hostThemeSource,\n resolveSiteTheme,\n themeArtifactContent,\n validateThemeForPublish,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/site-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 // What this site DESIGNED. On an installed theme that is the theme as\n // stored, never the resolved view: the edits there are this site's private\n // customisations of someone else's artifact, and publishing them would\n // bake them into a listing other people install. Every other theme is the\n // site's own work, and since every edit is an override on the picked theme\n // (AGL-3404) the work IS the resolved view — the stored copy of a built-in\n // theme holds none of it.\n const host = (hostSnapshot.data() ?? {}) as ThemeHostDocument\n const theme = themeArtifactContent(\n hostThemeSource(host) === 'installed'\n ? (host.theme ?? {})\n : (resolveSiteTheme(host) ?? {}),\n )\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","hostThemeSource","resolveSiteTheme","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","host","existing","orgForHost","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","org","orgId","publisher","refusal","sells","data","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":"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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 { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\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 hostThemeSource,\n resolveSiteTheme,\n themeArtifactContent,\n validateThemeForPublish,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/site-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 // \"Publish a site’s items to the marketplace\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.sitePublish', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.sitePublish') })\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 // What this site DESIGNED. On an installed theme that is the theme as\n // stored, never the resolved view: the edits there are this site's private\n // customisations of someone else's artifact, and publishing them would\n // bake them into a listing other people install. Every other theme is the\n // site's own work, and since every edit is an override on the picked theme\n // (AGL-3404) the work IS the resolved view — the stored copy of a built-in\n // theme holds none of it.\n const host = (hostSnapshot.data() ?? {}) as ThemeHostDocument\n const theme = themeArtifactContent(\n hostThemeSource(host) === 'installed'\n ? (host.theme ?? {})\n : (resolveSiteTheme(host) ?? {}),\n )\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":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","displayNameSearchFields","checkEntitlement","createResourceUid","hostThemeSource","resolveSiteTheme","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","host","existing","orgForHost","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","org","orgId","publisher","refusal","sells","data","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":"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"}
@@ -14,7 +14,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { displayNameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
17
+ */ import { holdsStampedSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
18
+ import { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from "../model/marketplace-site-permissions.js";
19
+ import { displayNameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
18
20
  import { checkEntitlement, createResourceUid, decodeStoredNodes } from "@aglyn/aglyn/server";
19
21
  import { marketplacePriceRefusal, sanitizeMarketplaceDefinition } from "../model/index.js";
20
22
  import { sanitizeMarketplaceProps } from "../model/marketplace-props.js";
@@ -67,7 +69,7 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
67
69
  error: 'Unauthenticated'
68
70
  });
69
71
  try {
70
- var _hostSnapshot_get, _ref6, _decodeStoredNodes, _existing_docs__get, _orgForHost_org;
72
+ var _ref6, _decodeStoredNodes, _existing_docs__get, _orgForHost_org;
71
73
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
72
74
  // Org-role permission gate (AGL-238).
73
75
  const membership = await resolveOrgPermissions(decoded.uid, {
@@ -85,10 +87,11 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
85
87
  error: 'Unknown site'
86
88
  });
87
89
  }
88
- const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
89
- if (memberRole !== 'admin' && memberRole !== 'editor') {
90
+ // "Publish a site’s items to the marketplace" (AGL-4047): the site's stamp, else
91
+ // the preset of the member's site role, the Admin and Editor this asked for.
92
+ if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.sitePublish', MARKETPLACE_SITE_ROLE_PRESET)) {
90
93
  return res.status(403).json({
91
- error: 'Not a site admin or editor'
94
+ error: marketplaceSitePermissionRefusal('marketplace.sitePublish')
92
95
  });
93
96
  }
94
97
  // Plan gate rides the owning org's doc (AGL-238).
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport {\n marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model'\nimport { sanitizeMarketplaceProps } from '../model/marketplace-props'\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\n/**\n * Publishes a host reusable component to the marketplace (AGL-44). Runs\n * server-side so the sanitization pass (component-id allowlist, key strip,\n * size cap — see `sanitizeMarketplaceDefinition`) cannot be bypassed: clients\n * have no write access to `marketplaceListings` creates. Requirements: host\n * admin, a marketplace profile, and the `marketplaceSelling` plan flag.\n * Re-publishing the same component bumps the listing's version.\n */\nexport const publishHandler: 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 componentId = String(req.body?.componentId ?? '')\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 || !componentId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing hostId, componentId, 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 // Org-role permission gate (AGL-238).\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 hostSnapshot = await firestore.collection('hosts').doc(hostId).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, '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): the publisher is the org, not\n // whoever clicked publish. `profileId` keeps its name — it is still the\n // publisher profile's doc id, which is now the org id.\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: 'components',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const definitionSnapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('components')\n .doc(componentId)\n .get()\n // Decoded, like the screen and layout publish paths (AGL-1395). This is\n // the component DOCUMENT rather than its versions subcollection, and it\n // is now compressed like everything else (AGL-1151) — a plain map from\n // before that comes back unchanged.\n //\n // `sanitizeMarketplaceDefinition` refuses an undecoded tree by name, so\n // reading raw here is the difference between publishing a component and\n // being told the design is stored compressed on every attempt.\n const definition = definitionSnapshot.data() as any\n if (!definition || definition.deletedAt) {\n return res.status(404).json({ error: 'Unknown component' })\n }\n // The properties the tree binds to (AGL-2933), read off the same document\n // the tree is: a publish copies a version's props onto it with its nodes.\n // A default the sanitizer refuses is cleared and its property kept — see\n // `sanitizeMarketplaceProps`.\n const props = sanitizeMarketplaceProps(definition.props)\n if (props.ok === false) {\n return res.status(422).json({ error: props.error })\n }\n // Held to the rule with those properties beside it, so a link or image\n // bound whole to a Link or Image property travels bound.\n const sanitized = sanitizeMarketplaceDefinition(\n {\n rootId: definition.rootId,\n nodes: decodeStoredNodes<Record<string, any>>(definition.nodes) ?? {},\n },\n { declaredProps: props.props },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // One listing per source component: re-publish bumps latestVersion.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceComponentId', '==', componentId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n // The phishing screen, before anything is listed (AGL-3365).\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n pageNodes: sanitized.nodes,\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'component',\n sourceComponentId: componentId,\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 // Version metadata for the detail page (AGL-95) — snapshots\n // themselves stay server-only, so history rides the listing doc.\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 rootId: sanitized.rootId,\n nodes: sanitized.nodes,\n // Always a list, even an empty one: install and update read a version\n // with none as one published before properties were carried.\n props: props.props,\n publishedAt: now,\n })\n\n return res\n .status(200)\n .json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n"],"names":["displayNameSearchFields","checkEntitlement","createResourceUid","decodeStoredNodes","marketplacePriceRefusal","sanitizeMarketplaceDefinition","sanitizeMarketplaceProps","firebaseAdmin","getOrgForHost","resolveOrgPermissions","resolvePublisherProfile","publishPreconditionRefusal","listingSubmissionRefusal","refreshListingQueryFields","publishHandler","req","res","method","status","json","error","hostId","String","body","componentId","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","collection","doc","get","exists","memberRole","org","orgId","publisher","refusal","sells","definitionSnapshot","definition","data","deletedAt","props","ok","sanitized","rootId","nodes","declaredProps","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","pageNodes","official","set","profileId","artifactType","sourceComponentId","latestVersion","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.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 { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport {\n marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model'\nimport { sanitizeMarketplaceProps } from '../model/marketplace-props'\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\n/**\n * Publishes a host reusable component to the marketplace (AGL-44). Runs\n * server-side so the sanitization pass (component-id allowlist, key strip,\n * size cap — see `sanitizeMarketplaceDefinition`) cannot be bypassed: clients\n * have no write access to `marketplaceListings` creates. Requirements: host\n * admin, a marketplace profile, and the `marketplaceSelling` plan flag.\n * Re-publishing the same component bumps the listing's version.\n */\nexport const publishHandler: 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 componentId = String(req.body?.componentId ?? '')\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 || !componentId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing hostId, componentId, 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 // Org-role permission gate (AGL-238).\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 hostSnapshot = await firestore.collection('hosts').doc(hostId).get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Publish a site’s items to the marketplace\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.sitePublish', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.sitePublish') })\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, '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): the publisher is the org, not\n // whoever clicked publish. `profileId` keeps its name — it is still the\n // publisher profile's doc id, which is now the org id.\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: 'components',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const definitionSnapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('components')\n .doc(componentId)\n .get()\n // Decoded, like the screen and layout publish paths (AGL-1395). This is\n // the component DOCUMENT rather than its versions subcollection, and it\n // is now compressed like everything else (AGL-1151) — a plain map from\n // before that comes back unchanged.\n //\n // `sanitizeMarketplaceDefinition` refuses an undecoded tree by name, so\n // reading raw here is the difference between publishing a component and\n // being told the design is stored compressed on every attempt.\n const definition = definitionSnapshot.data() as any\n if (!definition || definition.deletedAt) {\n return res.status(404).json({ error: 'Unknown component' })\n }\n // The properties the tree binds to (AGL-2933), read off the same document\n // the tree is: a publish copies a version's props onto it with its nodes.\n // A default the sanitizer refuses is cleared and its property kept — see\n // `sanitizeMarketplaceProps`.\n const props = sanitizeMarketplaceProps(definition.props)\n if (props.ok === false) {\n return res.status(422).json({ error: props.error })\n }\n // Held to the rule with those properties beside it, so a link or image\n // bound whole to a Link or Image property travels bound.\n const sanitized = sanitizeMarketplaceDefinition(\n {\n rootId: definition.rootId,\n nodes: decodeStoredNodes<Record<string, any>>(definition.nodes) ?? {},\n },\n { declaredProps: props.props },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // One listing per source component: re-publish bumps latestVersion.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceComponentId', '==', componentId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n // The phishing screen, before anything is listed (AGL-3365).\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n pageNodes: sanitized.nodes,\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'component',\n sourceComponentId: componentId,\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 // Version metadata for the detail page (AGL-95) — snapshots\n // themselves stay server-only, so history rides the listing doc.\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 rootId: sanitized.rootId,\n nodes: sanitized.nodes,\n // Always a list, even an empty one: install and update read a version\n // with none as one published before properties were carried.\n props: props.props,\n publishedAt: now,\n })\n\n return res\n .status(200)\n .json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","displayNameSearchFields","checkEntitlement","createResourceUid","decodeStoredNodes","marketplacePriceRefusal","sanitizeMarketplaceDefinition","sanitizeMarketplaceProps","firebaseAdmin","getOrgForHost","resolveOrgPermissions","resolvePublisherProfile","publishPreconditionRefusal","listingSubmissionRefusal","refreshListingQueryFields","publishHandler","req","res","method","status","json","error","hostId","String","body","componentId","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","length","undefined","existing","orgForHost","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostSnapshot","collection","doc","get","exists","org","orgId","publisher","refusal","sells","definitionSnapshot","definition","data","deletedAt","props","ok","sanitized","rootId","nodes","declaredProps","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","pageNodes","official","set","profileId","artifactType","sourceComponentId","latestVersion","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","listingId","id","console"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginSiteStarterSource } from '@aglyn/aglyn/plugin-manager/plugin-site-starters';
18
+ /** The marketplace's `core.site-starters` source (AGL-4049). */
19
+ export declare const marketplaceSiteStarterSource: Required<PluginSiteStarterSource>;
@@ -0,0 +1,189 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { matchSiteStarterFacets, SITE_STARTER_ARTIFACT_TYPE, siteStarterFacetsOf, siteStarterRefusal } from "@aglyn/aglyn/app-utils/site-starter";
18
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
+ import { BROWSE_EVERYONE, LISTING_BROWSE_AUDIENCE, LISTING_BROWSE_TOKENS } from "../model/listing-query.js";
20
+ import { isListingBrowsable, isListingDeleted, isPrivateListing, listingArtifactType } from "../model/marketplace.js";
21
+ import { officialStartersFirst, SITE_STARTER_KNOWN_IDS, siteStarterSearchLimit, siteStarterSearchTokens, siteStarterSummaryOf } from "../model/site-starter-listing.js";
22
+ import { canActAsPublisher, PUBLISHER_PROFILES } from "./publisher-profile.js";
23
+ import { requirePurchase } from "./purchase-entitlement.js";
24
+ import { isPublisherSecurityLocked } from "./sale-risk.js";
25
+ import { recordVersionMove } from "./version-stats.js";
26
+ /**
27
+ * Where the console's "Create site" finds its starters (AGL-4049): the
28
+ * marketplace's `siteStarter` listings, behind core's `core.site-starters`
29
+ * seam (`plugin-manager/plugin-site-starters.ts`), registered from
30
+ * `declarations.server.ts`.
31
+ *
32
+ * `resolve` is a content door like the install routes: it hands over a
33
+ * listing's stored version, so it refuses what they refuse — a taken-down
34
+ * listing, a publisher under a security lock, a private listing to anyone
35
+ * but its own workspace, an unpaid priced one (`install-listing-gates.spec.ts`
36
+ * holds the three). A starter still in review is its own workspace's to try,
37
+ * as a plugin is.
38
+ */ const LISTINGS = 'marketplaceListings';
39
+ const firestoreOf = ()=>firebaseAdmin.app().firestore();
40
+ const gone = (error = 'Unknown starter')=>({
41
+ error,
42
+ status: 404
43
+ });
44
+ /**
45
+ * Each publisher's name as a search card shows it, read once per publisher
46
+ * in one batch: the profile's display name, else its handle.
47
+ */ async function authorNames(firestore, profileIds) {
48
+ const ids = [
49
+ ...new Set(profileIds.filter((id)=>typeof id === 'string' && id))
50
+ ];
51
+ const names = new Map();
52
+ if (!ids.length) return names;
53
+ const refs = ids.map((id)=>firestore.collection(PUBLISHER_PROFILES).doc(id));
54
+ const snapshots = await firestore.getAll(...refs);
55
+ snapshots.forEach((snapshot, index)=>{
56
+ const displayName = snapshot.get('displayName');
57
+ const handle = snapshot.get('handle');
58
+ const name = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : typeof handle === 'string' ? handle : '';
59
+ names.set(ids[index], name);
60
+ });
61
+ return names;
62
+ }
63
+ /**
64
+ * What anyone may see: the search asks the public audience only, so who is
65
+ * asking changes nothing about the answer and the viewer is not read.
66
+ */ async function search(input) {
67
+ var _ref;
68
+ const limit = siteStarterSearchLimit(input == null ? void 0 : input.limit);
69
+ if (!limit) return [];
70
+ const filter = (_ref = input == null ? void 0 : input.filter) != null ? _ref : {};
71
+ const firestore = firestoreOf();
72
+ let query = firestore.collection(LISTINGS).where('artifactType', '==', SITE_STARTER_ARTIFACT_TYPE);
73
+ if (filter.mode === 'base' || filter.mode === 'ai') {
74
+ query = query.where('starterMode', '==', filter.mode);
75
+ }
76
+ if (typeof filter.kind === 'string' && filter.kind) {
77
+ query = query.where('starterKind', '==', filter.kind);
78
+ }
79
+ if (typeof filter.base === 'string' && filter.base) {
80
+ query = query.where('starterBase', '==', filter.base);
81
+ }
82
+ // One array clause answers who may see it, and the search folds into it:
83
+ // `browseTokens` holds only `scope~word` pairs, never the bare scope.
84
+ const tokens = siteStarterSearchTokens(input == null ? void 0 : input.word);
85
+ query = tokens.length ? query.where(LISTING_BROWSE_TOKENS, 'array-contains-any', tokens) : query.where(LISTING_BROWSE_AUDIENCE, 'array-contains', BROWSE_EVERYONE);
86
+ // Twice the page, so what the facets the query cannot ask (fonts, pages)
87
+ // filter out in memory still leaves a full one, most of the time.
88
+ const snapshot = await query.orderBy('createdAt', 'desc').limit(limit * 2).get();
89
+ const rows = snapshot.docs.map((doc)=>{
90
+ var _doc_data;
91
+ return {
92
+ id: doc.id,
93
+ listing: (_doc_data = doc.data()) != null ? _doc_data : {}
94
+ };
95
+ })// Defense in depth: the audience clause already admits only these.
96
+ .filter(({ listing })=>!isListingDeleted(listing) && isListingBrowsable(listing)).filter(({ listing })=>matchSiteStarterFacets(siteStarterFacetsOf(listing), filter));
97
+ const ordered = officialStartersFirst(rows.map((row)=>_extends({}, row, {
98
+ official: row.listing['official'] === true
99
+ }))).slice(0, limit);
100
+ const names = await authorNames(firestore, ordered.map(({ listing })=>{
101
+ var _listing_profileId;
102
+ return String((_listing_profileId = listing['profileId']) != null ? _listing_profileId : '');
103
+ }));
104
+ return ordered.map(({ id, listing })=>{
105
+ var _names_get, _listing_profileId;
106
+ return siteStarterSummaryOf(id, listing, (_names_get = names.get(String((_listing_profileId = listing['profileId']) != null ? _listing_profileId : ''))) != null ? _names_get : '');
107
+ });
108
+ }
109
+ async function resolve(choice, viewer) {
110
+ var _listing_profileId, _ref, _ref1, _listing_priceUsd, _ref2, _ref3, _choice_version;
111
+ const listingId = typeof (choice == null ? void 0 : choice.listingId) === 'string' ? choice.listingId : '';
112
+ if (!listingId) return gone();
113
+ const firestore = firestoreOf();
114
+ const listingRef = firestore.collection(LISTINGS).doc(listingId);
115
+ const listing = (await listingRef.get()).data();
116
+ if (!listing || listing.deletedAt || // A staff takedown hands nothing over, to anyone (AGL-2290).
117
+ listing.hiddenAt || // Nor does a publisher under a security lock (AGL-3365).
118
+ await isPublisherSecurityLocked(firestore, listing.profileId) || listingArtifactType(listing) !== SITE_STARTER_ARTIFACT_TYPE) {
119
+ return gone();
120
+ }
121
+ // The publisher's own workspace picks its own starter free, and only into
122
+ // itself: the waiver is about the publisher's OWN sites (AGL-2484), so a
123
+ // person who administers the publisher and creates a site elsewhere is a
124
+ // buyer like anyone else there.
125
+ const publisherOrgId = String((_listing_profileId = listing.profileId) != null ? _listing_profileId : '');
126
+ const viewerOrgId = String((_ref = viewer == null ? void 0 : viewer.orgId) != null ? _ref : '');
127
+ const ownsListing = viewerOrgId ? publisherOrgId !== '' && viewerOrgId === publisherOrgId && await canActAsPublisher(firestore, String((_ref1 = viewer == null ? void 0 : viewer.uid) != null ? _ref1 : ''), listing.profileId) : false;
128
+ // Private means its own workspace only (AGL-968).
129
+ if (isPrivateListing(listing) && !ownsListing) return gone();
130
+ // A starter still in review, or rejected, is its publisher's to try.
131
+ if (!isListingBrowsable(listing) && !ownsListing) return gone();
132
+ const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
133
+ const unpaid = await requirePurchase({
134
+ firestore,
135
+ buyerUid: String((_ref2 = viewer == null ? void 0 : viewer.uid) != null ? _ref2 : ''),
136
+ buyerOrgId: viewerOrgId,
137
+ listingId,
138
+ priceUsd,
139
+ ownsListing
140
+ });
141
+ if (unpaid) return {
142
+ error: unpaid.error,
143
+ status: 402,
144
+ priceUsd: unpaid.priceUsd
145
+ };
146
+ const version = String((_ref3 = (_choice_version = choice.version) != null ? _choice_version : listing.latestVersion) != null ? _ref3 : '');
147
+ if (!version) return gone();
148
+ const versionSnapshot = await listingRef.collection('versions').doc(version).get();
149
+ const payload = versionSnapshot.get('starter');
150
+ if (!payload || typeof payload !== 'object') return gone();
151
+ // A starter that cannot start a site is gone, as far as a buyer is concerned.
152
+ const refusal = siteStarterRefusal(payload, SITE_STARTER_KNOWN_IDS);
153
+ if (refusal) return gone(refusal);
154
+ return payload;
155
+ }
156
+ async function installed(choice, _viewer, site) {
157
+ try {
158
+ var _choice_version;
159
+ const listingId = typeof (choice == null ? void 0 : choice.listingId) === 'string' ? choice.listingId : '';
160
+ if (!listingId) return;
161
+ const firestore = firestoreOf();
162
+ const listingRef = firestore.collection(LISTINGS).doc(listingId);
163
+ const latest = (_choice_version = choice.version) != null ? _choice_version : (await listingRef.get()).get('latestVersion');
164
+ await recordVersionMove({
165
+ firestore,
166
+ listingRef,
167
+ artifactType: SITE_STARTER_ARTIFACT_TYPE,
168
+ from: null,
169
+ to: latest != null ? latest : null
170
+ });
171
+ await listingRef.update({
172
+ installCount: firebaseAdmin.firestore.FieldValue.increment(1)
173
+ });
174
+ } catch (error) {
175
+ // Counting never fails the site it counts.
176
+ console.error('site starter install count failed', {
177
+ listingId: choice == null ? void 0 : choice.listingId,
178
+ hostId: site == null ? void 0 : site.hostId,
179
+ error
180
+ });
181
+ }
182
+ }
183
+ /** The marketplace's `core.site-starters` source (AGL-4049). */ export const marketplaceSiteStarterSource = {
184
+ search,
185
+ resolve,
186
+ installed
187
+ };
188
+
189
+ //# sourceMappingURL=site-starter-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/site-starter-source.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n matchSiteStarterFacets,\n SITE_STARTER_ARTIFACT_TYPE,\n siteStarterFacetsOf,\n siteStarterRefusal,\n type SiteStarterChoice,\n type SiteStarterFacets,\n type SiteStarterPayload,\n} from '@aglyn/aglyn/app-utils/site-starter'\nimport type {\n PluginSiteStarterSource,\n SiteStarterSummary,\n SiteStarterViewer,\n} from '@aglyn/aglyn/plugin-manager/plugin-site-starters'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n BROWSE_EVERYONE,\n LISTING_BROWSE_AUDIENCE,\n LISTING_BROWSE_TOKENS,\n} from '../model/listing-query'\nimport {\n isListingBrowsable,\n isListingDeleted,\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport {\n officialStartersFirst,\n SITE_STARTER_KNOWN_IDS,\n siteStarterSearchLimit,\n siteStarterSearchTokens,\n siteStarterSummaryOf,\n} from '../model/site-starter-listing'\nimport { canActAsPublisher, PUBLISHER_PROFILES } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { isPublisherSecurityLocked } from './sale-risk'\nimport { recordVersionMove } from './version-stats'\n\n/**\n * Where the console's \"Create site\" finds its starters (AGL-4049): the\n * marketplace's `siteStarter` listings, behind core's `core.site-starters`\n * seam (`plugin-manager/plugin-site-starters.ts`), registered from\n * `declarations.server.ts`.\n *\n * `resolve` is a content door like the install routes: it hands over a\n * listing's stored version, so it refuses what they refuse — a taken-down\n * listing, a publisher under a security lock, a private listing to anyone\n * but its own workspace, an unpaid priced one (`install-listing-gates.spec.ts`\n * holds the three). A starter still in review is its own workspace's to try,\n * as a plugin is.\n */\n\nconst LISTINGS = 'marketplaceListings'\n\nconst firestoreOf = () => firebaseAdmin.app().firestore()\n\ntype ListingDoc = Record<string, any>\n\n/** The answer a refused pick gives, in the seam's shape. */\ntype Refused = { error: string; status: 402 | 403 | 404; priceUsd?: number }\n\nconst gone = (error = 'Unknown starter'): Refused => ({ error, status: 404 })\n\n/**\n * Each publisher's name as a search card shows it, read once per publisher\n * in one batch: the profile's display name, else its handle.\n */\nasync function authorNames(\n firestore: FirebaseFirestore.Firestore,\n profileIds: readonly string[],\n): Promise<Map<string, string>> {\n const ids = [...new Set(profileIds.filter((id) => typeof id === 'string' && id))]\n const names = new Map<string, string>()\n if (!ids.length) return names\n const refs = ids.map((id) => firestore.collection(PUBLISHER_PROFILES).doc(id))\n const snapshots = await firestore.getAll(...refs)\n snapshots.forEach((snapshot, index) => {\n const displayName = snapshot.get('displayName')\n const handle = snapshot.get('handle')\n const name =\n typeof displayName === 'string' && displayName.trim()\n ? displayName.trim()\n : typeof handle === 'string'\n ? handle\n : ''\n names.set(ids[index], name)\n })\n return names\n}\n\n/**\n * What anyone may see: the search asks the public audience only, so who is\n * asking changes nothing about the answer and the viewer is not read.\n */\nasync function search(\n input: { word: string; filter: Partial<SiteStarterFacets>; limit: number },\n): Promise<SiteStarterSummary[]> {\n const limit = siteStarterSearchLimit(input?.limit)\n if (!limit) return []\n const filter: Partial<SiteStarterFacets> = input?.filter ?? {}\n const firestore = firestoreOf()\n\n let query: FirebaseFirestore.Query = firestore\n .collection(LISTINGS)\n .where('artifactType', '==', SITE_STARTER_ARTIFACT_TYPE)\n if (filter.mode === 'base' || filter.mode === 'ai') {\n query = query.where('starterMode', '==', filter.mode)\n }\n if (typeof filter.kind === 'string' && filter.kind) {\n query = query.where('starterKind', '==', filter.kind)\n }\n if (typeof filter.base === 'string' && filter.base) {\n query = query.where('starterBase', '==', filter.base)\n }\n // One array clause answers who may see it, and the search folds into it:\n // `browseTokens` holds only `scope~word` pairs, never the bare scope.\n const tokens = siteStarterSearchTokens(input?.word)\n query = tokens.length\n ? query.where(LISTING_BROWSE_TOKENS, 'array-contains-any', tokens)\n : query.where(LISTING_BROWSE_AUDIENCE, 'array-contains', BROWSE_EVERYONE)\n // Twice the page, so what the facets the query cannot ask (fonts, pages)\n // filter out in memory still leaves a full one, most of the time.\n const snapshot = await query.orderBy('createdAt', 'desc').limit(limit * 2).get()\n\n const rows = snapshot.docs\n .map((doc) => ({ id: doc.id, listing: (doc.data() ?? {}) as ListingDoc }))\n // Defense in depth: the audience clause already admits only these.\n .filter(({ listing }) => !isListingDeleted(listing) && isListingBrowsable(listing))\n .filter(({ listing }) => matchSiteStarterFacets(siteStarterFacetsOf(listing), filter))\n const ordered = officialStartersFirst(\n rows.map((row) => ({ ...row, official: row.listing['official'] === true })),\n ).slice(0, limit)\n const names = await authorNames(\n firestore,\n ordered.map(({ listing }) => String(listing['profileId'] ?? '')),\n )\n return ordered.map(({ id, listing }) =>\n siteStarterSummaryOf(id, listing, names.get(String(listing['profileId'] ?? '')) ?? ''),\n )\n}\n\nasync function resolve(\n choice: SiteStarterChoice,\n viewer: SiteStarterViewer,\n): Promise<SiteStarterPayload | Refused> {\n const listingId = typeof choice?.listingId === 'string' ? choice.listingId : ''\n if (!listingId) return gone()\n const firestore = firestoreOf()\n const listingRef = firestore.collection(LISTINGS).doc(listingId)\n const listing = (await listingRef.get()).data() as ListingDoc | undefined\n if (\n !listing ||\n listing.deletedAt ||\n // A staff takedown hands nothing over, to anyone (AGL-2290).\n listing.hiddenAt ||\n // Nor does a publisher under a security lock (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== SITE_STARTER_ARTIFACT_TYPE\n ) {\n return gone()\n }\n\n // The publisher's own workspace picks its own starter free, and only into\n // itself: the waiver is about the publisher's OWN sites (AGL-2484), so a\n // person who administers the publisher and creates a site elsewhere is a\n // buyer like anyone else there.\n const publisherOrgId = String(listing.profileId ?? '')\n const viewerOrgId = String(viewer?.orgId ?? '')\n const ownsListing = viewerOrgId\n ? publisherOrgId !== '' &&\n viewerOrgId === publisherOrgId &&\n (await canActAsPublisher(firestore, String(viewer?.uid ?? ''), listing.profileId))\n : false\n // Private means its own workspace only (AGL-968).\n if (isPrivateListing(listing) && !ownsListing) return gone()\n // A starter still in review, or rejected, is its publisher's to try.\n if (!isListingBrowsable(listing) && !ownsListing) return gone()\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: String(viewer?.uid ?? ''),\n buyerOrgId: viewerOrgId,\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return { error: unpaid.error, status: 402, priceUsd: unpaid.priceUsd }\n\n const version = String(choice.version ?? listing.latestVersion ?? '')\n if (!version) return gone()\n const versionSnapshot = await listingRef.collection('versions').doc(version).get()\n const payload = versionSnapshot.get('starter') as SiteStarterPayload | undefined\n if (!payload || typeof payload !== 'object') return gone()\n // A starter that cannot start a site is gone, as far as a buyer is concerned.\n const refusal = siteStarterRefusal(payload, SITE_STARTER_KNOWN_IDS)\n if (refusal) return gone(refusal)\n return payload\n}\n\nasync function installed(\n choice: SiteStarterChoice,\n _viewer: SiteStarterViewer,\n site: { hostId: string },\n): Promise<void> {\n try {\n const listingId = typeof choice?.listingId === 'string' ? choice.listingId : ''\n if (!listingId) return\n const firestore = firestoreOf()\n const listingRef = firestore.collection(LISTINGS).doc(listingId)\n const latest = choice.version ?? (await listingRef.get()).get('latestVersion')\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: SITE_STARTER_ARTIFACT_TYPE,\n from: null,\n to: latest ?? null,\n })\n await listingRef.update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n } catch (error) {\n // Counting never fails the site it counts.\n console.error('site starter install count failed', {\n listingId: choice?.listingId,\n hostId: site?.hostId,\n error,\n })\n }\n}\n\n/** The marketplace's `core.site-starters` source (AGL-4049). */\nexport const marketplaceSiteStarterSource: Required<PluginSiteStarterSource> = {\n search,\n resolve,\n installed,\n}\n"],"names":["matchSiteStarterFacets","SITE_STARTER_ARTIFACT_TYPE","siteStarterFacetsOf","siteStarterRefusal","firebaseAdmin","BROWSE_EVERYONE","LISTING_BROWSE_AUDIENCE","LISTING_BROWSE_TOKENS","isListingBrowsable","isListingDeleted","isPrivateListing","listingArtifactType","officialStartersFirst","SITE_STARTER_KNOWN_IDS","siteStarterSearchLimit","siteStarterSearchTokens","siteStarterSummaryOf","canActAsPublisher","PUBLISHER_PROFILES","requirePurchase","isPublisherSecurityLocked","recordVersionMove","LISTINGS","firestoreOf","app","firestore","gone","error","status","authorNames","profileIds","ids","Set","filter","id","names","Map","length","refs","map","collection","doc","snapshots","getAll","forEach","snapshot","index","displayName","get","handle","name","trim","set","search","input","limit","query","where","mode","kind","base","tokens","word","orderBy","rows","docs","listing","data","ordered","row","official","slice","String","resolve","choice","viewer","listingId","listingRef","deletedAt","hiddenAt","profileId","publisherOrgId","viewerOrgId","orgId","ownsListing","uid","priceUsd","Number","unpaid","buyerUid","buyerOrgId","version","latestVersion","versionSnapshot","payload","refusal","installed","_viewer","site","latest","artifactType","from","to","update","installCount","FieldValue","increment","console","hostId","marketplaceSiteStarterSource"],"mappings":"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"}