@aglyn/plugins-marketplace 1.0.0 → 1.1.0

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 (48) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/listing-detail-editor.component.js +26 -26
  3. package/src/lib/components/listing-detail-editor.component.js.map +1 -1
  4. package/src/lib/components/plugin-review-detail.component.js +17 -3
  5. package/src/lib/components/plugin-review-detail.component.js.map +1 -1
  6. package/src/lib/components/plugin-reviews-queue.component.js +88 -12
  7. package/src/lib/components/plugin-reviews-queue.component.js.map +1 -1
  8. package/src/lib/components/site-starter-review-detail.component.d.ts +115 -0
  9. package/src/lib/components/site-starter-review-detail.component.js +785 -0
  10. package/src/lib/components/site-starter-review-detail.component.js.map +1 -0
  11. package/src/lib/components/starter-picker-shelf.component.d.ts +7 -0
  12. package/src/lib/components/starter-picker-shelf.component.js +75 -1
  13. package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
  14. package/src/lib/hooks/use-starter-answers.d.ts +41 -0
  15. package/src/lib/hooks/use-starter-answers.js +75 -0
  16. package/src/lib/hooks/use-starter-answers.js.map +1 -0
  17. package/src/lib/model/listing-query.d.ts +20 -2
  18. package/src/lib/model/listing-query.js +55 -0
  19. package/src/lib/model/listing-query.js.map +1 -1
  20. package/src/lib/model/plugin-rejection-categories.d.ts +17 -2
  21. package/src/lib/model/plugin-rejection-categories.js +48 -4
  22. package/src/lib/model/plugin-rejection-categories.js.map +1 -1
  23. package/src/lib/model/plugin-review-checklist.d.ts +38 -8
  24. package/src/lib/model/plugin-review-checklist.js +60 -11
  25. package/src/lib/model/plugin-review-checklist.js.map +1 -1
  26. package/src/lib/model/plugin-review-status.d.ts +8 -0
  27. package/src/lib/model/plugin-review-status.js +34 -0
  28. package/src/lib/model/plugin-review-status.js.map +1 -1
  29. package/src/lib/model/starter-category.d.ts +58 -0
  30. package/src/lib/model/starter-category.js +79 -0
  31. package/src/lib/model/starter-category.js.map +1 -0
  32. package/src/lib/model/starter-query.d.ts +27 -0
  33. package/src/lib/model/starter-query.js +42 -0
  34. package/src/lib/model/starter-query.js.map +1 -1
  35. package/src/lib/server/admin-reviews.js +294 -45
  36. package/src/lib/server/admin-reviews.js.map +1 -1
  37. package/src/lib/server/publish-plugin.js +46 -4
  38. package/src/lib/server/publish-plugin.js.map +1 -1
  39. package/src/lib/server/publish-starter.js +4 -0
  40. package/src/lib/server/publish-starter.js.map +1 -1
  41. package/src/lib/server/publisher-review-email.d.ts +5 -0
  42. package/src/lib/server/publisher-review-email.js +2 -1
  43. package/src/lib/server/publisher-review-email.js.map +1 -1
  44. package/src/lib/server/starters.d.ts +4 -1
  45. package/src/lib/server/starters.js +128 -28
  46. package/src/lib/server/starters.js.map +1 -1
  47. package/src/lib/server.js +12 -1
  48. package/src/lib/server.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-starter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DEFAULT_SITE_THEME } from '@aglyn/aglyn/app-utils/default-site'\nimport { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n SITE_STARTER_ARTIFACT_TYPE,\n SITE_STARTER_MAX_SCREENS,\n siteStarterFacetFields,\n siteStarterRefusal,\n type SiteStarterFacets,\n type SiteStarterMode,\n type SiteStarterPayload,\n type SiteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter'\nimport { starterLinksBySlug } from '@aglyn/aglyn/app-utils/site-starter-links'\nimport {\n SITE_STARTER_BRAND_KEYS,\n siteStarterPresetId,\n siteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter-theme'\nimport {\n sanitizeSiteStarterNodes,\n SITE_STARTER_LAYOUT_SLOT_ID,\n} from '@aglyn/aglyn/app-utils/site-starter-nodes'\nimport {\n contrastRatio,\n hostThemeSource,\n parseColor,\n resolveSiteTheme,\n themeArtifactContent,\n validateThemeForPublish,\n type ThemeHostDocument,\n type ThemeValidation,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport { listServerThemePresets } from '@aglyn/aglyn/plugin-manager/plugin-theme-presets'\nimport {\n CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal } from '../model/marketplace'\nimport { sanitizeMarketplaceProps } from '../model/marketplace-props'\nimport { SITE_STARTER_KNOWN_IDS } from '../model/site-starter-listing'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { recordPublisherAgreementAcceptance } from './publisher-agreement-acceptance'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { DecodedIdToken } from 'firebase-admin/auth'\n\n/**\n * The least a brand's primary may contrast with its page, the bar the AI\n * site start holds its own looks to: the color links and text buttons are\n * drawn in has to be readable where they sit.\n */\nconst BRAND_ON_PAGE_MIN_CONTRAST = 3\n\n/** The most an AI starter's answers may hold, as JSON. */\nconst MAX_ANSWERS_BYTES = 16 * 1024\n\n/**\n * The most a starter's stored version may hold, as JSON: every page and the\n * layout ride one version document, and Firestore refuses a document over\n * 1 MiB, so a site past this is refused here, in words, rather than failing\n * the write.\n */\nconst MAX_STARTER_BYTES = 900 * 1024\n\n/** A facet as the body sends it: a trimmed id, or null for none. */\nconst facetId = (value: unknown): string | null =>\n typeof value === 'string' && value.trim() ? value.trim().slice(0, 60) : null\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\ntype BuiltinTheme = Extract<SiteStarterTheme, { kind: 'builtin' }>\n\n/**\n * The body's built-in theme, or why it cannot be one. The brand names the\n * site's accents only, each a color the theme can use: status colors stay the\n * base theme's, since they mean the same thing on every site.\n */\nfunction builtinTheme(raw: Record<string, unknown>): BuiltinTheme | string {\n const base = facetId(raw['base'])\n if (!base) return 'Choose the built-in theme the starter uses'\n const fonts = raw['fonts'] == null ? null : facetId(raw['fonts'])\n if (raw['fonts'] != null && !fonts) return 'The font pairing must be a pairing id, or none'\n const brandIn = isRecord(raw['brand']) ? raw['brand'] : {}\n const brand: BuiltinTheme['brand'] = { light: {} }\n for (const scheme of ['light', 'dark'] as const) {\n const colors = brandIn[scheme]\n if (colors === undefined || (scheme === 'dark' && colors === null)) continue\n if (!isRecord(colors)) return `The ${scheme} brand colors must be a set of colors`\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(colors)) {\n if (!(SITE_STARTER_BRAND_KEYS as readonly string[]).includes(key)) {\n return `A brand color can be ${SITE_STARTER_BRAND_KEYS.join(', ')}; \"${key}\" is not one`\n }\n if (typeof value !== 'string' || !parseColor(value)) {\n return `The ${scheme} ${key} color \"${String(value)}\" is not a color`\n }\n out[key] = value.trim()\n }\n brand[scheme] = out\n }\n return { kind: 'builtin', base, brand, fonts }\n}\n\n/**\n * A built-in starter theme checked as the sites created from it will wear it\n * (AGL-4049): the base this deployment has registered for it — the theme\n * every new site is born with for `starter`, or a base it has none for, as\n * the create path falls back — with the brand and fonts over it, through\n * `siteStarterTheme`, the same composition the create path writes. Then the\n * publish check every theme passes, and the brand's own: a primary the\n * starter names must read on its page in both schemes.\n */\nfunction builtinThemeValidation(builtin: BuiltinTheme): ThemeValidation {\n const preset =\n builtin.base === 'starter'\n ? null\n : (listServerThemePresets().find((entry) => entry?.id === siteStarterPresetId(builtin.base)) ?? null)\n const base = preset ? themeArtifactContent(preset.theme) : DEFAULT_SITE_THEME\n const composed = themeArtifactContent(siteStarterTheme(builtin, base))\n const validation = validateThemeForPublish(composed)\n const errors = [...validation.errors]\n for (const scheme of ['light', 'dark'] as const) {\n const inherited = scheme === 'dark' && !builtin.brand.dark\n const named = (inherited ? builtin.brand.light : builtin.brand[scheme])?.['primary']\n if (!named) continue\n const colors = composed.colorSchemes?.[scheme]\n const page = colors?.background?.default\n const ratio = contrastRatio(colors?.primary?.main, page)\n if (ratio === null || ratio >= BRAND_ON_PAGE_MIN_CONTRAST) continue\n // A dark page needs a lighter color, a light one a darker color.\n const pageIsDark = (contrastRatio(page, '#ffffff') ?? 0) > (contrastRatio(page, '#000000') ?? 0)\n errors.push({\n path: `colorSchemes.${scheme}.primary.main`,\n message:\n `The ${scheme} primary color ${named} is ${ratio.toFixed(1)}:1 against ` +\n `the ${scheme} page background, below ${BRAND_ON_PAGE_MIN_CONTRAST}:1, so links and ` +\n `buttons drawn in it are hard to read. Choose a ${pageIsDark ? 'lighter' : 'darker'} ` +\n `primary${inherited ? ', or give the dark scheme its own brand colors' : ''}.`,\n })\n }\n return { ok: !errors.length, errors, warnings: validation.warnings }\n}\n\n/**\n * Publishes a site as a site starter (AGL-4049): a whole site a person picks\n * in \"Create site\" before it exists. Captures every published page with its\n * published version, the shared layout the Home page sits in — both through\n * `sanitizeSiteStarterNodes`, the starter's own rule, which keeps what a real\n * site is made of (its grids, cards, styles and safe interactions) where the\n * portable rule the other doors use would refuse it — and the theme\n * in one of its two shapes — a built-in theme with brand colors and fonts\n * over it, named in the body, or the site's own full theme — into a\n * `siteStarter` listing that waits for review before anyone else sees it.\n *\n * Gates mirror the template door exactly: the price, org permission, site\n * role, `marketplaceSelling`, a publisher profile with payouts and the current\n * agreement, then the phishing screen before anything is listed. One listing\n * per source site, keyed on `sourceStarterHostId` (never `sourceHostId`, which\n * the template, layout and theme doors share); re-publishing bumps the\n * version and keeps the review verdict, as a plugin's does.\n */\nexport const publishStarterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body = isRecord(req.body) ? req.body : {}\n const hostId = String(body['hostId'] ?? '')\n const displayName = String(body['displayName'] ?? '').slice(0, 80)\n const description = String(body['description'] ?? '').slice(0, 500)\n const category = String(body['category'] ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(body['priceUsd'] ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343). Also enforced\n // in `publishPreconditionRefusal` below.\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 const themeIn = isRecord(body['theme']) ? body['theme'] : {}\n if (themeIn['kind'] !== 'builtin' && themeIn['kind'] !== 'full') {\n return res.status(400).json({\n error: \"Choose the starter's theme: a built-in theme, or this site's own\",\n })\n }\n const mode: SiteStarterMode | null =\n body['mode'] === undefined || body['mode'] === 'base'\n ? 'base'\n : body['mode'] === 'ai'\n ? 'ai'\n : null\n if (!mode) {\n return res.status(400).json({ error: 'A starter is a base starter or an AI starter' })\n }\n const answers = body['answers']\n if (answers !== undefined && answers !== null) {\n if (!isRecord(answers)) {\n return res.status(400).json({ error: \"An AI starter's answers must be a set of answers\" })\n }\n if (JSON.stringify(answers).length > MAX_ANSWERS_BYTES) {\n return res.status(400).json({ error: \"An AI starter's answers are too long\" })\n }\n }\n const facetsIn = isRecord(body['facets']) ? body['facets'] : {}\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 let decoded: DecodedIdToken\n try {\n decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n } catch (error) {\n // Only a refused credential is the caller's 401; failing to check it is\n // ours and keeps the dispatcher's 5xx (AGL-2852).\n if (!isRefusedIdToken(error)) throw error\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n try {\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 // Publishing the site as a starter publishes its content: \"Publish site\n // content\" (`site.publish`, AGL-4047), whose preset is the site's admin\n // and editor, read as the rules read it.\n if (!holdsSiteKey(hostSnapshot, decoded.uid, 'site.publish')) {\n return res.status(403).json({ error: siteKeyRefusal('site.publish') })\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 (AGL-2282).\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'site starters',\n agreement: req.body?.agreement,\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n // Capture: every page in the routing map, with its published version,\n // under the starter rule (`site-starter-nodes.ts`). One past the most a starter\n // may have, so an oversized site is refused by the one validity check\n // below rather than silently cut short.\n const routingMap = (hostSnapshot.get('screens') ?? {}) as Record<string, string>\n const screenIds = Object.keys(routingMap).slice(0, SITE_STARTER_MAX_SCREENS + 1)\n const captured: Array<{ id: string; layoutId: string | null; screen: Record<string, unknown> }> = []\n for (const screenId of screenIds) {\n const screenRef = hostRef.collection('screens').doc(screenId)\n const screen = (await screenRef.get()).data() as any\n if (!screen || screen.deletedAt || !screen.versionId) continue\n const versionSnapshot = await screenRef\n .collection('versions')\n .doc(String(screen.versionId))\n .get()\n // The besigner writes `nodes` COMPRESSED, which the Admin SDK hands back\n // as a Node `Buffer` (AGL-1395): decode before anything reads it.\n const nodes = decodeStoredNodes(versionSnapshot.get('nodes'))\n if (!nodes) continue\n const sanitized = sanitizeSiteStarterNodes({\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes: nodes as Record<string, any>,\n })\n if (sanitized.ok === false) {\n return res.status(422).json({\n error: `Page \"${screen.displayName ?? screenId}\": ${sanitized.error}`,\n })\n }\n captured.push({\n id: screenId,\n layoutId: typeof screen.layoutId === 'string' && screen.layoutId ? screen.layoutId : null,\n screen: {\n displayName: screen.displayName ?? 'Page',\n slug: routingMap[screenId],\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n nodes: sanitized.nodes,\n },\n })\n }\n // A link to another page names it by slug in the starter: its pages have\n // no ids until a site is created from it (`site-starter-links.ts`).\n const routing: Record<string, string> = Object.fromEntries(\n captured.map(({ id }) => [id, routingMap[id]]),\n )\n const screens = captured.map(({ screen }) => ({\n ...screen,\n nodes: starterLinksBySlug(screen['nodes'] as Record<string, any>, routing),\n }))\n\n // The shared layout the Home page sits in: one per starter, since a new\n // site on the Free plan has room for one.\n const home = captured.find(({ id }) => routingMap[id] === '/')\n let layout: Record<string, unknown> | null = null\n if (home?.layoutId) {\n const layoutRef = hostRef.collection('layouts').doc(home.layoutId)\n const layoutSnapshot = await layoutRef.get()\n const versionId = layoutSnapshot.exists ? layoutSnapshot.get('versionId') : null\n const versionSnapshot = versionId\n ? await layoutRef.collection('versions').doc(String(versionId)).get()\n : null\n const nodes = versionSnapshot\n ? decodeStoredNodes<Record<string, any>>(versionSnapshot.get('nodes'))\n : null\n if (nodes && Object.keys(nodes).length) {\n // The layout properties its tree binds to (AGL-2933).\n const props = sanitizeMarketplaceProps(versionSnapshot?.get('props'))\n if (props.ok === false) {\n return res.status(422).json({ error: props.error })\n }\n const rootId: string =\n (nodes[CANVAS_ROOT_ELEMENT_ID] && CANVAS_ROOT_ELEMENT_ID) ||\n Object.keys(nodes).find((id) => !nodes[id]?.parentId) ||\n ''\n const sanitized = sanitizeSiteStarterNodes(\n { rootId, nodes },\n { layout: true, declaredProps: props.props },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: `The site's layout: ${sanitized.error}` })\n }\n // A layout whose slot did not survive renders every page as chrome\n // with the page missing.\n const hasSlot = Object.values(sanitized.nodes).some(\n (node: any) => node.componentId === SITE_STARTER_LAYOUT_SLOT_ID,\n )\n if (!hasSlot) {\n return res.status(422).json({\n error:\n \"The site's layout has no content slot, so pages using it would \" +\n 'have nowhere to render',\n })\n }\n layout = {\n displayName: String(layoutSnapshot.get('displayName') ?? 'Site header and footer'),\n nodes: starterLinksBySlug(sanitized.nodes, routing),\n ...(props.props.length && { props: props.props }),\n }\n }\n }\n\n // The theme, in the shape the publisher chose.\n let theme: SiteStarterTheme\n if (themeIn['kind'] === 'full') {\n // What this site DESIGNED: on an installed theme, the theme as stored\n // (its edits are this site's private changes to someone else's\n // artifact); otherwise the resolved view, edits included (AGL-3404).\n const host = (hostSnapshot.data() ?? {}) as ThemeHostDocument\n const full = themeArtifactContent(\n hostThemeSource(host) === 'installed' ? (host.theme ?? {}) : (resolveSiteTheme(host) ?? {}),\n )\n const validation = validateThemeForPublish(full)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = { kind: 'full', theme: full }\n } else {\n const builtin = builtinTheme(themeIn)\n if (typeof builtin === 'string') return res.status(422).json({ error: builtin })\n // Refused where the publisher can pick another color, not found on\n // every site created from it.\n const validation = builtinThemeValidation(builtin)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = builtin\n }\n\n // A built-in theme is what the starter's base and fonts facets describe;\n // a full theme is the publisher's to describe.\n const facets: SiteStarterFacets = {\n kind: facetId(facetsIn['kind']),\n base: theme.kind === 'builtin' ? theme.base : facetId(facetsIn['base']),\n fonts: theme.kind === 'builtin' ? theme.fonts : facetId(facetsIn['fonts']),\n pages: screens.length,\n mode,\n }\n const payload: SiteStarterPayload = {\n mode,\n facets,\n theme,\n screens,\n layout,\n ...(isRecord(answers) && { answers }),\n }\n // The one validity check, run here, at seed and again at resolve.\n const invalid = siteStarterRefusal(payload, SITE_STARTER_KNOWN_IDS)\n if (invalid) return res.status(422).json({ error: invalid })\n const bytes = JSON.stringify(payload).length\n if (bytes > MAX_STARTER_BYTES) {\n return res.status(422).json({\n error:\n `This site is too large to publish as one starter: its pages come to ` +\n `${Math.ceil(bytes / 1024)} KB, and a starter holds at most ` +\n `${MAX_STARTER_BYTES / 1024} KB. Publish fewer pages, or smaller ones.`,\n })\n }\n\n // One starter listing per source site; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceStarterHostId', '==', 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): every page,\n // the layout every page sits in, and the answers an AI starter writes from.\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n ...(isRecord(answers) && { readme: JSON.stringify(answers) }),\n pageNodes: [...screens, ...(layout ? [layout] : [])],\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n // The agreement this publish was sent with, recorded before anything\n // is listed under it (AGL-4105).\n await recordPublisherAgreementAcceptance({\n firestore,\n request: req,\n orgId: publisher.orgId,\n uid: decoded.uid,\n staff: decoded['staff'] === true,\n action: 'publish-starter',\n targetId: listingRef.id,\n })\n\n // The version before the listing that names it, so a pick made between\n // the two writes never finds a `latestVersion` with nothing stored at it.\n await listingRef.collection('versions').doc(String(version)).set({\n starter: payload,\n publishedAt: now,\n })\n await listingRef.set(\n {\n artifactType: SITE_STARTER_ARTIFACT_TYPE,\n profileId: publisher.orgId,\n sourceStarterHostId: hostId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321).\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 // A starter creates a whole site the moment it is picked, so a new one\n // waits for review (AGL-4049); a re-publish keeps the verdict it has.\n // `official` is never written here: it is the platform's to set.\n ...(existing.empty && { reviewStatus: 'submitted', createdAt: now }),\n ...siteStarterFacetFields(facets),\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\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Starter publish failed' })\n }\n}\n\nexport default publishStarterHandler\n"],"names":["DEFAULT_SITE_THEME","displayNameSearchFields","SITE_STARTER_ARTIFACT_TYPE","SITE_STARTER_MAX_SCREENS","siteStarterFacetFields","siteStarterRefusal","starterLinksBySlug","SITE_STARTER_BRAND_KEYS","siteStarterPresetId","siteStarterTheme","sanitizeSiteStarterNodes","SITE_STARTER_LAYOUT_SLOT_ID","contrastRatio","hostThemeSource","parseColor","resolveSiteTheme","themeArtifactContent","validateThemeForPublish","listServerThemePresets","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","marketplacePriceRefusal","sanitizeMarketplaceProps","SITE_STARTER_KNOWN_IDS","resolvePublisherProfile","publishPreconditionRefusal","recordPublisherAgreementAcceptance","listingSubmissionRefusal","refreshListingQueryFields","holdsSiteKey","siteKeyRefusal","isRefusedIdToken","BRAND_ON_PAGE_MIN_CONTRAST","MAX_ANSWERS_BYTES","MAX_STARTER_BYTES","facetId","value","trim","slice","isRecord","Array","isArray","builtinTheme","raw","base","fonts","brandIn","brand","light","scheme","colors","undefined","out","key","Object","entries","includes","join","String","kind","builtinThemeValidation","builtin","preset","find","entry","id","theme","composed","validation","errors","inherited","dark","named","colorSchemes","page","background","default","ratio","primary","main","pageIsDark","push","path","message","toFixed","ok","length","warnings","publishStarterHandler","req","res","body","method","status","json","error","hostId","displayName","description","category","priceUsd","Math","round","Number","priceRefusal","themeIn","mode","answers","JSON","stringify","facetsIn","authorization","headers","idToken","startsWith","decoded","app","auth","verifyIdToken","hostSnapshot","existing","orgForHost","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","org","orgId","publisher","refusal","sells","agreement","routingMap","screenIds","keys","captured","screenId","screen","screenRef","data","deletedAt","versionId","versionSnapshot","nodes","sanitized","rootId","layoutId","slug","seo","routing","fromEntries","map","screens","home","layout","layoutRef","layoutSnapshot","props","parentId","declaredProps","hasSlot","values","some","node","componentId","host","full","facets","pages","payload","invalid","bytes","ceil","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","readme","pageNodes","official","request","staff","action","targetId","set","starter","publishedAt","artifactType","profileId","sourceStarterHostId","screenCount","latestVersion","reviewStatus","createdAt","updatedAt","versionHistory","arrayUnion","Timestamp","merge","listingId","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-starter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DEFAULT_SITE_THEME } from '@aglyn/aglyn/app-utils/default-site'\nimport { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n SITE_STARTER_ARTIFACT_TYPE,\n SITE_STARTER_MAX_SCREENS,\n siteStarterFacetFields,\n siteStarterRefusal,\n type SiteStarterFacets,\n type SiteStarterMode,\n type SiteStarterPayload,\n type SiteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter'\nimport { starterLinksBySlug } from '@aglyn/aglyn/app-utils/site-starter-links'\nimport {\n SITE_STARTER_BRAND_KEYS,\n siteStarterPresetId,\n siteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter-theme'\nimport {\n sanitizeSiteStarterNodes,\n SITE_STARTER_LAYOUT_SLOT_ID,\n} from '@aglyn/aglyn/app-utils/site-starter-nodes'\nimport {\n contrastRatio,\n hostThemeSource,\n parseColor,\n resolveSiteTheme,\n themeArtifactContent,\n validateThemeForPublish,\n type ThemeHostDocument,\n type ThemeValidation,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport { listServerThemePresets } from '@aglyn/aglyn/plugin-manager/plugin-theme-presets'\nimport {\n CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal } from '../model/marketplace'\nimport { sanitizeMarketplaceProps } from '../model/marketplace-props'\nimport { SITE_STARTER_KNOWN_IDS } from '../model/site-starter-listing'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { recordPublisherAgreementAcceptance } from './publisher-agreement-acceptance'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { DecodedIdToken } from 'firebase-admin/auth'\n\n/**\n * The least a brand's primary may contrast with its page, the bar the AI\n * site start holds its own looks to: the color links and text buttons are\n * drawn in has to be readable where they sit.\n */\nconst BRAND_ON_PAGE_MIN_CONTRAST = 3\n\n/** The most an AI starter's answers may hold, as JSON. */\nconst MAX_ANSWERS_BYTES = 16 * 1024\n\n/**\n * The most a starter's stored version may hold, as JSON: every page and the\n * layout ride one version document, and Firestore refuses a document over\n * 1 MiB, so a site past this is refused here, in words, rather than failing\n * the write.\n */\nconst MAX_STARTER_BYTES = 900 * 1024\n\n/** A facet as the body sends it: a trimmed id, or null for none. */\nconst facetId = (value: unknown): string | null =>\n typeof value === 'string' && value.trim() ? value.trim().slice(0, 60) : null\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\ntype BuiltinTheme = Extract<SiteStarterTheme, { kind: 'builtin' }>\n\n/**\n * The body's built-in theme, or why it cannot be one. The brand names the\n * site's accents only, each a color the theme can use: status colors stay the\n * base theme's, since they mean the same thing on every site.\n */\nfunction builtinTheme(raw: Record<string, unknown>): BuiltinTheme | string {\n const base = facetId(raw['base'])\n if (!base) return 'Choose the built-in theme the starter uses'\n const fonts = raw['fonts'] == null ? null : facetId(raw['fonts'])\n if (raw['fonts'] != null && !fonts) return 'The font pairing must be a pairing id, or none'\n const brandIn = isRecord(raw['brand']) ? raw['brand'] : {}\n const brand: BuiltinTheme['brand'] = { light: {} }\n for (const scheme of ['light', 'dark'] as const) {\n const colors = brandIn[scheme]\n if (colors === undefined || (scheme === 'dark' && colors === null)) continue\n if (!isRecord(colors)) return `The ${scheme} brand colors must be a set of colors`\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(colors)) {\n if (!(SITE_STARTER_BRAND_KEYS as readonly string[]).includes(key)) {\n return `A brand color can be ${SITE_STARTER_BRAND_KEYS.join(', ')}; \"${key}\" is not one`\n }\n if (typeof value !== 'string' || !parseColor(value)) {\n return `The ${scheme} ${key} color \"${String(value)}\" is not a color`\n }\n out[key] = value.trim()\n }\n brand[scheme] = out\n }\n return { kind: 'builtin', base, brand, fonts }\n}\n\n/**\n * A built-in starter theme checked as the sites created from it will wear it\n * (AGL-4049): the base this deployment has registered for it — the theme\n * every new site is born with for `starter`, or a base it has none for, as\n * the create path falls back — with the brand and fonts over it, through\n * `siteStarterTheme`, the same composition the create path writes. Then the\n * publish check every theme passes, and the brand's own: a primary the\n * starter names must read on its page in both schemes.\n */\nfunction builtinThemeValidation(builtin: BuiltinTheme): ThemeValidation {\n const preset =\n builtin.base === 'starter'\n ? null\n : (listServerThemePresets().find((entry) => entry?.id === siteStarterPresetId(builtin.base)) ?? null)\n const base = preset ? themeArtifactContent(preset.theme) : DEFAULT_SITE_THEME\n const composed = themeArtifactContent(siteStarterTheme(builtin, base))\n const validation = validateThemeForPublish(composed)\n const errors = [...validation.errors]\n for (const scheme of ['light', 'dark'] as const) {\n const inherited = scheme === 'dark' && !builtin.brand.dark\n const named = (inherited ? builtin.brand.light : builtin.brand[scheme])?.['primary']\n if (!named) continue\n const colors = composed.colorSchemes?.[scheme]\n const page = colors?.background?.default\n const ratio = contrastRatio(colors?.primary?.main, page)\n if (ratio === null || ratio >= BRAND_ON_PAGE_MIN_CONTRAST) continue\n // A dark page needs a lighter color, a light one a darker color.\n const pageIsDark = (contrastRatio(page, '#ffffff') ?? 0) > (contrastRatio(page, '#000000') ?? 0)\n errors.push({\n path: `colorSchemes.${scheme}.primary.main`,\n message:\n `The ${scheme} primary color ${named} is ${ratio.toFixed(1)}:1 against ` +\n `the ${scheme} page background, below ${BRAND_ON_PAGE_MIN_CONTRAST}:1, so links and ` +\n `buttons drawn in it are hard to read. Choose a ${pageIsDark ? 'lighter' : 'darker'} ` +\n `primary${inherited ? ', or give the dark scheme its own brand colors' : ''}.`,\n })\n }\n return { ok: !errors.length, errors, warnings: validation.warnings }\n}\n\n/**\n * Publishes a site as a site starter (AGL-4049): a whole site a person picks\n * in \"Create site\" before it exists. Captures every published page with its\n * published version, the shared layout the Home page sits in — both through\n * `sanitizeSiteStarterNodes`, the starter's own rule, which keeps what a real\n * site is made of (its grids, cards, styles and safe interactions) where the\n * portable rule the other doors use would refuse it — and the theme\n * in one of its two shapes — a built-in theme with brand colors and fonts\n * over it, named in the body, or the site's own full theme — into a\n * `siteStarter` listing that waits for review before anyone else sees it.\n *\n * Gates mirror the template door exactly: the price, org permission, site\n * role, `marketplaceSelling`, a publisher profile with payouts and the current\n * agreement, then the phishing screen before anything is listed. One listing\n * per source site, keyed on `sourceStarterHostId` (never `sourceHostId`, which\n * the template, layout and theme doors share); re-publishing bumps the\n * version and keeps the review verdict, as a plugin's does.\n */\nexport const publishStarterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body = isRecord(req.body) ? req.body : {}\n const hostId = String(body['hostId'] ?? '')\n const displayName = String(body['displayName'] ?? '').slice(0, 80)\n const description = String(body['description'] ?? '').slice(0, 500)\n const category = String(body['category'] ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(body['priceUsd'] ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343). Also enforced\n // in `publishPreconditionRefusal` below.\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 const themeIn = isRecord(body['theme']) ? body['theme'] : {}\n if (themeIn['kind'] !== 'builtin' && themeIn['kind'] !== 'full') {\n return res.status(400).json({\n error: \"Choose the starter's theme: a built-in theme, or this site's own\",\n })\n }\n const mode: SiteStarterMode | null =\n body['mode'] === undefined || body['mode'] === 'base'\n ? 'base'\n : body['mode'] === 'ai'\n ? 'ai'\n : null\n if (!mode) {\n return res.status(400).json({ error: 'A starter is a base starter or an AI starter' })\n }\n const answers = body['answers']\n if (answers !== undefined && answers !== null) {\n if (!isRecord(answers)) {\n return res.status(400).json({ error: \"An AI starter's answers must be a set of answers\" })\n }\n if (JSON.stringify(answers).length > MAX_ANSWERS_BYTES) {\n return res.status(400).json({ error: \"An AI starter's answers are too long\" })\n }\n }\n const facetsIn = isRecord(body['facets']) ? body['facets'] : {}\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 let decoded: DecodedIdToken\n try {\n decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n } catch (error) {\n // Only a refused credential is the caller's 401; failing to check it is\n // ours and keeps the dispatcher's 5xx (AGL-2852).\n if (!isRefusedIdToken(error)) throw error\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n try {\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 // Publishing the site as a starter publishes its content: \"Publish site\n // content\" (`site.publish`, AGL-4047), whose preset is the site's admin\n // and editor, read as the rules read it.\n if (!holdsSiteKey(hostSnapshot, decoded.uid, 'site.publish')) {\n return res.status(403).json({ error: siteKeyRefusal('site.publish') })\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 (AGL-2282).\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'site starters',\n agreement: req.body?.agreement,\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n // Capture: every page in the routing map, with its published version,\n // under the starter rule (`site-starter-nodes.ts`). One past the most a starter\n // may have, so an oversized site is refused by the one validity check\n // below rather than silently cut short.\n const routingMap = (hostSnapshot.get('screens') ?? {}) as Record<string, string>\n const screenIds = Object.keys(routingMap).slice(0, SITE_STARTER_MAX_SCREENS + 1)\n const captured: Array<{ id: string; layoutId: string | null; screen: Record<string, unknown> }> = []\n for (const screenId of screenIds) {\n const screenRef = hostRef.collection('screens').doc(screenId)\n const screen = (await screenRef.get()).data() as any\n if (!screen || screen.deletedAt || !screen.versionId) continue\n const versionSnapshot = await screenRef\n .collection('versions')\n .doc(String(screen.versionId))\n .get()\n // The besigner writes `nodes` COMPRESSED, which the Admin SDK hands back\n // as a Node `Buffer` (AGL-1395): decode before anything reads it.\n const nodes = decodeStoredNodes(versionSnapshot.get('nodes'))\n if (!nodes) continue\n const sanitized = sanitizeSiteStarterNodes({\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes: nodes as Record<string, any>,\n })\n if (sanitized.ok === false) {\n return res.status(422).json({\n error: `Page \"${screen.displayName ?? screenId}\": ${sanitized.error}`,\n })\n }\n captured.push({\n id: screenId,\n layoutId: typeof screen.layoutId === 'string' && screen.layoutId ? screen.layoutId : null,\n screen: {\n displayName: screen.displayName ?? 'Page',\n slug: routingMap[screenId],\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n nodes: sanitized.nodes,\n },\n })\n }\n // A link to another page names it by slug in the starter: its pages have\n // no ids until a site is created from it (`site-starter-links.ts`).\n const routing: Record<string, string> = Object.fromEntries(\n captured.map(({ id }) => [id, routingMap[id]]),\n )\n const screens = captured.map(({ screen }) => ({\n ...screen,\n nodes: starterLinksBySlug(screen['nodes'] as Record<string, any>, routing),\n }))\n\n // The shared layout the Home page sits in: one per starter, since a new\n // site on the Free plan has room for one.\n const home = captured.find(({ id }) => routingMap[id] === '/')\n let layout: Record<string, unknown> | null = null\n if (home?.layoutId) {\n const layoutRef = hostRef.collection('layouts').doc(home.layoutId)\n const layoutSnapshot = await layoutRef.get()\n const versionId = layoutSnapshot.exists ? layoutSnapshot.get('versionId') : null\n const versionSnapshot = versionId\n ? await layoutRef.collection('versions').doc(String(versionId)).get()\n : null\n const nodes = versionSnapshot\n ? decodeStoredNodes<Record<string, any>>(versionSnapshot.get('nodes'))\n : null\n if (nodes && Object.keys(nodes).length) {\n // The layout properties its tree binds to (AGL-2933).\n const props = sanitizeMarketplaceProps(versionSnapshot?.get('props'))\n if (props.ok === false) {\n return res.status(422).json({ error: props.error })\n }\n const rootId: string =\n (nodes[CANVAS_ROOT_ELEMENT_ID] && CANVAS_ROOT_ELEMENT_ID) ||\n Object.keys(nodes).find((id) => !nodes[id]?.parentId) ||\n ''\n const sanitized = sanitizeSiteStarterNodes(\n { rootId, nodes },\n { layout: true, declaredProps: props.props },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: `The site's layout: ${sanitized.error}` })\n }\n // A layout whose slot did not survive renders every page as chrome\n // with the page missing.\n const hasSlot = Object.values(sanitized.nodes).some(\n (node: any) => node.componentId === SITE_STARTER_LAYOUT_SLOT_ID,\n )\n if (!hasSlot) {\n return res.status(422).json({\n error:\n \"The site's layout has no content slot, so pages using it would \" +\n 'have nowhere to render',\n })\n }\n layout = {\n displayName: String(layoutSnapshot.get('displayName') ?? 'Site header and footer'),\n nodes: starterLinksBySlug(sanitized.nodes, routing),\n ...(props.props.length && { props: props.props }),\n }\n }\n }\n\n // The theme, in the shape the publisher chose.\n let theme: SiteStarterTheme\n if (themeIn['kind'] === 'full') {\n // What this site DESIGNED: on an installed theme, the theme as stored\n // (its edits are this site's private changes to someone else's\n // artifact); otherwise the resolved view, edits included (AGL-3404).\n const host = (hostSnapshot.data() ?? {}) as ThemeHostDocument\n const full = themeArtifactContent(\n hostThemeSource(host) === 'installed' ? (host.theme ?? {}) : (resolveSiteTheme(host) ?? {}),\n )\n const validation = validateThemeForPublish(full)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = { kind: 'full', theme: full }\n } else {\n const builtin = builtinTheme(themeIn)\n if (typeof builtin === 'string') return res.status(422).json({ error: builtin })\n // Refused where the publisher can pick another color, not found on\n // every site created from it.\n const validation = builtinThemeValidation(builtin)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = builtin\n }\n\n // A built-in theme is what the starter's base and fonts facets describe;\n // a full theme is the publisher's to describe.\n const facets: SiteStarterFacets = {\n kind: facetId(facetsIn['kind']),\n base: theme.kind === 'builtin' ? theme.base : facetId(facetsIn['base']),\n fonts: theme.kind === 'builtin' ? theme.fonts : facetId(facetsIn['fonts']),\n pages: screens.length,\n mode,\n }\n const payload: SiteStarterPayload = {\n mode,\n facets,\n theme,\n screens,\n layout,\n ...(isRecord(answers) && { answers }),\n }\n // The one validity check, run here, at seed and again at resolve.\n const invalid = siteStarterRefusal(payload, SITE_STARTER_KNOWN_IDS)\n if (invalid) return res.status(422).json({ error: invalid })\n const bytes = JSON.stringify(payload).length\n if (bytes > MAX_STARTER_BYTES) {\n return res.status(422).json({\n error:\n `This site is too large to publish as one starter: its pages come to ` +\n `${Math.ceil(bytes / 1024)} KB, and a starter holds at most ` +\n `${MAX_STARTER_BYTES / 1024} KB. Publish fewer pages, or smaller ones.`,\n })\n }\n\n // One starter listing per source site; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceStarterHostId', '==', 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): every page,\n // the layout every page sits in, and the answers an AI starter writes from.\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n ...(isRecord(answers) && { readme: JSON.stringify(answers) }),\n pageNodes: [...screens, ...(layout ? [layout] : [])],\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n // The agreement this publish was sent with, recorded before anything\n // is listed under it (AGL-4105).\n await recordPublisherAgreementAcceptance({\n firestore,\n request: req,\n orgId: publisher.orgId,\n uid: decoded.uid,\n staff: decoded['staff'] === true,\n action: 'publish-starter',\n targetId: listingRef.id,\n })\n\n // The version before the listing that names it, so a pick made between\n // the two writes never finds a `latestVersion` with nothing stored at it.\n await listingRef.collection('versions').doc(String(version)).set({\n starter: payload,\n publishedAt: now,\n })\n await listingRef.set(\n {\n artifactType: SITE_STARTER_ARTIFACT_TYPE,\n profileId: publisher.orgId,\n sourceStarterHostId: hostId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321).\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 // A starter creates a whole site the moment it is picked, so a new one\n // waits for review (AGL-4049); a re-publish keeps the verdict it has,\n // except a rejection: publishing it again is how its publisher sends it\n // back for review, as the rejection told them, and the queue's Starters\n // section reads `submitted`. `official` is never written here: it is\n // the platform's to set.\n ...(existing.empty && { reviewStatus: 'submitted', createdAt: now }),\n ...(!existing.empty &&\n existing.docs[0].get('reviewStatus') === 'rejected' && {\n reviewStatus: 'submitted',\n rejectionReason: firebaseAdmin.firestore.FieldValue.delete(),\n rejectionCategory: firebaseAdmin.firestore.FieldValue.delete(),\n }),\n ...siteStarterFacetFields(facets),\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\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Starter publish failed' })\n }\n}\n\nexport default publishStarterHandler\n"],"names":["DEFAULT_SITE_THEME","displayNameSearchFields","SITE_STARTER_ARTIFACT_TYPE","SITE_STARTER_MAX_SCREENS","siteStarterFacetFields","siteStarterRefusal","starterLinksBySlug","SITE_STARTER_BRAND_KEYS","siteStarterPresetId","siteStarterTheme","sanitizeSiteStarterNodes","SITE_STARTER_LAYOUT_SLOT_ID","contrastRatio","hostThemeSource","parseColor","resolveSiteTheme","themeArtifactContent","validateThemeForPublish","listServerThemePresets","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","marketplacePriceRefusal","sanitizeMarketplaceProps","SITE_STARTER_KNOWN_IDS","resolvePublisherProfile","publishPreconditionRefusal","recordPublisherAgreementAcceptance","listingSubmissionRefusal","refreshListingQueryFields","holdsSiteKey","siteKeyRefusal","isRefusedIdToken","BRAND_ON_PAGE_MIN_CONTRAST","MAX_ANSWERS_BYTES","MAX_STARTER_BYTES","facetId","value","trim","slice","isRecord","Array","isArray","builtinTheme","raw","base","fonts","brandIn","brand","light","scheme","colors","undefined","out","key","Object","entries","includes","join","String","kind","builtinThemeValidation","builtin","preset","find","entry","id","theme","composed","validation","errors","inherited","dark","named","colorSchemes","page","background","default","ratio","primary","main","pageIsDark","push","path","message","toFixed","ok","length","warnings","publishStarterHandler","req","res","body","method","status","json","error","hostId","displayName","description","category","priceUsd","Math","round","Number","priceRefusal","themeIn","mode","answers","JSON","stringify","facetsIn","authorization","headers","idToken","startsWith","decoded","app","auth","verifyIdToken","hostSnapshot","existing","orgForHost","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","org","orgId","publisher","refusal","sells","agreement","routingMap","screenIds","keys","captured","screenId","screen","screenRef","data","deletedAt","versionId","versionSnapshot","nodes","sanitized","rootId","layoutId","slug","seo","routing","fromEntries","map","screens","home","layout","layoutRef","layoutSnapshot","props","parentId","declaredProps","hasSlot","values","some","node","componentId","host","full","facets","pages","payload","invalid","bytes","ceil","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","readme","pageNodes","official","request","staff","action","targetId","set","starter","publishedAt","artifactType","profileId","sourceStarterHostId","screenCount","latestVersion","reviewStatus","createdAt","rejectionReason","delete","rejectionCategory","updatedAt","versionHistory","arrayUnion","Timestamp","merge","listingId","console"],"mappings":"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"}
@@ -54,9 +54,14 @@ export declare function emailPublisher(orgId: string, subject: string, text: str
54
54
  */
55
55
  options?: {
56
56
  firestore?: any;
57
+ /**
58
+ * `noun` is what the listing is called in the details panel: "Plugin"
59
+ * unless named, "Site starter" for a starter's verdict (AGL-4049).
60
+ */
57
61
  frame?: {
58
62
  plugin: string;
59
63
  version?: string;
60
64
  outcome: string;
65
+ noun?: string;
61
66
  };
62
67
  }): Promise<void>;
@@ -63,6 +63,7 @@ import { platformConsoleOrigin } from "@aglyn/aglyn/app-utils/platform-brand";
63
63
  * Firestore. `frame` is the verdict as the designed email's details panel.
64
64
  */ options) {
65
65
  try {
66
+ var _frame_noun;
66
67
  const members = await listOrgMembers(orgId);
67
68
  const uids = members.filter((member)=>member.role === 'owner' || member.role === 'admin').map((member)=>member.$id);
68
69
  if (!uids.length) return;
@@ -90,7 +91,7 @@ import { platformConsoleOrigin } from "@aglyn/aglyn/app-utils/platform-brand";
90
91
  'review.subject': subject,
91
92
  'review.body': text,
92
93
  'review.details': frame ? [
93
- `Plugin: ${frame.plugin}`,
94
+ `${(_frame_noun = frame.noun) != null ? _frame_noun : 'Plugin'}: ${frame.plugin}`,
94
95
  frame.version ? `Version: ${frame.version}` : '',
95
96
  `Outcome: ${frame.outcome}`
96
97
  ].filter(Boolean).join('\n') : '',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publisher-review-email.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 firebaseAdmin,\n listOrgMembers,\n meterPlatformEmail,\n} from '@aglyn/tenant-data-admin'\n/*\n * From the LEAF, not the barrel (AGL-2407), for the reason\n * `usage-alert-email.ts` states: the review route's own spec mocks\n * `@aglyn/tenant-data-admin` wholesale because its graph reaches the admin\n * SDK, and a `jest.mock` factory is a closed world. A gate imported through\n * the barrel is silently replaced by `undefined` or by whatever the factory\n * happens to list, which is a check that reads as present and runs never.\n */\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport {\n renderSystemEmailContent,\n systemEmailBrand,\n} from '@aglyn/tenant-data-admin/server/render-system-email'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { platformConsoleOrigin } from '@aglyn/aglyn/app-utils/platform-brand'\n\n/**\n * Emails a publisher's owners and admins about a review outcome (AGL-972).\n *\n * In-app notifications alone assume the publisher is sitting in the console\n * — but review is asynchronous by nature: a submission can wait days, and\n * the publisher has no reason to keep checking. Best effort, and never\n * allowed to fail the verdict that triggered it.\n *\n * ## The platform hard-bounce list, and deliberately NOT the marketing gate\n *\n * This is the console's one multi-recipient platform fan-out, and a fan-out\n * is the shape the suppression list exists for: a publisher who submits\n * monthly is mailed monthly, and an address that permanently bounced is\n * re-attempted on every submission forever unless something looks. Repeat\n * delivery to a mailbox that has said it does not exist is what a mailbox\n * provider scores a sending domain on, and `aglyn.com` carries the password\n * resets and receipts on the same key.\n *\n * So it reads {@link filterSuppressedEmails} — the PLATFORM list, which\n * answers \"did this mailbox permanently fail, or did its owner report spam,\n * anywhere in the product\". It does not read `filterSendableForHost`: that\n * one adds `hosts/{hostId}/suppressions`, whose entries mean \"not from this\n * SITE'S campaigns\", and a review verdict is sent on no site's behalf. There\n * is no host to key it by, and a publisher opting out of their own site's\n * marketing says nothing about whether they want to hear that their plugin\n * was rejected.\n *\n * It also does not pass the marketing gate. A review verdict is a platform\n * notice to a publisher about their own submission — nearer a receipt than a\n * newsletter — and the gate would apply a topic the publisher never\n * subscribed to and a frequency cap that would silently drop the second\n * verdict of a busy week.\n */\nexport async function emailPublisher(\n orgId: string,\n subject: string,\n text: string,\n /**\n * `firestore` is injectable for tests; the list read defaults to the admin\n * Firestore. `frame` is the verdict as the designed email's details panel.\n */\n options?: {\n firestore?: any\n frame?: { plugin: string; version?: string; outcome: string }\n },\n): Promise<void> {\n try {\n const members = await listOrgMembers(orgId)\n const uids = members\n .filter((member) => member.role === 'owner' || member.role === 'admin')\n .map((member) => member.$id)\n if (!uids.length) return\n const users = await firebaseAdmin\n .app()\n .auth()\n .getUsers(uids.map((uid) => ({ uid })))\n const addresses = users.users\n .map((user) => user.email)\n .filter((email): email is string => Boolean(email))\n // Per address, so one dead mailbox among a publisher's admins does not\n // silence the verdict for the rest of them.\n const recipients = await filterSuppressedEmails(\n addresses,\n options?.firestore,\n )\n if (!recipients.length) return\n // The `plugin-review-update` system email (AGL-3367), in the platform's\n // brand: review is the platform talking about its own marketplace.\n // The publisher's Marketplace page, where the listing and its versions are.\n let slug = ''\n try {\n const org = await (options?.firestore ?? firebaseAdmin.app().firestore())\n .collection('orgs')\n .doc(orgId)\n .get()\n slug = String(org.get('slug') ?? '').trim()\n } catch {\n // Unread, the button opens the console home instead.\n }\n const frame = options?.frame\n const content = await renderSystemEmailContent(\n 'plugin-review-update',\n {\n 'review.subject': subject,\n 'review.body': text,\n 'review.details': frame\n ? [\n `Plugin: ${frame.plugin}`,\n frame.version ? `Version: ${frame.version}` : '',\n `Outcome: ${frame.outcome}`,\n ]\n .filter(Boolean)\n .join('\\n')\n : '',\n 'review.url': `${platformConsoleOrigin()}${slug ? `/${slug}/marketplace` : ''}`,\n },\n systemEmailBrand(null),\n { subject, text },\n )\n const results = await Promise.all(\n recipients.map((to) =>\n sendEmail({ to, ...content, context: 'plugin review update' }),\n ),\n )\n // Cost meter (AGL-1438). Platform-scoped: marketplace review is Aglyn's\n // own workflow talking to a publisher, not mail the publisher's org sent.\n await meterPlatformEmail(results.filter((result) => result.sent).length)\n } catch (error) {\n console.error('publisher review email failed', error)\n }\n}\n"],"names":["firebaseAdmin","listOrgMembers","meterPlatformEmail","filterSuppressedEmails","renderSystemEmailContent","systemEmailBrand","sendEmail","platformConsoleOrigin","emailPublisher","orgId","subject","text","options","members","uids","filter","member","role","map","$id","length","users","app","auth","getUsers","uid","addresses","user","email","Boolean","recipients","firestore","slug","org","collection","doc","get","String","trim","frame","content","plugin","version","outcome","join","results","Promise","all","to","context","result","sent","error","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,aAAa,EACbC,cAAc,EACdC,kBAAkB,QACb,2BAA0B;AACjC;;;;;;;CAOC,GACD,SAASC,sBAAsB,QAAQ,oDAAmD;AAC1F,SACEC,wBAAwB,EACxBC,gBAAgB,QACX,sDAAqD;AAC5D,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,qBAAqB,QAAQ,wCAAuC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,eAAeC,eACpBC,KAAa,EACbC,OAAe,EACfC,IAAY,EACZ;;;GAGC,GACDC,OAGC;IAED,IAAI;QACF,MAAMC,UAAU,MAAMZ,eAAeQ;QACrC,MAAMK,OAAOD,QACVE,MAAM,CAAC,CAACC,SAAWA,OAAOC,IAAI,KAAK,WAAWD,OAAOC,IAAI,KAAK,SAC9DC,GAAG,CAAC,CAACF,SAAWA,OAAOG,GAAG;QAC7B,IAAI,CAACL,KAAKM,MAAM,EAAE;QAClB,MAAMC,QAAQ,MAAMrB,cACjBsB,GAAG,GACHC,IAAI,GACJC,QAAQ,CAACV,KAAKI,GAAG,CAAC,CAACO,MAAS,CAAA;gBAAEA;YAAI,CAAA;QACrC,MAAMC,YAAYL,MAAMA,KAAK,CAC1BH,GAAG,CAAC,CAACS,OAASA,KAAKC,KAAK,EACxBb,MAAM,CAAC,CAACa,QAA2BC,QAAQD;QAC9C,uEAAuE;QACvE,4CAA4C;QAC5C,MAAME,aAAa,MAAM3B,uBACvBuB,WACAd,2BAAAA,QAASmB,SAAS;QAEpB,IAAI,CAACD,WAAWV,MAAM,EAAE;QACxB,wEAAwE;QACxE,mEAAmE;QACnE,4EAA4E;QAC5E,IAAIY,OAAO;QACX,IAAI;sBAKYC;YAJd,MAAMA,MAAM,MAAM,SAACrB,2BAAAA,QAASmB,SAAS,mBAAI/B,cAAcsB,GAAG,GAAGS,SAAS,IACnEG,UAAU,CAAC,QACXC,GAAG,CAAC1B,OACJ2B,GAAG;YACNJ,OAAOK,QAAOJ,WAAAA,IAAIG,GAAG,CAAC,mBAARH,WAAmB,IAAIK,IAAI;QAC3C,EAAE,eAAM;QACN,qDAAqD;QACvD;QACA,MAAMC,QAAQ3B,2BAAAA,QAAS2B,KAAK;QAC5B,MAAMC,UAAU,MAAMpC,yBACpB,wBACA;YACE,kBAAkBM;YAClB,eAAeC;YACf,kBAAkB4B,QACd;gBACE,CAAC,QAAQ,EAAEA,MAAME,MAAM,EAAE;gBACzBF,MAAMG,OAAO,GAAG,CAAC,SAAS,EAAEH,MAAMG,OAAO,EAAE,GAAG;gBAC9C,CAAC,SAAS,EAAEH,MAAMI,OAAO,EAAE;aAC5B,CACE5B,MAAM,CAACc,SACPe,IAAI,CAAC,QACR;YACJ,cAAc,GAAGrC,0BAA0ByB,OAAO,CAAC,CAAC,EAAEA,KAAK,YAAY,CAAC,GAAG,IAAI;QACjF,GACA3B,iBAAiB,OACjB;YAAEK;YAASC;QAAK;QAElB,MAAMkC,UAAU,MAAMC,QAAQC,GAAG,CAC/BjB,WAAWZ,GAAG,CAAC,CAAC8B,KACd1C,UAAU;gBAAE0C;eAAOR;gBAASS,SAAS;;QAGzC,wEAAwE;QACxE,0EAA0E;QAC1E,MAAM/C,mBAAmB2C,QAAQ9B,MAAM,CAAC,CAACmC,SAAWA,OAAOC,IAAI,EAAE/B,MAAM;IACzE,EAAE,OAAOgC,OAAO;QACdC,QAAQD,KAAK,CAAC,iCAAiCA;IACjD;AACF"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publisher-review-email.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 firebaseAdmin,\n listOrgMembers,\n meterPlatformEmail,\n} from '@aglyn/tenant-data-admin'\n/*\n * From the LEAF, not the barrel (AGL-2407), for the reason\n * `usage-alert-email.ts` states: the review route's own spec mocks\n * `@aglyn/tenant-data-admin` wholesale because its graph reaches the admin\n * SDK, and a `jest.mock` factory is a closed world. A gate imported through\n * the barrel is silently replaced by `undefined` or by whatever the factory\n * happens to list, which is a check that reads as present and runs never.\n */\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport {\n renderSystemEmailContent,\n systemEmailBrand,\n} from '@aglyn/tenant-data-admin/server/render-system-email'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { platformConsoleOrigin } from '@aglyn/aglyn/app-utils/platform-brand'\n\n/**\n * Emails a publisher's owners and admins about a review outcome (AGL-972).\n *\n * In-app notifications alone assume the publisher is sitting in the console\n * — but review is asynchronous by nature: a submission can wait days, and\n * the publisher has no reason to keep checking. Best effort, and never\n * allowed to fail the verdict that triggered it.\n *\n * ## The platform hard-bounce list, and deliberately NOT the marketing gate\n *\n * This is the console's one multi-recipient platform fan-out, and a fan-out\n * is the shape the suppression list exists for: a publisher who submits\n * monthly is mailed monthly, and an address that permanently bounced is\n * re-attempted on every submission forever unless something looks. Repeat\n * delivery to a mailbox that has said it does not exist is what a mailbox\n * provider scores a sending domain on, and `aglyn.com` carries the password\n * resets and receipts on the same key.\n *\n * So it reads {@link filterSuppressedEmails} — the PLATFORM list, which\n * answers \"did this mailbox permanently fail, or did its owner report spam,\n * anywhere in the product\". It does not read `filterSendableForHost`: that\n * one adds `hosts/{hostId}/suppressions`, whose entries mean \"not from this\n * SITE'S campaigns\", and a review verdict is sent on no site's behalf. There\n * is no host to key it by, and a publisher opting out of their own site's\n * marketing says nothing about whether they want to hear that their plugin\n * was rejected.\n *\n * It also does not pass the marketing gate. A review verdict is a platform\n * notice to a publisher about their own submission — nearer a receipt than a\n * newsletter — and the gate would apply a topic the publisher never\n * subscribed to and a frequency cap that would silently drop the second\n * verdict of a busy week.\n */\nexport async function emailPublisher(\n orgId: string,\n subject: string,\n text: string,\n /**\n * `firestore` is injectable for tests; the list read defaults to the admin\n * Firestore. `frame` is the verdict as the designed email's details panel.\n */\n options?: {\n firestore?: any\n /**\n * `noun` is what the listing is called in the details panel: \"Plugin\"\n * unless named, \"Site starter\" for a starter's verdict (AGL-4049).\n */\n frame?: { plugin: string; version?: string; outcome: string; noun?: string }\n },\n): Promise<void> {\n try {\n const members = await listOrgMembers(orgId)\n const uids = members\n .filter((member) => member.role === 'owner' || member.role === 'admin')\n .map((member) => member.$id)\n if (!uids.length) return\n const users = await firebaseAdmin\n .app()\n .auth()\n .getUsers(uids.map((uid) => ({ uid })))\n const addresses = users.users\n .map((user) => user.email)\n .filter((email): email is string => Boolean(email))\n // Per address, so one dead mailbox among a publisher's admins does not\n // silence the verdict for the rest of them.\n const recipients = await filterSuppressedEmails(\n addresses,\n options?.firestore,\n )\n if (!recipients.length) return\n // The `plugin-review-update` system email (AGL-3367), in the platform's\n // brand: review is the platform talking about its own marketplace.\n // The publisher's Marketplace page, where the listing and its versions are.\n let slug = ''\n try {\n const org = await (options?.firestore ?? firebaseAdmin.app().firestore())\n .collection('orgs')\n .doc(orgId)\n .get()\n slug = String(org.get('slug') ?? '').trim()\n } catch {\n // Unread, the button opens the console home instead.\n }\n const frame = options?.frame\n const content = await renderSystemEmailContent(\n 'plugin-review-update',\n {\n 'review.subject': subject,\n 'review.body': text,\n 'review.details': frame\n ? [\n `${frame.noun ?? 'Plugin'}: ${frame.plugin}`,\n frame.version ? `Version: ${frame.version}` : '',\n `Outcome: ${frame.outcome}`,\n ]\n .filter(Boolean)\n .join('\\n')\n : '',\n 'review.url': `${platformConsoleOrigin()}${slug ? `/${slug}/marketplace` : ''}`,\n },\n systemEmailBrand(null),\n { subject, text },\n )\n const results = await Promise.all(\n recipients.map((to) =>\n sendEmail({ to, ...content, context: 'plugin review update' }),\n ),\n )\n // Cost meter (AGL-1438). Platform-scoped: marketplace review is Aglyn's\n // own workflow talking to a publisher, not mail the publisher's org sent.\n await meterPlatformEmail(results.filter((result) => result.sent).length)\n } catch (error) {\n console.error('publisher review email failed', error)\n }\n}\n"],"names":["firebaseAdmin","listOrgMembers","meterPlatformEmail","filterSuppressedEmails","renderSystemEmailContent","systemEmailBrand","sendEmail","platformConsoleOrigin","emailPublisher","orgId","subject","text","options","frame","members","uids","filter","member","role","map","$id","length","users","app","auth","getUsers","uid","addresses","user","email","Boolean","recipients","firestore","slug","org","collection","doc","get","String","trim","content","noun","plugin","version","outcome","join","results","Promise","all","to","context","result","sent","error","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,aAAa,EACbC,cAAc,EACdC,kBAAkB,QACb,2BAA0B;AACjC;;;;;;;CAOC,GACD,SAASC,sBAAsB,QAAQ,oDAAmD;AAC1F,SACEC,wBAAwB,EACxBC,gBAAgB,QACX,sDAAqD;AAC5D,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,qBAAqB,QAAQ,wCAAuC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,eAAeC,eACpBC,KAAa,EACbC,OAAe,EACfC,IAAY,EACZ;;;GAGC,GACDC,OAOC;IAED,IAAI;YAyCWC;QAxCb,MAAMC,UAAU,MAAMb,eAAeQ;QACrC,MAAMM,OAAOD,QACVE,MAAM,CAAC,CAACC,SAAWA,OAAOC,IAAI,KAAK,WAAWD,OAAOC,IAAI,KAAK,SAC9DC,GAAG,CAAC,CAACF,SAAWA,OAAOG,GAAG;QAC7B,IAAI,CAACL,KAAKM,MAAM,EAAE;QAClB,MAAMC,QAAQ,MAAMtB,cACjBuB,GAAG,GACHC,IAAI,GACJC,QAAQ,CAACV,KAAKI,GAAG,CAAC,CAACO,MAAS,CAAA;gBAAEA;YAAI,CAAA;QACrC,MAAMC,YAAYL,MAAMA,KAAK,CAC1BH,GAAG,CAAC,CAACS,OAASA,KAAKC,KAAK,EACxBb,MAAM,CAAC,CAACa,QAA2BC,QAAQD;QAC9C,uEAAuE;QACvE,4CAA4C;QAC5C,MAAME,aAAa,MAAM5B,uBACvBwB,WACAf,2BAAAA,QAASoB,SAAS;QAEpB,IAAI,CAACD,WAAWV,MAAM,EAAE;QACxB,wEAAwE;QACxE,mEAAmE;QACnE,4EAA4E;QAC5E,IAAIY,OAAO;QACX,IAAI;sBAKYC;YAJd,MAAMA,MAAM,MAAM,SAACtB,2BAAAA,QAASoB,SAAS,mBAAIhC,cAAcuB,GAAG,GAAGS,SAAS,IACnEG,UAAU,CAAC,QACXC,GAAG,CAAC3B,OACJ4B,GAAG;YACNJ,OAAOK,QAAOJ,WAAAA,IAAIG,GAAG,CAAC,mBAARH,WAAmB,IAAIK,IAAI;QAC3C,EAAE,eAAM;QACN,qDAAqD;QACvD;QACA,MAAM1B,QAAQD,2BAAAA,QAASC,KAAK;QAC5B,MAAM2B,UAAU,MAAMpC,yBACpB,wBACA;YACE,kBAAkBM;YAClB,eAAeC;YACf,kBAAkBE,QACd;gBACE,IAAGA,cAAAA,MAAM4B,IAAI,YAAV5B,cAAc,SAAS,EAAE,EAAEA,MAAM6B,MAAM,EAAE;gBAC5C7B,MAAM8B,OAAO,GAAG,CAAC,SAAS,EAAE9B,MAAM8B,OAAO,EAAE,GAAG;gBAC9C,CAAC,SAAS,EAAE9B,MAAM+B,OAAO,EAAE;aAC5B,CACE5B,MAAM,CAACc,SACPe,IAAI,CAAC,QACR;YACJ,cAAc,GAAGtC,0BAA0B0B,OAAO,CAAC,CAAC,EAAEA,KAAK,YAAY,CAAC,GAAG,IAAI;QACjF,GACA5B,iBAAiB,OACjB;YAAEK;YAASC;QAAK;QAElB,MAAMmC,UAAU,MAAMC,QAAQC,GAAG,CAC/BjB,WAAWZ,GAAG,CAAC,CAAC8B,KACd3C,UAAU;gBAAE2C;eAAOT;gBAASU,SAAS;;QAGzC,wEAAwE;QACxE,0EAA0E;QAC1E,MAAMhD,mBAAmB4C,QAAQ9B,MAAM,CAAC,CAACmC,SAAWA,OAAOC,IAAI,EAAE/B,MAAM;IACzE,EAAE,OAAOgC,OAAO;QACdC,QAAQD,KAAK,CAAC,iCAAiCA;IACjD;AACF"}
@@ -28,4 +28,7 @@ export declare const SITE_STARTERS_UNAVAILABLE = "Starters are unavailable right
28
28
  */
29
29
  export declare function starterSearchLabels(): StarterSearchLabels;
30
30
  declare function handler(request: Request): Promise<Response>;
31
- export { handler as marketplaceStarters };
31
+ /** What the answers door says of a starter site, which asks nothing. */
32
+ export declare const STARTER_HAS_NO_QUESTIONS = "This starter is a ready-made site, with no questions to answer";
33
+ declare function answersHandler(request: Request): Promise<Response>;
34
+ export { handler as marketplaceStarters, answersHandler as marketplaceStarterAnswers };
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -22,7 +23,7 @@ import { describeSiteStarter, resolveSiteStarterSource } from "@aglyn/aglyn/plug
22
23
  import { listServerThemePresets } from "@aglyn/aglyn/plugin-manager/plugin-theme-presets";
23
24
  import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession, isServerReleaseFlagOnForOrg, memberHasOrgPermission, resolveOrgMembership } from "@aglyn/tenant-data-admin";
24
25
  import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
25
- import { readStarterSummary, starterQueryFrom } from "../model/starter-query.js";
26
+ import { readStarterSummary, starterAnswersQueryFrom, starterQueryFrom } from "../model/starter-query.js";
26
27
  /*
27
28
  * THE STARTER SEARCH "Choose your starter" ASKS (AGL-4049).
28
29
  *
@@ -58,6 +59,22 @@ import { readStarterSummary, starterQueryFrom } from "../model/starter-query.js"
58
59
  *
59
60
  * The dispatcher in front refuses an unverified account and a lockdown too;
60
61
  * the verification check is repeated here so the route stands on its own.
62
+ *
63
+ * AN AI STARTER'S ANSWERS (AGL-4049 phase 4).
64
+ *
65
+ * `GET /api/marketplace/starters/answers?orgId=&listingId=` answers
66
+ * `{ answers, facets }`: what an AI starter carries to fill the AI start's
67
+ * questions once a person picks it. Behind the same gates in the same order
68
+ * (its 400 is a listing id no listing can have), then the seam's `resolve`,
69
+ * which is where a priced starter is held to its purchase:
70
+ *
71
+ * 402 a priced starter the workspace has not bought, with its price and
72
+ * `code: purchase_required`, as the create route words it
73
+ * 403 404 passed on from the source, naming the listing
74
+ * 409 a starter site, which is created as it is and asks nothing
75
+ *
76
+ * The answers and the facets only: never the starter's pages, its layout or
77
+ * its theme, which the site is created from, not the questions.
61
78
  */ const NO_STORE = {
62
79
  'Cache-Control': 'no-store'
63
80
  };
@@ -105,7 +122,12 @@ const refuse = (status, error)=>answer({
105
122
  });
106
123
  return resolveSiteStarterSource();
107
124
  }
108
- async function handler(request) {
125
+ /**
126
+ * Both doors' first gates and their failure handling: GET only, a bearer
127
+ * token the verifier accepts, a verified address (impersonation exempt).
128
+ * `run` answers the rest; whatever it throws is a 401 for a credential the
129
+ * verifier refused (AGL-1993) and a 500 in `failed`'s words otherwise.
130
+ */ async function asVerifiedCaller(request, failed, run) {
109
131
  var _request_headers_get;
110
132
  if (request.method !== 'GET') return refuse(405, 'Method not allowed');
111
133
  const authorization = (_request_headers_get = request.headers.get('authorization')) != null ? _request_headers_get : '';
@@ -115,25 +137,54 @@ async function handler(request) {
115
137
  if (!decoded.email_verified && !isImpersonationSession(decoded)) {
116
138
  return emailUnverifiedResponse();
117
139
  }
140
+ return await run(decoded);
141
+ } catch (error) {
142
+ // An unverifiable credential is a 401, not a fault of ours (AGL-1993);
143
+ // anything else keeps its 500.
144
+ const unauthenticated = invalidIdTokenResponse(error);
145
+ if (unauthenticated) return unauthenticated;
146
+ console.error(failed.log, error);
147
+ return refuse(500, failed.error);
148
+ }
149
+ }
150
+ /**
151
+ * The gates after the question is read, in order: a member of the workspace
152
+ * named, holding "Create sites", the release flag on for it (staff pass
153
+ * without it being asked), and a plugin that serves starters. Answers the
154
+ * viewer every source call is made for — the membership's workspace, never
155
+ * the query's — and the source, or the refusal.
156
+ */ async function admitViewer(caller, askedOrgId) {
157
+ // The membership's org, never the query's: every gate below asks about
158
+ // the workspace the caller actually belongs to.
159
+ const membership = await resolveOrgMembership(caller.uid, askedOrgId);
160
+ if (!membership) return refuse(403, 'You are not a member of that organization');
161
+ const orgId = membership.orgId;
162
+ if (!await memberHasOrgPermission(orgId, membership.member, 'hosts.create')) {
163
+ return refuse(403, permissionRefusalText('hosts.create'));
164
+ }
165
+ if (caller['staff'] !== true && !await isServerReleaseFlagOnForOrg('release_site_starters', orgId)) {
166
+ return refuse(403, SITE_STARTERS_UNRELEASED);
167
+ }
168
+ const source = await starterSource();
169
+ if (!source) return refuse(503, SITE_STARTERS_UNAVAILABLE);
170
+ return {
171
+ viewer: {
172
+ orgId,
173
+ uid: caller.uid
174
+ },
175
+ source
176
+ };
177
+ }
178
+ async function handler(request) {
179
+ return asVerifiedCaller(request, {
180
+ log: '[marketplace/starters] search failed',
181
+ error: 'Starters could not be loaded'
182
+ }, async (caller)=>{
118
183
  const asked = starterQueryFrom(new URL(request.url).searchParams);
119
184
  if ('error' in asked) return refuse(400, asked.error);
120
- // The membership's org, never the query's: every gate below asks about
121
- // the workspace the caller actually belongs to.
122
- const membership = await resolveOrgMembership(decoded.uid, asked.orgId);
123
- if (!membership) return refuse(403, 'You are not a member of that organization');
124
- const orgId = membership.orgId;
125
- if (!await memberHasOrgPermission(orgId, membership.member, 'hosts.create')) {
126
- return refuse(403, permissionRefusalText('hosts.create'));
127
- }
128
- if (decoded['staff'] !== true && !await isServerReleaseFlagOnForOrg('release_site_starters', orgId)) {
129
- return refuse(403, SITE_STARTERS_UNRELEASED);
130
- }
131
- const source = await starterSource();
132
- if (!source) return refuse(503, SITE_STARTERS_UNAVAILABLE);
133
- const viewer = {
134
- orgId,
135
- uid: decoded.uid
136
- };
185
+ const admitted = await admitViewer(caller, asked.orgId);
186
+ if (admitted instanceof Response) return admitted;
187
+ const { viewer, source } = admitted;
137
188
  const found = await source.search({
138
189
  word: asked.word,
139
190
  filter: asked.filter,
@@ -156,15 +207,64 @@ async function handler(request) {
156
207
  labels: starterSearchLabels()
157
208
  };
158
209
  return answer(body);
159
- } catch (error) {
160
- // An unverifiable credential is a 401, not a fault of ours (AGL-1993);
161
- // anything else keeps its 500.
162
- const unauthenticated = invalidIdTokenResponse(error);
163
- if (unauthenticated) return unauthenticated;
164
- console.error('[marketplace/starters] search failed', error);
165
- return refuse(500, 'Starters could not be loaded');
166
- }
210
+ });
211
+ }
212
+ /** What the answers door says of a starter site, which asks nothing. */ export const STARTER_HAS_NO_QUESTIONS = 'This starter is a ready-made site, with no questions to answer';
213
+ const isRecord = (value)=>!!value && typeof value === 'object' && !Array.isArray(value);
214
+ async function answersHandler(request) {
215
+ return asVerifiedCaller(request, {
216
+ log: '[marketplace/starters/answers] read failed',
217
+ error: 'This starter could not be read'
218
+ }, async (caller)=>{
219
+ var _ref;
220
+ var _readStarterSummary;
221
+ const asked = starterAnswersQueryFrom(new URL(request.url).searchParams);
222
+ if ('error' in asked) return refuse(400, asked.error);
223
+ const admitted = await admitViewer(caller, asked.orgId);
224
+ if (admitted instanceof Response) return admitted;
225
+ const { viewer, source } = admitted;
226
+ const listingId = asked.listingId;
227
+ const resolved = await source.resolve({
228
+ listingId
229
+ }, viewer);
230
+ if (!resolved || typeof resolved !== 'object') {
231
+ return answer({
232
+ error: 'That starter is no longer available',
233
+ listingId
234
+ }, 404);
235
+ }
236
+ if ('error' in resolved) {
237
+ const status = resolved.status === 402 || resolved.status === 403 ? resolved.status : 404;
238
+ return answer(_extends({
239
+ error: resolved.error
240
+ }, status === 402 ? {
241
+ code: 'purchase_required'
242
+ } : {}, {
243
+ listingId
244
+ }, status === 402 && typeof resolved.priceUsd === 'number' ? {
245
+ priceUsd: resolved.priceUsd
246
+ } : {}), status);
247
+ }
248
+ if (resolved.mode !== 'ai') return answer({
249
+ error: STARTER_HAS_NO_QUESTIONS,
250
+ listingId
251
+ }, 409);
252
+ // `resolve` refuses an AI starter without them; checked again, so a
253
+ // source that skipped the check hands over nothing.
254
+ if (!isRecord(resolved.answers)) return answer({
255
+ error: 'That starter is no longer available',
256
+ listingId
257
+ }, 404);
258
+ const body = {
259
+ answers: resolved.answers,
260
+ facets: (_ref = (_readStarterSummary = readStarterSummary({
261
+ listingId,
262
+ facets: resolved.facets
263
+ })) == null ? void 0 : _readStarterSummary.facets) != null ? _ref : resolved.facets
264
+ };
265
+ return answer(body);
266
+ });
167
267
  }
168
- export { handler as marketplaceStarters };
268
+ export { handler as marketplaceStarters, answersHandler as marketplaceStarterAnswers };
169
269
 
170
270
  //# sourceMappingURL=starters.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/starters.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 { permissionRefusalText } from '@aglyn/aglyn/app-utils/org-permissions'\nimport { SITE_FONT_PAIRINGS } from '@aglyn/aglyn/app-utils/site-font-pairings'\nimport { SITE_THEME_BASES } from '@aglyn/aglyn/app-utils/site-starter'\nimport { siteStarterPresetId } from '@aglyn/aglyn/app-utils/site-starter-theme'\nimport { runPluginDeclarationsRepair } from '@aglyn/aglyn/plugin-manager/plugin-declarations-repair'\nimport {\n describeSiteStarter,\n resolveSiteStarterSource,\n type PluginSiteStarterSource,\n type SiteStarterSummary,\n} from '@aglyn/aglyn/plugin-manager/plugin-site-starters'\nimport { listServerThemePresets } from '@aglyn/aglyn/plugin-manager/plugin-theme-presets'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n readStarterSummary,\n starterQueryFrom,\n type StarterSearchAnswer,\n type StarterSearchLabels,\n} from '../model/starter-query'\n\n/*\n * THE STARTER SEARCH \"Choose your starter\" ASKS (AGL-4049).\n *\n * `GET /api/marketplace/starters?orgId=&mode=base|ai&q=&kind=&base=&fonts=&limit=&include=`\n * answers `{ rows, labels }`: the starters matching a search, each a summary\n * a card shows, and the names for the theme and font ids they carry.\n *\n * `include` names a listing the answer carries first whatever the search\n * finds: the person's own pick, restored after they went to pay for it,\n * which a search for other words would not list. It is described for this\n * viewer through the seam's `describe`, behind the same gates as a pick, so\n * it lists nothing the person could not pick anyway.\n *\n * Summaries only. A paid AI starter's answers ARE what it sells, and a\n * starter's pages are what a site is created from; both come out of the\n * seam's `resolve`, which is purchase-gated, never out of a search. Each row\n * is read back through `readStarterSummary`, so what the source hands over\n * beyond a summary does not leave this route.\n *\n * The gates, in order, each answered before the next is read:\n *\n * 401 no bearer token, or one the verifier refused\n * 403 an unverified address (impersonation exempt)\n * 400 no workspace named, or a facet no starter can carry\n * 403 not a member of the workspace named\n * 403 a member without \"Create sites\" (`hosts.create`): searching is the\n * first step of creating a site, so it takes the create's permission,\n * and the refusal names the permission, never a role\n * 403 `release_site_starters` off for the workspace, unless the caller is\n * staff — the same flag, the same words and the same staff pass as\n * the create route's starter pick; refused BEFORE the source is read\n * 503 no plugin serves starters\n *\n * The dispatcher in front refuses an unverified account and a lockdown too;\n * the verification check is repeated here so the route stands on its own.\n */\n\nconst NO_STORE = { 'Cache-Control': 'no-store' }\n\nconst answer = (body: unknown, status = 200): Response =>\n Response.json(body, { status, headers: NO_STORE })\n\nconst refuse = (status: number, error: string): Response => answer({ error }, status)\n\n/** The create route's words for the same flag, so the two doors never disagree. */\nexport const SITE_STARTERS_UNRELEASED = 'Site starters are not available for this workspace yet'\n\n/** And for a deployment where no plugin serves starters. */\nexport const SITE_STARTERS_UNAVAILABLE = 'Starters are unavailable right now. Try again in a moment.'\n\n/**\n * The names a card and a filter show for a starter's ids: each built-in\n * theme by the name the theme picker gives its preset, and each font pairing\n * by its families (\"Inter\", or \"Fraunces & Inter\" when they differ). A base\n * whose preset no plugin registered in this process is left out, and the\n * surface shows nothing for it rather than an id.\n */\nexport function starterSearchLabels(): StarterSearchLabels {\n const presets = listServerThemePresets()\n const base: Record<string, string> = {}\n for (const id of SITE_THEME_BASES) {\n const name = presets.find((preset) => preset?.id === siteStarterPresetId(id))?.name\n if (typeof name === 'string' && name) base[id] = name\n }\n const fonts: Record<string, string> = {}\n for (const pairing of SITE_FONT_PAIRINGS) {\n fonts[pairing.id] =\n pairing.heading.family === pairing.body.family\n ? pairing.heading.family\n : `${pairing.heading.family} & ${pairing.body.family}`\n }\n return { base, fonts }\n}\n\n/**\n * The source, asking the app to run its boot declarations once when nobody\n * registered one — a boot whose declarations failed leaves the seam empty,\n * and the create route repairs the same way.\n */\nasync function starterSource(): Promise<PluginSiteStarterSource | null> {\n const registered = resolveSiteStarterSource()\n if (registered) return registered\n await runPluginDeclarationsRepair().catch((error: unknown) => {\n console.error('[marketplace/starters] plugin declarations failed', error)\n })\n return resolveSiteStarterSource()\n}\n\nasync function handler(request: Request): Promise<Response> {\n if (request.method !== 'GET') return refuse(405, 'Method not allowed')\n const authorization = request.headers.get('authorization') ?? ''\n if (!authorization.startsWith('Bearer ')) return refuse(401, 'Unauthenticated')\n\n try {\n const decoded = await firebaseAdmin\n .app()\n .auth()\n .verifyIdToken(authorization.slice('Bearer '.length))\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const asked = starterQueryFrom(new URL(request.url).searchParams)\n if ('error' in asked) return refuse(400, asked.error)\n\n // The membership's org, never the query's: every gate below asks about\n // the workspace the caller actually belongs to.\n const membership = await resolveOrgMembership(decoded.uid, asked.orgId)\n if (!membership) return refuse(403, 'You are not a member of that organization')\n const orgId = membership.orgId\n if (!(await memberHasOrgPermission(orgId, membership.member, 'hosts.create'))) {\n return refuse(403, permissionRefusalText('hosts.create'))\n }\n if (\n decoded['staff'] !== true &&\n !(await isServerReleaseFlagOnForOrg('release_site_starters', orgId))\n ) {\n return refuse(403, SITE_STARTERS_UNRELEASED)\n }\n\n const source = await starterSource()\n if (!source) return refuse(503, SITE_STARTERS_UNAVAILABLE)\n const viewer = { orgId, uid: decoded.uid }\n const found = await source.search(\n { word: asked.word, filter: asked.filter, limit: asked.limit },\n viewer,\n )\n const rows: SiteStarterSummary[] = []\n // The restored pick first, so the card the person chose is the one they\n // see, wherever the search would have put it, or left it out.\n const included = asked.include\n ? readStarterSummary(await describeSiteStarter(source, { listingId: asked.include }, viewer))\n : null\n if (included) rows.push(included)\n for (const value of Array.isArray(found) ? found : []) {\n const row = readStarterSummary(value)\n if (row && row.listingId !== included?.listingId) rows.push(row)\n if (rows.length >= asked.limit + (included ? 1 : 0)) break\n }\n const body: StarterSearchAnswer = { rows, labels: starterSearchLabels() }\n return answer(body)\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours (AGL-1993);\n // anything else keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[marketplace/starters] search failed', error)\n return refuse(500, 'Starters could not be loaded')\n }\n}\n\nexport { handler as marketplaceStarters }\n"],"names":["permissionRefusalText","SITE_FONT_PAIRINGS","SITE_THEME_BASES","siteStarterPresetId","runPluginDeclarationsRepair","describeSiteStarter","resolveSiteStarterSource","listServerThemePresets","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","isServerReleaseFlagOnForOrg","memberHasOrgPermission","resolveOrgMembership","invalidIdTokenResponse","readStarterSummary","starterQueryFrom","NO_STORE","answer","body","status","Response","json","headers","refuse","error","SITE_STARTERS_UNRELEASED","SITE_STARTERS_UNAVAILABLE","starterSearchLabels","presets","base","id","name","find","preset","fonts","pairing","heading","family","starterSource","registered","catch","console","handler","request","method","authorization","get","startsWith","decoded","app","auth","verifyIdToken","slice","length","email_verified","asked","URL","url","searchParams","membership","uid","orgId","member","source","viewer","found","search","word","filter","limit","rows","included","include","listingId","push","value","Array","isArray","row","labels","unauthenticated","marketplaceStarters"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/starters.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 { permissionRefusalText } from '@aglyn/aglyn/app-utils/org-permissions'\nimport { SITE_FONT_PAIRINGS } from '@aglyn/aglyn/app-utils/site-font-pairings'\nimport { SITE_THEME_BASES } from '@aglyn/aglyn/app-utils/site-starter'\nimport { siteStarterPresetId } from '@aglyn/aglyn/app-utils/site-starter-theme'\nimport { runPluginDeclarationsRepair } from '@aglyn/aglyn/plugin-manager/plugin-declarations-repair'\nimport {\n describeSiteStarter,\n resolveSiteStarterSource,\n type PluginSiteStarterSource,\n type SiteStarterSummary,\n type SiteStarterViewer,\n} from '@aglyn/aglyn/plugin-manager/plugin-site-starters'\nimport { listServerThemePresets } from '@aglyn/aglyn/plugin-manager/plugin-theme-presets'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n readStarterSummary,\n starterAnswersQueryFrom,\n starterQueryFrom,\n type StarterAnswersAnswer,\n type StarterSearchAnswer,\n type StarterSearchLabels,\n} from '../model/starter-query'\n\n/*\n * THE STARTER SEARCH \"Choose your starter\" ASKS (AGL-4049).\n *\n * `GET /api/marketplace/starters?orgId=&mode=base|ai&q=&kind=&base=&fonts=&limit=&include=`\n * answers `{ rows, labels }`: the starters matching a search, each a summary\n * a card shows, and the names for the theme and font ids they carry.\n *\n * `include` names a listing the answer carries first whatever the search\n * finds: the person's own pick, restored after they went to pay for it,\n * which a search for other words would not list. It is described for this\n * viewer through the seam's `describe`, behind the same gates as a pick, so\n * it lists nothing the person could not pick anyway.\n *\n * Summaries only. A paid AI starter's answers ARE what it sells, and a\n * starter's pages are what a site is created from; both come out of the\n * seam's `resolve`, which is purchase-gated, never out of a search. Each row\n * is read back through `readStarterSummary`, so what the source hands over\n * beyond a summary does not leave this route.\n *\n * The gates, in order, each answered before the next is read:\n *\n * 401 no bearer token, or one the verifier refused\n * 403 an unverified address (impersonation exempt)\n * 400 no workspace named, or a facet no starter can carry\n * 403 not a member of the workspace named\n * 403 a member without \"Create sites\" (`hosts.create`): searching is the\n * first step of creating a site, so it takes the create's permission,\n * and the refusal names the permission, never a role\n * 403 `release_site_starters` off for the workspace, unless the caller is\n * staff — the same flag, the same words and the same staff pass as\n * the create route's starter pick; refused BEFORE the source is read\n * 503 no plugin serves starters\n *\n * The dispatcher in front refuses an unverified account and a lockdown too;\n * the verification check is repeated here so the route stands on its own.\n *\n * AN AI STARTER'S ANSWERS (AGL-4049 phase 4).\n *\n * `GET /api/marketplace/starters/answers?orgId=&listingId=` answers\n * `{ answers, facets }`: what an AI starter carries to fill the AI start's\n * questions once a person picks it. Behind the same gates in the same order\n * (its 400 is a listing id no listing can have), then the seam's `resolve`,\n * which is where a priced starter is held to its purchase:\n *\n * 402 a priced starter the workspace has not bought, with its price and\n * `code: purchase_required`, as the create route words it\n * 403 404 passed on from the source, naming the listing\n * 409 a starter site, which is created as it is and asks nothing\n *\n * The answers and the facets only: never the starter's pages, its layout or\n * its theme, which the site is created from, not the questions.\n */\n\nconst NO_STORE = { 'Cache-Control': 'no-store' }\n\nconst answer = (body: unknown, status = 200): Response =>\n Response.json(body, { status, headers: NO_STORE })\n\nconst refuse = (status: number, error: string): Response => answer({ error }, status)\n\n/** The create route's words for the same flag, so the two doors never disagree. */\nexport const SITE_STARTERS_UNRELEASED = 'Site starters are not available for this workspace yet'\n\n/** And for a deployment where no plugin serves starters. */\nexport const SITE_STARTERS_UNAVAILABLE = 'Starters are unavailable right now. Try again in a moment.'\n\n/**\n * The names a card and a filter show for a starter's ids: each built-in\n * theme by the name the theme picker gives its preset, and each font pairing\n * by its families (\"Inter\", or \"Fraunces & Inter\" when they differ). A base\n * whose preset no plugin registered in this process is left out, and the\n * surface shows nothing for it rather than an id.\n */\nexport function starterSearchLabels(): StarterSearchLabels {\n const presets = listServerThemePresets()\n const base: Record<string, string> = {}\n for (const id of SITE_THEME_BASES) {\n const name = presets.find((preset) => preset?.id === siteStarterPresetId(id))?.name\n if (typeof name === 'string' && name) base[id] = name\n }\n const fonts: Record<string, string> = {}\n for (const pairing of SITE_FONT_PAIRINGS) {\n fonts[pairing.id] =\n pairing.heading.family === pairing.body.family\n ? pairing.heading.family\n : `${pairing.heading.family} & ${pairing.body.family}`\n }\n return { base, fonts }\n}\n\n/**\n * The source, asking the app to run its boot declarations once when nobody\n * registered one — a boot whose declarations failed leaves the seam empty,\n * and the create route repairs the same way.\n */\nasync function starterSource(): Promise<PluginSiteStarterSource | null> {\n const registered = resolveSiteStarterSource()\n if (registered) return registered\n await runPluginDeclarationsRepair().catch((error: unknown) => {\n console.error('[marketplace/starters] plugin declarations failed', error)\n })\n return resolveSiteStarterSource()\n}\n\n/** A verified caller, as the token says it. */\ntype Caller = { uid: string } & Record<string, unknown>\n\n/**\n * Both doors' first gates and their failure handling: GET only, a bearer\n * token the verifier accepts, a verified address (impersonation exempt).\n * `run` answers the rest; whatever it throws is a 401 for a credential the\n * verifier refused (AGL-1993) and a 500 in `failed`'s words otherwise.\n */\nasync function asVerifiedCaller(\n request: Request,\n failed: { log: string; error: string },\n run: (caller: Caller) => Promise<Response>,\n): Promise<Response> {\n if (request.method !== 'GET') return refuse(405, 'Method not allowed')\n const authorization = request.headers.get('authorization') ?? ''\n if (!authorization.startsWith('Bearer ')) return refuse(401, 'Unauthenticated')\n try {\n const decoded = await firebaseAdmin\n .app()\n .auth()\n .verifyIdToken(authorization.slice('Bearer '.length))\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n return await run(decoded as unknown as Caller)\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours (AGL-1993);\n // anything else keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(failed.log, error)\n return refuse(500, failed.error)\n }\n}\n\n/**\n * The gates after the question is read, in order: a member of the workspace\n * named, holding \"Create sites\", the release flag on for it (staff pass\n * without it being asked), and a plugin that serves starters. Answers the\n * viewer every source call is made for — the membership's workspace, never\n * the query's — and the source, or the refusal.\n */\nasync function admitViewer(\n caller: Caller,\n askedOrgId: string,\n): Promise<{ viewer: SiteStarterViewer; source: PluginSiteStarterSource } | Response> {\n // The membership's org, never the query's: every gate below asks about\n // the workspace the caller actually belongs to.\n const membership = await resolveOrgMembership(caller.uid, askedOrgId)\n if (!membership) return refuse(403, 'You are not a member of that organization')\n const orgId = membership.orgId\n if (!(await memberHasOrgPermission(orgId, membership.member, 'hosts.create'))) {\n return refuse(403, permissionRefusalText('hosts.create'))\n }\n if (\n caller['staff'] !== true &&\n !(await isServerReleaseFlagOnForOrg('release_site_starters', orgId))\n ) {\n return refuse(403, SITE_STARTERS_UNRELEASED)\n }\n const source = await starterSource()\n if (!source) return refuse(503, SITE_STARTERS_UNAVAILABLE)\n return { viewer: { orgId, uid: caller.uid }, source }\n}\n\nasync function handler(request: Request): Promise<Response> {\n return asVerifiedCaller(\n request,\n { log: '[marketplace/starters] search failed', error: 'Starters could not be loaded' },\n async (caller) => {\n const asked = starterQueryFrom(new URL(request.url).searchParams)\n if ('error' in asked) return refuse(400, asked.error)\n const admitted = await admitViewer(caller, asked.orgId)\n if (admitted instanceof Response) return admitted\n const { viewer, source } = admitted\n const found = await source.search(\n { word: asked.word, filter: asked.filter, limit: asked.limit },\n viewer,\n )\n const rows: SiteStarterSummary[] = []\n // The restored pick first, so the card the person chose is the one they\n // see, wherever the search would have put it, or left it out.\n const included = asked.include\n ? readStarterSummary(await describeSiteStarter(source, { listingId: asked.include }, viewer))\n : null\n if (included) rows.push(included)\n for (const value of Array.isArray(found) ? found : []) {\n const row = readStarterSummary(value)\n if (row && row.listingId !== included?.listingId) rows.push(row)\n if (rows.length >= asked.limit + (included ? 1 : 0)) break\n }\n const body: StarterSearchAnswer = { rows, labels: starterSearchLabels() }\n return answer(body)\n },\n )\n}\n\n/** What the answers door says of a starter site, which asks nothing. */\nexport const STARTER_HAS_NO_QUESTIONS = 'This starter is a ready-made site, with no questions to answer'\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\nasync function answersHandler(request: Request): Promise<Response> {\n return asVerifiedCaller(\n request,\n { log: '[marketplace/starters/answers] read failed', error: 'This starter could not be read' },\n async (caller) => {\n const asked = starterAnswersQueryFrom(new URL(request.url).searchParams)\n if ('error' in asked) return refuse(400, asked.error)\n const admitted = await admitViewer(caller, asked.orgId)\n if (admitted instanceof Response) return admitted\n const { viewer, source } = admitted\n const listingId = asked.listingId\n const resolved = await source.resolve({ listingId }, viewer)\n if (!resolved || typeof resolved !== 'object') {\n return answer({ error: 'That starter is no longer available', listingId }, 404)\n }\n if ('error' in resolved) {\n const status = resolved.status === 402 || resolved.status === 403 ? resolved.status : 404\n return answer(\n {\n error: resolved.error,\n ...(status === 402 ? { code: 'purchase_required' } : {}),\n listingId,\n ...(status === 402 && typeof resolved.priceUsd === 'number' ? { priceUsd: resolved.priceUsd } : {}),\n },\n status,\n )\n }\n if (resolved.mode !== 'ai') return answer({ error: STARTER_HAS_NO_QUESTIONS, listingId }, 409)\n // `resolve` refuses an AI starter without them; checked again, so a\n // source that skipped the check hands over nothing.\n if (!isRecord(resolved.answers)) return answer({ error: 'That starter is no longer available', listingId }, 404)\n const body: StarterAnswersAnswer = {\n answers: resolved.answers,\n facets: readStarterSummary({ listingId, facets: resolved.facets })?.facets ?? resolved.facets,\n }\n return answer(body)\n },\n )\n}\n\nexport { handler as marketplaceStarters, answersHandler as marketplaceStarterAnswers }\n"],"names":["permissionRefusalText","SITE_FONT_PAIRINGS","SITE_THEME_BASES","siteStarterPresetId","runPluginDeclarationsRepair","describeSiteStarter","resolveSiteStarterSource","listServerThemePresets","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","isServerReleaseFlagOnForOrg","memberHasOrgPermission","resolveOrgMembership","invalidIdTokenResponse","readStarterSummary","starterAnswersQueryFrom","starterQueryFrom","NO_STORE","answer","body","status","Response","json","headers","refuse","error","SITE_STARTERS_UNRELEASED","SITE_STARTERS_UNAVAILABLE","starterSearchLabels","presets","base","id","name","find","preset","fonts","pairing","heading","family","starterSource","registered","catch","console","asVerifiedCaller","request","failed","run","method","authorization","get","startsWith","decoded","app","auth","verifyIdToken","slice","length","email_verified","unauthenticated","log","admitViewer","caller","askedOrgId","membership","uid","orgId","member","source","viewer","handler","asked","URL","url","searchParams","admitted","found","search","word","filter","limit","rows","included","include","listingId","push","value","Array","isArray","row","labels","STARTER_HAS_NO_QUESTIONS","isRecord","answersHandler","resolved","resolve","code","priceUsd","mode","answers","facets","marketplaceStarters","marketplaceStarterAnswers"],"mappings":"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"}