@aglyn/plugins-marketplace 1.0.0-beta.285 → 1.0.0-beta.288
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +37 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/declarations.server.d.ts +2 -1
- package/src/lib/declarations.server.js +18 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/marketplace-site-permissions.d.ts +26 -0
- package/src/lib/model/marketplace-site-permissions.js +62 -0
- package/src/lib/model/marketplace-site-permissions.js.map +1 -0
- package/src/lib/model/marketplace.d.ts +24 -0
- package/src/lib/model/marketplace.js.map +1 -1
- package/src/lib/model/site-starter-listing.d.ts +49 -0
- package/src/lib/model/site-starter-listing.js +75 -0
- package/src/lib/model/site-starter-listing.js.map +1 -0
- package/src/lib/server/install-email-starter.js +7 -4
- package/src/lib/server/install-email-starter.js.map +1 -1
- package/src/lib/server/install-email-template.js +8 -5
- package/src/lib/server/install-email-template.js.map +1 -1
- package/src/lib/server/install-layout.js +7 -4
- package/src/lib/server/install-layout.js.map +1 -1
- package/src/lib/server/install-plugin.js +8 -8
- package/src/lib/server/install-plugin.js.map +1 -1
- package/src/lib/server/install-template.js +7 -4
- package/src/lib/server/install-template.js.map +1 -1
- package/src/lib/server/install-theme.js +10 -7
- package/src/lib/server/install-theme.js.map +1 -1
- package/src/lib/server/install.js +7 -4
- package/src/lib/server/install.js.map +1 -1
- package/src/lib/server/publish-email-starter.js +8 -5
- package/src/lib/server/publish-email-starter.js.map +1 -1
- package/src/lib/server/publish-email-template.js +8 -5
- package/src/lib/server/publish-email-template.js.map +1 -1
- package/src/lib/server/publish-layout.js +8 -5
- package/src/lib/server/publish-layout.js.map +1 -1
- package/src/lib/server/publish-starter.d.ts +37 -0
- package/src/lib/server/publish-starter.js +501 -0
- package/src/lib/server/publish-starter.js.map +1 -0
- package/src/lib/server/publish-template.js +9 -6
- package/src/lib/server/publish-template.js.map +1 -1
- package/src/lib/server/publish-theme.js +8 -5
- package/src/lib/server/publish-theme.js.map +1 -1
- package/src/lib/server/publish.js +8 -5
- package/src/lib/server/publish.js.map +1 -1
- package/src/lib/server/site-starter-source.d.ts +19 -0
- package/src/lib/server/site-starter-source.js +189 -0
- package/src/lib/server/site-starter-source.js.map +1 -0
- package/src/lib/server.js +3 -0
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-template.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { artifactCreateListKeys, artifactDeleteListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n checkQuota,\n createResourceUid,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { TEMPLATE_SOURCE_TYPE } from '../constants/template-source'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\n\n/**\n * Installs a site template into a host's TEMPLATE LIBRARY (AGL-137,\n * reworked by AGL-669).\n *\n * This used to instantiate screens and write routing-map entries in the\n * same call, which made installed pages public the instant you clicked\n * install — browsing the marketplace could publish to a production site by\n * mis-click. Now it only writes to `hosts/{hostId}/templates`; creating\n * pages is a separate, deliberate step (AGL-670).\n *\n * The snapshot's screens become one page template each, sharing a\n * `source.listingId` so the library can group them and the user can pick\n * which ones to use. The theme is carried on the templates rather than\n * applied — a theme change is site-wide and instant, the same class of\n * surprise this removed.\n *\n * Access is unchanged: free, purchased, or your own listing. Still\n * server-side because version snapshots are not client-readable.\n */\nexport const installTemplateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n // `applyTheme` is accepted and ignored (AGL-669): installing no longer\n // touches the host doc, so there is nothing to apply. Kept in the\n // signature so existing callers don't break.\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'template'\n ) {\n return res.status(404).json({ error: 'Unknown template' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // The publisher installs their own listing for free. Org-owned now\n // (AGL-652), so this is a role check — comparing a uid to an org id\n // would never match and would charge publishers for their own work.\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const template = versionSnapshot.get('template') as any\n const screens: any[] = Array.isArray(template?.screens)\n ? template.screens\n : []\n if (!screens.length) {\n return res.status(500).json({ error: 'Template version missing' })\n }\n\n // Template-library quota, not screensPerHost: nothing becomes a screen\n // here. The screen cap is enforced later, when pages are actually\n // created from these (AGL-670). Enforced for every org, since a\n // plan-less org resolves as `free` (not unmetered).\n const org = (await getOrgForHost(hostId))?.org\n const templatesRef = hostRef.collection('templates')\n /**\n * The library slots this install would spend, off a set of template docs.\n *\n * Templates from THIS listing are about to be replaced, so counting them\n * would make a re-install look like it doubles the library and fail the\n * quota on an update the user is entitled to. Platform-seeded starters are\n * excluded for the same reason the resources route excludes them\n * (AGL-687): they are not the user's spend of their template allowance.\n *\n * A function rather than an inlined filter because the count is now taken\n * TWICE — once here as a fast refusal, once inside the transaction as the\n * authority — and two copies of a counting rule is how the rule drifts.\n */\n const slotsAfterInstall = (\n docs: Array<FirebaseFirestore.QueryDocumentSnapshot>,\n ) =>\n docs.filter(\n (entry) =>\n !entry.get('deletedAt') &&\n entry.get('source.type') !== 'starter' &&\n entry.get('source.listingId') !== listingId,\n ).length +\n screens.length -\n 1\n const overQuota = (limit: number) =>\n `This template adds ${screens.length} template${\n screens.length === 1 ? '' : 's'\n } to your library — your plan allows ${limit}. ` +\n 'See Billing to upgrade.'\n {\n // The fast refusal. NOT the enforcement point — see the transaction\n // below — but it keeps the base-snapshot write and the version tally off\n // the path of an install that was never going to be allowed, and it is\n // what answers a caller who is simply over their limit.\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(await templatesRef.get().then((snap) => snap.docs)),\n )\n if (!quota.allowed) {\n return res.status(403).json({ error: overQuota(quota.limit) })\n }\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Shared across the bundle so the library can group these as one\n // install and offer them together.\n const source = {\n type: TEMPLATE_SOURCE_TYPE,\n listingId,\n version: listing.latestVersion ?? null,\n }\n // Provenance + base snapshot (AGL-1015). One snapshot for the whole\n // bundle, matching how the bundle is replaced: the published screens carry\n // no stable identity, so per-template bases would key on nothing and could\n // not be paired back up on update.\n //\n // Stays OUTSIDE the transaction below: it does its own read and write, and\n // repeating those on every retry attempt is exactly what a transaction\n // body must not do. That is safe because the base collection is\n // content-addressed and shared — a race that ends in a refusal leaves at\n // most a snapshot of a published version, keyed by its own hash, which the\n // next successful install of that version reuses.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'template',\n content: { screens, ...(template.theme ? { theme: template.theme } : {}) },\n })\n /**\n * Re-installing the same listing REPLACES its previous bundle rather than\n * stacking a second copy (AGL-671) — that is what makes \"Update available\"\n * actionable with no separate refresh route. Matching old templates to new\n * ones individually is not possible: a published snapshot's screens carry\n * no stable identity, only a displayName and slug, so any pairing would be\n * a guess. Replacing the bundle wholesale is honest about that. Pages\n * already created from the old templates are untouched — they are ordinary\n * screens with no link back.\n *\n * THE ENFORCEMENT POINT: the count, the decision and every write in ONE\n * transaction (AGL-2371, the AGL-2231 treatment).\n *\n * The check above ran, then `recordInstallProvenance` awaited, then a\n * second query awaited, and only then did a `WriteBatch` commit. Every\n * await is a yield, so N concurrent installs each read the same pre-count,\n * each found room, and each landed — and nothing re-counts afterwards, so\n * a free plan's ten templates became two hundred by clicking install\n * twenty times. A batch is atomic but NOT conditional on a read taken\n * before it, which is the same lesson AGL-2369 paid for one route over.\n *\n * `tx.get` on the templates collection takes a pessimistic lock on every\n * document it matched, so the loser of a race retries, re-reads the higher\n * count and is refused.\n *\n * The `source.listingId` query is GONE: the superseded bundle is a subset\n * of the rows just counted, so deriving it here costs no read and — more to\n * the point — cannot disagree with the count the decision was made from.\n * That disagreement was its own latent bug: the count said one thing about\n * the library and the replacement set was fetched from a later state.\n *\n * A refusal comes back as data and is rendered outside. Building a response\n * inside a body that can run several times reads as if the transaction were\n * a place effects happen.\n *\n * The 500-write ceiling is unchanged: the `WriteBatch` this replaces\n * carried the same replacements and the same creates and had the same\n * limit, so a bundle too large to commit was already too large. That is\n * why this is not AGL-2370 — there the import chunks PAST 500 deliberately\n * and cannot be wrapped at all.\n */\n const outcome = await firestore.runTransaction<\n | { error: string }\n | { created: number; replaced: number; from: string | number | null }\n >(async (tx) => {\n const live = await tx.get(templatesRef)\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(live.docs),\n )\n if (!quota.allowed) return { error: overQuota(quota.limit) }\n\n // ALL READS BEFORE THE WRITES, which Firestore requires.\n const superseded = live.docs.filter(\n (entry) => entry.get('source.listingId') === listingId,\n )\n let replaced = 0\n for (const stale of superseded) {\n if (stale.get('deletedAt')) continue\n tx.update(stale.ref, {\n deletedAt: now,\n updatedAt: now,\n ...artifactDeleteListKeys('templates'),\n })\n replaced += 1\n }\n let created = 0\n for (const screen of screens) {\n const pageName = String(screen.displayName ?? 'Page').slice(0, 80)\n tx.set(templatesRef.doc(createResourceUid()), {\n kind: 'page',\n displayName: pageName,\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n // A suggestion only — de-conflicted against the routing map when a\n // page is actually created from this.\n ...(screen.slug && { slug: String(screen.slug) }),\n // Compressed at rest, matching what the template converter writes\n // on every save after this install (AGL-1151). A listing version\n // holds the tree as a plain map; `encodeStoredNodes` passes an\n // already-encoded one through, so this cannot double-encode if the\n // listing form ever changes.\n nodes: Buffer.from(encodeStoredNodes(screen.nodes ?? {})!),\n // Carried, not applied: see the note on AglynTemplate.theme. The\n // `applyTheme` request flag is now meaningless here — nothing is\n // applied at install — so the theme always travels with the\n // template and the decision moves to whoever uses it.\n ...(template.theme ? { theme: template.theme } : {}),\n source,\n installedFrom: provenance.installedFrom,\n // The keys the templates library finds it by (AGL-3321).\n ...artifactCreateListKeys('templates', {\n kind: 'page',\n displayName: pageName,\n source,\n }),\n createdAt: now,\n updatedAt: now,\n })\n created += 1\n }\n // Per-version tally input (AGL-1036). A bundle is one install however\n // many templates it lands, so the version it left is read off the first\n // superseded doc rather than counted per template — and off the docs the\n // transaction actually replaced, not a snapshot taken beside it.\n return {\n created,\n replaced,\n from:\n superseded[0]?.get('installedFrom.version') ??\n superseded[0]?.get('source.version') ??\n null,\n }\n })\n if ('error' in outcome) {\n return res.status(403).json({ error: outcome.error })\n }\n const { created, replaced } = outcome\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'template',\n from: outcome.from,\n to: listing.latestVersion,\n })\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n\n return res.status(200).json({\n installed: true,\n templates: created,\n /** Prior templates from this listing, superseded by this install. */\n replaced,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n // Retained so an older client doesn't render \"Added undefined screens\".\n screens: 0,\n // `themeApplied: false` used to ride along here and is GONE (AGL-2339).\n // It had zero references anywhere in the repo — not a client, not a\n // spec, not a type — so unlike `screens` above it was not back-compat\n // for anything. A hardcoded `false` returned as a result is worse than\n // an absent field: it reads as a measured fact, and the first caller to\n // trust it would have been told a template install never applies a\n // theme by something that never looked.\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Template install failed' })\n }\n}\n"],"names":["artifactCreateListKeys","artifactDeleteListKeys","checkQuota","createResourceUid","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","canActAsPublisher","requirePurchase","recordInstallProvenance","TEMPLATE_SOURCE_TYPE","recordVersionMove","isPublisherSecurityLocked","isPrivateListing","listingArtifactType","installTemplateHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","template","screens","Array","isArray","org","templatesRef","slotsAfterInstall","docs","filter","entry","overQuota","limit","quota","then","snap","allowed","now","FieldValue","serverTimestamp","source","type","version","provenance","artifactType","content","theme","outcome","runTransaction","tx","superseded","live","replaced","stale","update","ref","updatedAt","created","screen","pageName","displayName","set","kind","description","seo","slug","nodes","Buffer","from","installedFrom","createdAt","to","installCount","increment","catch","installed","templates","baseStored","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-template.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { artifactCreateListKeys, artifactDeleteListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n checkQuota,\n createResourceUid,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { TEMPLATE_SOURCE_TYPE } from '../constants/template-source'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\n\n/**\n * Installs a site template into a host's TEMPLATE LIBRARY (AGL-137,\n * reworked by AGL-669).\n *\n * This used to instantiate screens and write routing-map entries in the\n * same call, which made installed pages public the instant you clicked\n * install — browsing the marketplace could publish to a production site by\n * mis-click. Now it only writes to `hosts/{hostId}/templates`; creating\n * pages is a separate, deliberate step (AGL-670).\n *\n * The snapshot's screens become one page template each, sharing a\n * `source.listingId` so the library can group them and the user can pick\n * which ones to use. The theme is carried on the templates rather than\n * applied — a theme change is site-wide and instant, the same class of\n * surprise this removed.\n *\n * Access is unchanged: free, purchased, or your own listing. Still\n * server-side because version snapshots are not client-readable.\n */\nexport const installTemplateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n // `applyTheme` is accepted and ignored (AGL-669): installing no longer\n // touches the host doc, so there is nothing to apply. Kept in the\n // signature so existing callers don't break.\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from 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 // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'template'\n ) {\n return res.status(404).json({ error: 'Unknown template' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // The publisher installs their own listing for free. Org-owned now\n // (AGL-652), so this is a role check — comparing a uid to an org id\n // would never match and would charge publishers for their own work.\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const template = versionSnapshot.get('template') as any\n const screens: any[] = Array.isArray(template?.screens)\n ? template.screens\n : []\n if (!screens.length) {\n return res.status(500).json({ error: 'Template version missing' })\n }\n\n // Template-library quota, not screensPerHost: nothing becomes a screen\n // here. The screen cap is enforced later, when pages are actually\n // created from these (AGL-670). Enforced for every org, since a\n // plan-less org resolves as `free` (not unmetered).\n const org = (await getOrgForHost(hostId))?.org\n const templatesRef = hostRef.collection('templates')\n /**\n * The library slots this install would spend, off a set of template docs.\n *\n * Templates from THIS listing are about to be replaced, so counting them\n * would make a re-install look like it doubles the library and fail the\n * quota on an update the user is entitled to. Platform-seeded starters are\n * excluded for the same reason the resources route excludes them\n * (AGL-687): they are not the user's spend of their template allowance.\n *\n * A function rather than an inlined filter because the count is now taken\n * TWICE — once here as a fast refusal, once inside the transaction as the\n * authority — and two copies of a counting rule is how the rule drifts.\n */\n const slotsAfterInstall = (\n docs: Array<FirebaseFirestore.QueryDocumentSnapshot>,\n ) =>\n docs.filter(\n (entry) =>\n !entry.get('deletedAt') &&\n entry.get('source.type') !== 'starter' &&\n entry.get('source.listingId') !== listingId,\n ).length +\n screens.length -\n 1\n const overQuota = (limit: number) =>\n `This template adds ${screens.length} template${\n screens.length === 1 ? '' : 's'\n } to your library — your plan allows ${limit}. ` +\n 'See Billing to upgrade.'\n {\n // The fast refusal. NOT the enforcement point — see the transaction\n // below — but it keeps the base-snapshot write and the version tally off\n // the path of an install that was never going to be allowed, and it is\n // what answers a caller who is simply over their limit.\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(await templatesRef.get().then((snap) => snap.docs)),\n )\n if (!quota.allowed) {\n return res.status(403).json({ error: overQuota(quota.limit) })\n }\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Shared across the bundle so the library can group these as one\n // install and offer them together.\n const source = {\n type: TEMPLATE_SOURCE_TYPE,\n listingId,\n version: listing.latestVersion ?? null,\n }\n // Provenance + base snapshot (AGL-1015). One snapshot for the whole\n // bundle, matching how the bundle is replaced: the published screens carry\n // no stable identity, so per-template bases would key on nothing and could\n // not be paired back up on update.\n //\n // Stays OUTSIDE the transaction below: it does its own read and write, and\n // repeating those on every retry attempt is exactly what a transaction\n // body must not do. That is safe because the base collection is\n // content-addressed and shared — a race that ends in a refusal leaves at\n // most a snapshot of a published version, keyed by its own hash, which the\n // next successful install of that version reuses.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'template',\n content: { screens, ...(template.theme ? { theme: template.theme } : {}) },\n })\n /**\n * Re-installing the same listing REPLACES its previous bundle rather than\n * stacking a second copy (AGL-671) — that is what makes \"Update available\"\n * actionable with no separate refresh route. Matching old templates to new\n * ones individually is not possible: a published snapshot's screens carry\n * no stable identity, only a displayName and slug, so any pairing would be\n * a guess. Replacing the bundle wholesale is honest about that. Pages\n * already created from the old templates are untouched — they are ordinary\n * screens with no link back.\n *\n * THE ENFORCEMENT POINT: the count, the decision and every write in ONE\n * transaction (AGL-2371, the AGL-2231 treatment).\n *\n * The check above ran, then `recordInstallProvenance` awaited, then a\n * second query awaited, and only then did a `WriteBatch` commit. Every\n * await is a yield, so N concurrent installs each read the same pre-count,\n * each found room, and each landed — and nothing re-counts afterwards, so\n * a free plan's ten templates became two hundred by clicking install\n * twenty times. A batch is atomic but NOT conditional on a read taken\n * before it, which is the same lesson AGL-2369 paid for one route over.\n *\n * `tx.get` on the templates collection takes a pessimistic lock on every\n * document it matched, so the loser of a race retries, re-reads the higher\n * count and is refused.\n *\n * The `source.listingId` query is GONE: the superseded bundle is a subset\n * of the rows just counted, so deriving it here costs no read and — more to\n * the point — cannot disagree with the count the decision was made from.\n * That disagreement was its own latent bug: the count said one thing about\n * the library and the replacement set was fetched from a later state.\n *\n * A refusal comes back as data and is rendered outside. Building a response\n * inside a body that can run several times reads as if the transaction were\n * a place effects happen.\n *\n * The 500-write ceiling is unchanged: the `WriteBatch` this replaces\n * carried the same replacements and the same creates and had the same\n * limit, so a bundle too large to commit was already too large. That is\n * why this is not AGL-2370 — there the import chunks PAST 500 deliberately\n * and cannot be wrapped at all.\n */\n const outcome = await firestore.runTransaction<\n | { error: string }\n | { created: number; replaced: number; from: string | number | null }\n >(async (tx) => {\n const live = await tx.get(templatesRef)\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(live.docs),\n )\n if (!quota.allowed) return { error: overQuota(quota.limit) }\n\n // ALL READS BEFORE THE WRITES, which Firestore requires.\n const superseded = live.docs.filter(\n (entry) => entry.get('source.listingId') === listingId,\n )\n let replaced = 0\n for (const stale of superseded) {\n if (stale.get('deletedAt')) continue\n tx.update(stale.ref, {\n deletedAt: now,\n updatedAt: now,\n ...artifactDeleteListKeys('templates'),\n })\n replaced += 1\n }\n let created = 0\n for (const screen of screens) {\n const pageName = String(screen.displayName ?? 'Page').slice(0, 80)\n tx.set(templatesRef.doc(createResourceUid()), {\n kind: 'page',\n displayName: pageName,\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n // A suggestion only — de-conflicted against the routing map when a\n // page is actually created from this.\n ...(screen.slug && { slug: String(screen.slug) }),\n // Compressed at rest, matching what the template converter writes\n // on every save after this install (AGL-1151). A listing version\n // holds the tree as a plain map; `encodeStoredNodes` passes an\n // already-encoded one through, so this cannot double-encode if the\n // listing form ever changes.\n nodes: Buffer.from(encodeStoredNodes(screen.nodes ?? {})!),\n // Carried, not applied: see the note on AglynTemplate.theme. The\n // `applyTheme` request flag is now meaningless here — nothing is\n // applied at install — so the theme always travels with the\n // template and the decision moves to whoever uses it.\n ...(template.theme ? { theme: template.theme } : {}),\n source,\n installedFrom: provenance.installedFrom,\n // The keys the templates library finds it by (AGL-3321).\n ...artifactCreateListKeys('templates', {\n kind: 'page',\n displayName: pageName,\n source,\n }),\n createdAt: now,\n updatedAt: now,\n })\n created += 1\n }\n // Per-version tally input (AGL-1036). A bundle is one install however\n // many templates it lands, so the version it left is read off the first\n // superseded doc rather than counted per template — and off the docs the\n // transaction actually replaced, not a snapshot taken beside it.\n return {\n created,\n replaced,\n from:\n superseded[0]?.get('installedFrom.version') ??\n superseded[0]?.get('source.version') ??\n null,\n }\n })\n if ('error' in outcome) {\n return res.status(403).json({ error: outcome.error })\n }\n const { created, replaced } = outcome\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'template',\n from: outcome.from,\n to: listing.latestVersion,\n })\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n\n return res.status(200).json({\n installed: true,\n templates: created,\n /** Prior templates from this listing, superseded by this install. */\n replaced,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n // Retained so an older client doesn't render \"Added undefined screens\".\n screens: 0,\n // `themeApplied: false` used to ride along here and is GONE (AGL-2339).\n // It had zero references anywhere in the repo — not a client, not a\n // spec, not a type — so unlike `screens` above it was not back-compat\n // for anything. A hardcoded `false` returned as a result is worse than\n // an absent field: it reads as a measured fact, and the first caller to\n // trust it would have been told a template install never applies a\n // theme by something that never looked.\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Template install failed' })\n }\n}\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","artifactCreateListKeys","artifactDeleteListKeys","checkQuota","createResourceUid","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","canActAsPublisher","requirePurchase","recordInstallProvenance","TEMPLATE_SOURCE_TYPE","recordVersionMove","isPublisherSecurityLocked","isPrivateListing","listingArtifactType","installTemplateHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","template","screens","Array","isArray","org","templatesRef","slotsAfterInstall","docs","filter","entry","overQuota","limit","quota","then","snap","allowed","now","FieldValue","serverTimestamp","source","type","version","provenance","artifactType","content","theme","outcome","runTransaction","tx","superseded","live","replaced","stale","update","ref","updatedAt","created","screen","pageName","displayName","set","kind","description","seo","slug","nodes","Buffer","from","installedFrom","createdAt","to","installCount","increment","catch","installed","templates","baseStored","console"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -13,7 +14,8 @@
|
|
|
13
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
15
|
* See the License for the specific language governing permissions and
|
|
15
16
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
17
|
+
*/ import { holdsStampedSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
18
|
+
import { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from "../model/marketplace-site-permissions.js";
|
|
17
19
|
import { describeTheme } from "@aglyn/aglyn/app-utils/site-theme";
|
|
18
20
|
import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
|
|
19
21
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
@@ -78,7 +80,7 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
|
|
|
78
80
|
error: 'Unauthenticated'
|
|
79
81
|
});
|
|
80
82
|
try {
|
|
81
|
-
var _hostSnapshot_get,
|
|
83
|
+
var _hostSnapshot_get, _listing_priceUsd, _membership_orgId, _hostSnapshot_get1, _listing_latestVersion;
|
|
82
84
|
var _req_body3;
|
|
83
85
|
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
84
86
|
const membership = await resolveOrgPermissions(decoded.uid, {
|
|
@@ -97,14 +99,15 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
|
|
|
97
99
|
error: 'Unknown site'
|
|
98
100
|
});
|
|
99
101
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
// "Install marketplace items into a site" (AGL-4047): the site's stamp, else
|
|
103
|
+
// the preset of the member's site role, the Admin and Editor this asked for.
|
|
104
|
+
if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {
|
|
102
105
|
return res.status(403).json({
|
|
103
|
-
error: '
|
|
106
|
+
error: marketplaceSitePermissionRefusal('marketplace.siteInstall')
|
|
104
107
|
});
|
|
105
108
|
}
|
|
106
109
|
const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
|
|
107
|
-
const currentTheme = (
|
|
110
|
+
const currentTheme = (_hostSnapshot_get = hostSnapshot.get('theme')) != null ? _hostSnapshot_get : null;
|
|
108
111
|
// Awaited before the response, because a serverless function may be
|
|
109
112
|
// frozen once it has answered. Never throws: a drop that fails leaves the
|
|
110
113
|
// theme saved and the hour-long cache as the backstop.
|
|
@@ -280,7 +283,7 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
|
|
|
280
283
|
artifactType: 'theme',
|
|
281
284
|
content: theme
|
|
282
285
|
});
|
|
283
|
-
const previousVersion = (
|
|
286
|
+
const previousVersion = (_hostSnapshot_get1 = hostSnapshot.get('themeInstalledFrom.version')) != null ? _hostSnapshot_get1 : null;
|
|
284
287
|
// The theme joins the site's library and is picked (AGL-3404). The theme
|
|
285
288
|
// it replaces is filed in the library with its edits, so installing is
|
|
286
289
|
// reversible by picking it again — the job `themeReplaced` used to do
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-theme.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { describeTheme } from '@aglyn/aglyn/app-utils/site-theme'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { runThemeLibraryAction } from '@aglyn/tenant-data-admin/server/theme-library-write'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs a marketplace theme onto a site (AGL-1020).\n *\n * Themes are the exception to \"installing never changes a running site\". A\n * template or a layout can land inert in a library because it is one thing\n * among many; a theme IS the site's appearance, and installing one into a\n * library where it does nothing would be a control that appears to work and\n * does not. So this writes `hosts/{hostId}.theme` and the site repaints.\n *\n * Which makes reversibility the requirement, not a nicety. Two things provide\n * it, and neither is a confirmation dialog:\n *\n * * The theme being replaced is kept verbatim in `themeReplaced`, so `revert`\n * restores exactly what was there — including a hand-built theme that was\n * never a marketplace artifact and exists nowhere else.\n * * `reset` clears the theme entirely, which is the \"use the default theme\"\n * the issue asks to keep first-class. It is a distinct action from revert\n * because \"back to how it was\" and \"back to stock\" are different intents and\n * guessing between them is how people lose work.\n *\n * `preview` changes nothing and returns what the swap would do, so the\n * confirmation can name it rather than showing a JSON blob.\n *\n * Every action that writes the theme then drops the site's cached pages\n * (AGL-3386). The theme styles every page and nothing publishes it, so\n * without the drop \"the site repaints\" meant \"within the hour\".\n */\nexport const installThemeHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const action = ['revert', 'reset', 'preview', 'clear-overrides'].includes(\n req.body?.action,\n )\n ? (req.body.action as\n | 'revert'\n | 'reset'\n | 'preview'\n | 'clear-overrides')\n : 'install'\n const listingId = String(req.body?.listingId ?? '')\n if (!hostId || (action === 'install' && !listingId)) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const currentTheme = hostSnapshot.get('theme') ?? null\n // Awaited before the response, because a serverless function may be\n // frozen once it has answered. Never throws: a drop that fails leaves the\n // theme saved and the hour-long cache as the backstop.\n const repaintLiveSite = (action: string) =>\n dropPluginSiteCache({\n hostIds: [hostId],\n reason: `marketplace theme ${action}`,\n })\n\n // ---- reset: back to the platform default ----\n // Picking the default from the site's theme library (AGL-3404): the theme\n // being left is filed in the library, edits and all, so resetting stays\n // reversible without a second copy of it on the host.\n if (action === 'reset') {\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, {\n action: 'select',\n target: { kind: 'default' },\n }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n await repaintLiveSite('reset')\n return res.status(200).json({ reset: true })\n }\n\n // ---- clear-overrides: drop this site's changes, keep the theme ----\n // The third way back, and the narrowest: \"reset to the publisher's\n // version\" is deleting the patch — the whole point of owning the patch\n // rather than the copy (AGL-1019). The theme itself is untouched.\n //\n // The library's `restore`, which keeps the site's own dark-scheme setting\n // (AGL-3404): that is a decision about the site, not an edit to the theme.\n if (action === 'clear-overrides') {\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, { action: 'restore' }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n await repaintLiveSite('overrides cleared')\n return res.status(200).json({ cleared: true })\n }\n\n // ---- revert: back to whatever this site had before the last swap ----\n if (action === 'revert') {\n const replaced = hostSnapshot.get('themeReplaced')\n if (!replaced) {\n return res.status(409).json({\n error: 'There is no previous theme to go back to on this site.',\n })\n }\n await hostRef.set(\n {\n theme: replaced.theme ?? firebaseAdmin.firestore.FieldValue.delete(),\n ...(replaced.installedFrom\n ? { themeInstalledFrom: replaced.installedFrom }\n : { themeInstalledFrom: firebaseAdmin.firestore.FieldValue.delete() }),\n // The overrides that were live alongside that theme come back with\n // it. Reverting the base and keeping a patch authored against the\n // theme being reverted FROM is the one combination nobody asked for.\n ...(replaced.override\n ? { themeOverride: replaced.override }\n : { themeOverride: firebaseAdmin.firestore.FieldValue.delete() }),\n themeReplaced: firebaseAdmin.firestore.FieldValue.delete(),\n // A `themeReplaced` predates the library (every library switch\n // clears it), so the theme it restores is read the way a\n // pre-library site's is — from the fields above, not a selection.\n themeSelection: firebaseAdmin.firestore.FieldValue.delete(),\n updatedAt: now,\n },\n { merge: true },\n )\n await repaintLiveSite('reverted')\n return res.status(200).json({ reverted: true })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'theme'\n ) {\n return res.status(404).json({ error: 'Unknown theme' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const theme = versionSnapshot.get('theme')\n if (!theme || !Object.keys(theme).length) {\n return res.status(500).json({ error: 'Theme version missing' })\n }\n\n // ---- preview: say what the swap changes, change nothing ----\n if (action === 'preview') {\n return res.status(200).json({\n incoming: describeTheme(theme),\n current: describeTheme(currentTheme),\n // A site with no theme is on the platform default, so there is nothing\n // to lose and the confirmation should not imply there is.\n replaces: Boolean(currentTheme && Object.keys(currentTheme).length),\n version: listing.latestVersion ?? null,\n })\n }\n\n // ---- install ----\n // Replacing a theme the workspace has edited destroys those edits (AGL-1018).\n // Unlike a component this is not recoverable by looking in a library — the\n // theme lives on one field on one document — so the refusal matters more.\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: hostSnapshot.get('themeInstalledFrom.sha256'),\n current: currentTheme,\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'This site’s theme has been edited since it was installed. Review ' +\n 'the update to see what would change.',\n diverged: true,\n })\n }\n\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'theme',\n content: theme,\n })\n const previousVersion =\n hostSnapshot.get('themeInstalledFrom.version') ?? null\n\n // The theme joins the site's library and is picked (AGL-3404). The theme\n // it replaces is filed in the library with its edits, so installing is\n // reversible by picking it again — the job `themeReplaced` used to do\n // for one theme, for every theme.\n //\n // Edits belong to the theme they were made on. They used to SURVIVE a\n // swap (AGL-1021), because the theme being replaced was about to exist\n // nowhere and carrying its patch forward was the lesser loss. With the\n // library keeping it, a patch tuned for one design no longer lands on\n // another: each theme gets its own back when it is picked again. An\n // UPDATE of the theme already picked keeps its patch exactly as stored,\n // hash included, so the editor can still say it predates this version.\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, {\n action: 'install',\n listingId,\n name: String(listing.displayName ?? '') || 'Marketplace theme',\n theme,\n installedFrom: { ...provenance.installedFrom },\n }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n\n await repaintLiveSite('installed')\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'theme',\n from: previousVersion,\n to: listing.latestVersion,\n })\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n\n return res.status(200).json({\n installed: true,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n applied: describeTheme(theme),\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Theme install failed' })\n }\n}\n\nexport default installThemeHandler\n"],"names":["describeTheme","dropPluginSiteCache","firebaseAdmin","runThemeLibraryAction","resolveOrgPermissions","isPrivateListing","listingArtifactType","canActAsPublisher","requirePurchase","hasDivergedFromBase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installThemeHandler","req","res","method","status","json","error","hostId","String","body","action","includes","listingId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","now","FieldValue","serverTimestamp","currentTheme","repaintLiveSite","hostIds","reason","plan","runTransaction","tx","target","kind","ok","reset","cleared","replaced","set","theme","delete","installedFrom","themeInstalledFrom","override","themeOverride","themeReplaced","themeSelection","updatedAt","merge","reverted","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","Object","keys","incoming","current","replaces","Boolean","version","diverged","sha256","mode","provenance","artifactType","content","previousVersion","name","displayName","from","to","update","installCount","increment","catch","installed","baseStored","applied","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,aAAa,QAAQ,oCAAmC;AACjE,SAASC,mBAAmB,QAAQ,gDAA+C;AACnF,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,qBAAqB,QAAQ,sDAAqD;AAC3F,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SACEC,gBAAgB,EAChBC,mBAAmB,QACd,0BAAsB;AAC7B,SAASC,iBAAiB,QAAQ,yBAAqB;AACvD,SAASC,eAAe,QAAQ,4BAAwB;AACxD,SAASC,mBAAmB,EAAEC,uBAAuB,QAAQ,kBAAc;AAC3E,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SAASC,yBAAyB,QAAQ,iBAAa;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMC,sBAAwC,OAAOC,KAAKC;qBAkBlCD;QAdPA,WAEpBA,YAQuBA;IAbzB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,SAASC,gBAAOP,YAAAA,IAAIQ,IAAI,qBAARR,UAAUM,MAAM,mBAAI;IAC1C,MAAMG,SAAS;QAAC;QAAU;QAAS;QAAW;KAAkB,CAACC,QAAQ,EACvEV,aAAAA,IAAIQ,IAAI,qBAARR,WAAUS,MAAM,IAEbT,IAAIQ,IAAI,CAACC,MAAM,GAKhB;IACJ,MAAME,YAAYJ,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUW,SAAS,oBAAI;IAChD,IAAI,CAACL,UAAWG,WAAW,aAAa,CAACE,WAAY;QACnD,OAAOV,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA8B;IACrE;IACA,MAAMO,gBAAgBL,QAAOP,6BAAAA,IAAIa,OAAO,CAACD,aAAa,YAAzBZ,6BAA6B;IAC1D,MAAMc,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS,OAAOb,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,IAAI;YAekBc,mBAKCA,oBAoGGC,mBA4BVC,mBAuDZF,oBA0CSC;YA5DKpB;QAxLhB,MAAMsB,UAAU,MAAMlC,cAAcmC,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACX;QAC/D,MAAMO,aAAa,MAAM/B,sBAAsBgC,QAAQI,GAAG,EAAE;YAAEpB;QAAO;QACrE,IAAI,CAACe,WAAWM,WAAW,CAACC,cAAc,EAAE;YAC1C,OAAO3B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OACE;YACJ;QACF;QACA,MAAMwB,YAAYzC,cAAcmC,GAAG,GAAGM,SAAS;QAC/C,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAAC1B;QAClD,MAAMa,eAAe,MAAMW,QAAQG,GAAG;QACtC,IAAI,CAACd,aAAae,MAAM,EAAE;YACxB,OAAOjC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAe;QACtD;QACA,MAAM8B,aAAa,EAAChB,oBAAAA,aAAac,GAAG,CAAC,0BAAjBd,oBAAmC,CAAC,EAAE,CAACG,QAAQI,GAAG,CAAC;QACvE,IAAIS,eAAe,WAAWA,eAAe,UAAU;YACrD,OAAOlC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA6B;QACpE;QACA,MAAM+B,MAAMhD,cAAcyC,SAAS,CAACQ,UAAU,CAACC,eAAe;QAC9D,MAAMC,gBAAepB,qBAAAA,aAAac,GAAG,CAAC,oBAAjBd,qBAA6B;QAClD,oEAAoE;QACpE,0EAA0E;QAC1E,uDAAuD;QACvD,MAAMqB,kBAAkB,CAAC/B,SACvBtB,oBAAoB;gBAClBsD,SAAS;oBAACnC;iBAAO;gBACjBoC,QAAQ,CAAC,kBAAkB,EAAEjC,QAAQ;YACvC;QAEF,gDAAgD;QAChD,0EAA0E;QAC1E,wEAAwE;QACxE,sDAAsD;QACtD,IAAIA,WAAW,SAAS;YACtB,MAAMkC,OAAO,MAAMd,UAAUe,cAAc,CAAC,CAACC,KAC3CxD,sBAAsBwD,IAAIf,SAAS;oBACjCrB,QAAQ;oBACRqC,QAAQ;wBAAEC,MAAM;oBAAU;gBAC5B;YAEF,IAAIJ,KAAKK,EAAE,KAAK,OAAO,OAAO/C,IAAIE,MAAM,CAACwC,KAAKxC,MAAM,EAAEC,IAAI,CAAC;gBAAEC,OAAOsC,KAAKtC,KAAK;YAAC;YAC/E,MAAMmC,gBAAgB;YACtB,OAAOvC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAE6C,OAAO;YAAK;QAC5C;QAEA,sEAAsE;QACtE,mEAAmE;QACnE,uEAAuE;QACvE,kEAAkE;QAClE,EAAE;QACF,0EAA0E;QAC1E,2EAA2E;QAC3E,IAAIxC,WAAW,mBAAmB;YAChC,MAAMkC,OAAO,MAAMd,UAAUe,cAAc,CAAC,CAACC,KAC3CxD,sBAAsBwD,IAAIf,SAAS;oBAAErB,QAAQ;gBAAU;YAEzD,IAAIkC,KAAKK,EAAE,KAAK,OAAO,OAAO/C,IAAIE,MAAM,CAACwC,KAAKxC,MAAM,EAAEC,IAAI,CAAC;gBAAEC,OAAOsC,KAAKtC,KAAK;YAAC;YAC/E,MAAMmC,gBAAgB;YACtB,OAAOvC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAE8C,SAAS;YAAK;QAC9C;QAEA,wEAAwE;QACxE,IAAIzC,WAAW,UAAU;gBASZ0C;YARX,MAAMA,WAAWhC,aAAac,GAAG,CAAC;YAClC,IAAI,CAACkB,UAAU;gBACb,OAAOlD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAC1BC,OAAO;gBACT;YACF;YACA,MAAMyB,QAAQsB,GAAG,CACf;gBACEC,KAAK,GAAEF,kBAAAA,SAASE,KAAK,YAAdF,kBAAkB/D,cAAcyC,SAAS,CAACQ,UAAU,CAACiB,MAAM;eAC9DH,SAASI,aAAa,GACtB;gBAAEC,oBAAoBL,SAASI,aAAa;YAAC,IAC7C;gBAAEC,oBAAoBpE,cAAcyC,SAAS,CAACQ,UAAU,CAACiB,MAAM;YAAG,GAIlEH,SAASM,QAAQ,GACjB;gBAAEC,eAAeP,SAASM,QAAQ;YAAC,IACnC;gBAAEC,eAAetE,cAAcyC,SAAS,CAACQ,UAAU,CAACiB,MAAM;YAAG;gBACjEK,eAAevE,cAAcyC,SAAS,CAACQ,UAAU,CAACiB,MAAM;gBACxD,+DAA+D;gBAC/D,yDAAyD;gBACzD,kEAAkE;gBAClEM,gBAAgBxE,cAAcyC,SAAS,CAACQ,UAAU,CAACiB,MAAM;gBACzDO,WAAWzB;gBAEb;gBAAE0B,OAAO;YAAK;YAEhB,MAAMtB,gBAAgB;YACtB,OAAOvC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAE2D,UAAU;YAAK;QAC/C;QAEA,MAAMC,aAAanC,UAAUE,UAAU,CAAC,uBAAuBC,GAAG,CAACrB;QACnE,MAAMsD,kBAAkB,MAAMD,WAAW/B,GAAG;QAC5C,MAAMb,UAAU6C,gBAAgBC,IAAI;QACpC,IACE,CAAC9C,WACDA,QAAQ+C,SAAS,IACjB,4DAA4D;QAC5D,mEAAmE;QACnE,wEAAwE;QACxE,gEAAgE;QAChE,sEAAsE;QACtE,8DAA8D;QAC9D,wEAAwE;QACxE,kDAAkD;QAClD,EAAE;QACF,oEAAoE;QACpE,mEAAmE;QACnE/C,QAAQgD,QAAQ,IAChB,mEAAmE;QAClE,MAAMtE,0BAA0B+B,WAAWT,QAAQiD,SAAS,KAC7D7E,oBAAoB4B,aAAa,SACjC;YACA,OAAOnB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAgB;QACvD;QAEA,MAAMiE,WAAWC,QAAOnD,oBAAAA,QAAQkD,QAAQ,YAAhBlD,oBAAoB;QAC5C,MAAMoD,cAAc,MAAM/E,kBACxBoC,WACAP,QAAQI,GAAG,EACXN,QAAQiD,SAAS;QAEnB,+DAA+D;QAC/D,EAAE;QACF,0EAA0E;QAC1E,0EAA0E;QAC1E,2EAA2E;QAC3E,sEAAsE;QACtE,YAAY;QACZ,IAAI9E,iBAAiB6B,YAAY,CAACoD,aAAa;YAC7C,OAAOvE,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAkB;QACzD;QACA,kEAAkE;QAClE,wEAAwE;QACxE,qEAAqE;QACrE,qEAAqE;QACrE,MAAMoE,SAAS,MAAM/E,gBAAgB;YACnCmC;YACA6C,UAAUpD,QAAQI,GAAG;YACrB,qEAAqE;YACrE,+DAA+D;YAC/D,sEAAsE;YACtE,uEAAuE;YACvE,oBAAoB;YACpBiD,UAAU,GAAEtD,oBAAAA,WAAWuD,KAAK,YAAhBvD,oBAAoB;YAChCV;YACA2D;YACAE;QACF;QACA,IAAIC,QAAQ,OAAOxE,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAACqE;QAExC,MAAMI,kBAAkB,MAAMb,WAC3BjC,UAAU,CAAC,YACXC,GAAG,CAACzB,OAAOa,QAAQ0D,aAAa,GAChC7C,GAAG;QACN,MAAMoB,QAAQwB,gBAAgB5C,GAAG,CAAC;QAClC,IAAI,CAACoB,SAAS,CAAC0B,OAAOC,IAAI,CAAC3B,OAAOpC,MAAM,EAAE;YACxC,OAAOhB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAwB;QAC/D;QAEA,+DAA+D;QAC/D,IAAII,WAAW,WAAW;gBAObW;YANX,OAAOnB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1B6E,UAAU/F,cAAcmE;gBACxB6B,SAAShG,cAAcqD;gBACvB,uEAAuE;gBACvE,0DAA0D;gBAC1D4C,UAAUC,QAAQ7C,gBAAgBwC,OAAOC,IAAI,CAACzC,cAActB,MAAM;gBAClEoE,OAAO,GAAEjE,0BAAAA,QAAQ0D,aAAa,YAArB1D,0BAAyB;YACpC;QACF;QAEA,oBAAoB;QACpB,8EAA8E;QAC9E,2EAA2E;QAC3E,0EAA0E;QAC1E,MAAMkE,WAAW,MAAM3F,oBAAoB;YACzCkC;YACA0D,QAAQpE,aAAac,GAAG,CAAC;YACzBiD,SAAS3C;QACX;QACA,IAAI+C,YAAYtF,EAAAA,aAAAA,IAAIQ,IAAI,qBAARR,WAAUwF,IAAI,MAAK,WAAW;YAC5C,OAAOvF,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OACE,sEACA;gBACFiF,UAAU;YACZ;QACF;QAEA,MAAMG,aAAa,MAAM7F,wBAAwB;YAC/CiC;YACAlB;YACAS;YACAiE,SAASjE,QAAQ0D,aAAa;YAC9BY,cAAc;YACdC,SAAStC;QACX;QACA,MAAMuC,mBACJzE,qBAAAA,aAAac,GAAG,CAAC,yCAAjBd,qBAAkD;QAEpD,yEAAyE;QACzE,uEAAuE;QACvE,sEAAsE;QACtE,kCAAkC;QAClC,EAAE;QACF,sEAAsE;QACtE,uEAAuE;QACvE,uEAAuE;QACvE,sEAAsE;QACtE,oEAAoE;QACpE,wEAAwE;QACxE,uEAAuE;QACvE,MAAMwB,OAAO,MAAMd,UAAUe,cAAc,CAAC,CAACC;gBAI5BzB;mBAHf/B,sBAAsBwD,IAAIf,SAAS;gBACjCrB,QAAQ;gBACRE;gBACAkF,MAAMtF,QAAOa,uBAAAA,QAAQ0E,WAAW,YAAnB1E,uBAAuB,OAAO;gBAC3CiC;gBACAE,eAAe,aAAKkC,WAAWlC,aAAa;YAC9C;;QAEF,IAAIZ,KAAKK,EAAE,KAAK,OAAO,OAAO/C,IAAIE,MAAM,CAACwC,KAAKxC,MAAM,EAAEC,IAAI,CAAC;YAAEC,OAAOsC,KAAKtC,KAAK;QAAC;QAE/E,MAAMmC,gBAAgB;QAEtB,MAAM3C,kBAAkB;YACtBgC;YACAmC;YACA0B,cAAc;YACdK,MAAMH;YACNI,IAAI5E,QAAQ0D,aAAa;QAC3B;QACA,MAAMd,WACHiC,MAAM,CAAC;YACNC,cAAc9G,cAAcyC,SAAS,CAACQ,UAAU,CAAC8D,SAAS,CAAC;QAC7D,GACCC,KAAK,CAAC,IAAMlF;QAEf,OAAOjB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAC1BiG,WAAW;YACXhB,OAAO,GAAEjE,yBAAAA,QAAQ0D,aAAa,YAArB1D,yBAAyB;YAClCkF,YAAYb,WAAWa,UAAU;YACjCC,SAASrH,cAAcmE;QACzB;IACF,EAAE,OAAOhD,OAAO;QACdmG,QAAQnG,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAuB;IAC9D;AACF,EAAC;AAED,eAAeN,oBAAmB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-theme.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { describeTheme } from '@aglyn/aglyn/app-utils/site-theme'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { runThemeLibraryAction } from '@aglyn/tenant-data-admin/server/theme-library-write'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs a marketplace theme onto a site (AGL-1020).\n *\n * Themes are the exception to \"installing never changes a running site\". A\n * template or a layout can land inert in a library because it is one thing\n * among many; a theme IS the site's appearance, and installing one into a\n * library where it does nothing would be a control that appears to work and\n * does not. So this writes `hosts/{hostId}.theme` and the site repaints.\n *\n * Which makes reversibility the requirement, not a nicety. Two things provide\n * it, and neither is a confirmation dialog:\n *\n * * The theme being replaced is kept verbatim in `themeReplaced`, so `revert`\n * restores exactly what was there — including a hand-built theme that was\n * never a marketplace artifact and exists nowhere else.\n * * `reset` clears the theme entirely, which is the \"use the default theme\"\n * the issue asks to keep first-class. It is a distinct action from revert\n * because \"back to how it was\" and \"back to stock\" are different intents and\n * guessing between them is how people lose work.\n *\n * `preview` changes nothing and returns what the swap would do, so the\n * confirmation can name it rather than showing a JSON blob.\n *\n * Every action that writes the theme then drops the site's cached pages\n * (AGL-3386). The theme styles every page and nothing publishes it, so\n * without the drop \"the site repaints\" meant \"within the hour\".\n */\nexport const installThemeHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const action = ['revert', 'reset', 'preview', 'clear-overrides'].includes(\n req.body?.action,\n )\n ? (req.body.action as\n | 'revert'\n | 'reset'\n | 'preview'\n | 'clear-overrides')\n : 'install'\n const listingId = String(req.body?.listingId ?? '')\n if (!hostId || (action === 'install' && !listingId)) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const currentTheme = hostSnapshot.get('theme') ?? null\n // Awaited before the response, because a serverless function may be\n // frozen once it has answered. Never throws: a drop that fails leaves the\n // theme saved and the hour-long cache as the backstop.\n const repaintLiveSite = (action: string) =>\n dropPluginSiteCache({\n hostIds: [hostId],\n reason: `marketplace theme ${action}`,\n })\n\n // ---- reset: back to the platform default ----\n // Picking the default from the site's theme library (AGL-3404): the theme\n // being left is filed in the library, edits and all, so resetting stays\n // reversible without a second copy of it on the host.\n if (action === 'reset') {\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, {\n action: 'select',\n target: { kind: 'default' },\n }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n await repaintLiveSite('reset')\n return res.status(200).json({ reset: true })\n }\n\n // ---- clear-overrides: drop this site's changes, keep the theme ----\n // The third way back, and the narrowest: \"reset to the publisher's\n // version\" is deleting the patch — the whole point of owning the patch\n // rather than the copy (AGL-1019). The theme itself is untouched.\n //\n // The library's `restore`, which keeps the site's own dark-scheme setting\n // (AGL-3404): that is a decision about the site, not an edit to the theme.\n if (action === 'clear-overrides') {\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, { action: 'restore' }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n await repaintLiveSite('overrides cleared')\n return res.status(200).json({ cleared: true })\n }\n\n // ---- revert: back to whatever this site had before the last swap ----\n if (action === 'revert') {\n const replaced = hostSnapshot.get('themeReplaced')\n if (!replaced) {\n return res.status(409).json({\n error: 'There is no previous theme to go back to on this site.',\n })\n }\n await hostRef.set(\n {\n theme: replaced.theme ?? firebaseAdmin.firestore.FieldValue.delete(),\n ...(replaced.installedFrom\n ? { themeInstalledFrom: replaced.installedFrom }\n : { themeInstalledFrom: firebaseAdmin.firestore.FieldValue.delete() }),\n // The overrides that were live alongside that theme come back with\n // it. Reverting the base and keeping a patch authored against the\n // theme being reverted FROM is the one combination nobody asked for.\n ...(replaced.override\n ? { themeOverride: replaced.override }\n : { themeOverride: firebaseAdmin.firestore.FieldValue.delete() }),\n themeReplaced: firebaseAdmin.firestore.FieldValue.delete(),\n // A `themeReplaced` predates the library (every library switch\n // clears it), so the theme it restores is read the way a\n // pre-library site's is — from the fields above, not a selection.\n themeSelection: firebaseAdmin.firestore.FieldValue.delete(),\n updatedAt: now,\n },\n { merge: true },\n )\n await repaintLiveSite('reverted')\n return res.status(200).json({ reverted: true })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'theme'\n ) {\n return res.status(404).json({ error: 'Unknown theme' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const theme = versionSnapshot.get('theme')\n if (!theme || !Object.keys(theme).length) {\n return res.status(500).json({ error: 'Theme version missing' })\n }\n\n // ---- preview: say what the swap changes, change nothing ----\n if (action === 'preview') {\n return res.status(200).json({\n incoming: describeTheme(theme),\n current: describeTheme(currentTheme),\n // A site with no theme is on the platform default, so there is nothing\n // to lose and the confirmation should not imply there is.\n replaces: Boolean(currentTheme && Object.keys(currentTheme).length),\n version: listing.latestVersion ?? null,\n })\n }\n\n // ---- install ----\n // Replacing a theme the workspace has edited destroys those edits (AGL-1018).\n // Unlike a component this is not recoverable by looking in a library — the\n // theme lives on one field on one document — so the refusal matters more.\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: hostSnapshot.get('themeInstalledFrom.sha256'),\n current: currentTheme,\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'This site’s theme has been edited since it was installed. Review ' +\n 'the update to see what would change.',\n diverged: true,\n })\n }\n\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'theme',\n content: theme,\n })\n const previousVersion =\n hostSnapshot.get('themeInstalledFrom.version') ?? null\n\n // The theme joins the site's library and is picked (AGL-3404). The theme\n // it replaces is filed in the library with its edits, so installing is\n // reversible by picking it again — the job `themeReplaced` used to do\n // for one theme, for every theme.\n //\n // Edits belong to the theme they were made on. They used to SURVIVE a\n // swap (AGL-1021), because the theme being replaced was about to exist\n // nowhere and carrying its patch forward was the lesser loss. With the\n // library keeping it, a patch tuned for one design no longer lands on\n // another: each theme gets its own back when it is picked again. An\n // UPDATE of the theme already picked keeps its patch exactly as stored,\n // hash included, so the editor can still say it predates this version.\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, {\n action: 'install',\n listingId,\n name: String(listing.displayName ?? '') || 'Marketplace theme',\n theme,\n installedFrom: { ...provenance.installedFrom },\n }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n\n await repaintLiveSite('installed')\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'theme',\n from: previousVersion,\n to: listing.latestVersion,\n })\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n\n return res.status(200).json({\n installed: true,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n applied: describeTheme(theme),\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Theme install failed' })\n }\n}\n\nexport default installThemeHandler\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","describeTheme","dropPluginSiteCache","firebaseAdmin","runThemeLibraryAction","resolveOrgPermissions","isPrivateListing","listingArtifactType","canActAsPublisher","requirePurchase","hasDivergedFromBase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installThemeHandler","req","res","method","status","json","error","hostId","String","body","action","includes","listingId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","now","FieldValue","serverTimestamp","currentTheme","repaintLiveSite","hostIds","reason","plan","runTransaction","tx","target","kind","ok","reset","cleared","replaced","set","theme","delete","installedFrom","themeInstalledFrom","override","themeOverride","themeReplaced","themeSelection","updatedAt","merge","reverted","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","Object","keys","incoming","current","replaces","Boolean","version","diverged","sha256","mode","provenance","artifactType","content","previousVersion","name","displayName","from","to","update","installCount","increment","catch","installed","baseStored","applied","console"],"mappings":"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"}
|
|
@@ -17,6 +17,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ // The leaf module, not the server barrel: the list keys every artifact
|
|
18
18
|
// create stamps (AGL-3321), which the install specs' closed-world barrel
|
|
19
19
|
// mocks have no reason to stage.
|
|
20
|
+
import { holdsStampedSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
21
|
+
import { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from "../model/marketplace-site-permissions.js";
|
|
20
22
|
import { artifactCreateListKeys } from "@aglyn/aglyn/app-utils/artifact-list-keys";
|
|
21
23
|
import { checkQuota, createResourceUid, decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/server";
|
|
22
24
|
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
@@ -57,7 +59,7 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
|
|
|
57
59
|
error: 'Unauthenticated'
|
|
58
60
|
});
|
|
59
61
|
try {
|
|
60
|
-
var
|
|
62
|
+
var _listing_priceUsd, _membership_orgId, _version_nodes, _existing_docs__get, _existing_docs__get1;
|
|
61
63
|
var _this;
|
|
62
64
|
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
63
65
|
// Org-role permission gate (AGL-238).
|
|
@@ -77,10 +79,11 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
|
|
|
77
79
|
error: 'Unknown site'
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
// "Install marketplace items into a site" (AGL-4047): the site's stamp, else
|
|
83
|
+
// the preset of the member's site role, the Admin and Editor this asked for.
|
|
84
|
+
if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {
|
|
82
85
|
return res.status(403).json({
|
|
83
|
-
error: '
|
|
86
|
+
error: marketplaceSitePermissionRefusal('marketplace.siteInstall')
|
|
84
87
|
});
|
|
85
88
|
}
|
|
86
89
|
// The SAME gate the console's own create path applies to this exact
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n checkQuota,\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { isPrivateListing } from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\nimport { canActAsPublisher } from './publisher-profile'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs (or updates) a marketplace listing into a host (AGL-44/46).\n * Server-side because version snapshots are not client-readable — paid\n * content would otherwise be free to anyone who read the subcollection.\n * Access: the listing is free, the caller bought it (webhook-written\n * purchase record), or the caller published it. Installing copies the\n * latest version into `hosts/{hostId}/components` with `marketplace` source\n * metadata; re-installing updates the same component doc in place.\n */\nexport const installHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n // Org-role permission gate (AGL-238).\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n // The SAME gate the console's own create path applies to this exact\n // document (AGL-2072). `/api/hosts/resources` creates\n // `hosts/{hostId}/components` and refuses past the plan's allowance,\n // because a reusable component renders on the LIVE SITE and the gate has\n // to be server-enforced rather than merely hidden in the console\n // (AGL-473). This route writes a byte-for-byte equivalent doc into the\n // same collection, so it asks the same question: before AGL-2072 it asked\n // nothing, and a free org got the working feature its own console would\n // have refused.\n //\n // The question is a COUNT since AGL-3615: `componentsPerHost`, Free 1 and\n // every paid plan unlimited, no longer the `reusableComponents` flag. A\n // re-install lands on the copy the site already holds, so it creates\n // nothing and is not counted; only a first install of the listing is.\n //\n // Ahead of `requirePurchase`, deliberately, and matching\n // `install-dataset-schema.ts:89`: refusing on plan AFTER taking someone's\n // money for the listing would be the worse order. The whole org doc is\n // read (`getOrgDoc` projects nothing), so `resolveEffectivePlan` sees the\n // subscription status and a LAPSED org is counted exactly as a free one\n // is — the gate is re-asked per install, never inherited from whatever\n // plan was in force when an earlier copy was installed.\n const org = (await getOrgForHost(hostId))?.org\n const componentsRef = hostRef.collection('components')\n const existing = await componentsRef\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\n if (existing.empty) {\n // LIVE components only, as `/api/hosts/resources` counts them: a\n // deletion stamps `deletedAt` and keeps the document. Nothing is read\n // on a plan whose allowance is unlimited.\n const limit = checkQuota(org as any, 'componentsPerHost', 0).limit\n const used = Number.isFinite(limit)\n ? (await componentsRef.select('deletedAt').get()).docs.filter(\n (entry) => entry.get('deletedAt') == null,\n ).length\n : 0\n const quota = checkQuota(org as any, 'componentsPerHost', used)\n if (!quota.allowed) {\n return res.status(403).json({\n error:\n `Your plan includes ${quota.limit} reusable component${quota.limit === 1 ? '' : 's'} — ` +\n 'upgrade in Billing for more',\n })\n }\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId))\n ) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // The publisher installs their own listing for free. Org-owned now\n // (AGL-652), so this is a role check — comparing a uid to an org id\n // would never match and would charge publishers for their own work.\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase no longer entitles (AGL-1546). The gate this\n // route once inlined is now shared (AGL-1699): it was the only one of\n // eight paid-content paths that had learned the refund rule.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const version = versionSnapshot.data() as any\n if (!version?.nodes || !version?.rootId) {\n return res.status(500).json({ error: 'Listing version missing' })\n }\n\n // The properties the tree binds to (AGL-2933), held to the sanitizer again\n // on the way in. `undefined` for a version published before they were\n // carried, which leaves a re-installed copy's own properties in place.\n const props = readPublishedProps(version.props)\n\n const componentRef = existing.empty\n ? componentsRef.doc(createResourceUid())\n : existing.docs[0].ref\n // Re-installing over a copy the workspace has EDITED is the silent\n // destructive overwrite AGL-1018 exists to stop. The install still happens\n // when it is asked for explicitly (`mode: 'replace'`, which the update\n // dialog sends after showing what goes), but it is never the default.\n if (!existing.empty) {\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: existing.docs[0].get('installedFrom.sha256'),\n current: {\n rootId: existing.docs[0].get('rootId'),\n // DECODED, because the comparison is by value: the base snapshot\n // recorded at install is a node map and `hasDivergedFromBase`\n // hashes a stable stringification of both sides. A stored `Bytes`\n // matches no map, so an untouched copy would read as edited and\n // every re-install would 409 (AGL-1151).\n nodes: decodeStoredNodes(existing.docs[0].get('nodes')),\n },\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'Your copy of this component has been edited since it was ' +\n 'installed. Review the update to see what would change.',\n diverged: true,\n })\n }\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Provenance + base snapshot (AGL-1015). The snapshot holds only the\n // vendored tree — the display name and description come from the listing\n // and the user may rename their copy, so including them would make every\n // rename read as a diverged component.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'component',\n content: { rootId: version.rootId, nodes: version.nodes },\n })\n await componentRef.set(\n {\n displayName: listing.displayName,\n ...(listing.description && { description: listing.description }),\n rootId: version.rootId,\n // Compressed at rest, matching what a promote writes onto this same\n // document (AGL-1151). The provenance base above keeps the decoded\n // map: it is compared by value, never rendered.\n nodes: Buffer.from(encodeStoredNodes(version.nodes ?? {})!),\n // Beside the tree, as a publish writes them onto this document. The\n // first version the console mints for this component copies them\n // from here (AGL-2932).\n ...(props && { props }),\n deletedAt: null,\n // The keys the components list finds it by (AGL-3321): the listing's\n // name, and — on a first install — the stored kind; a re-install\n // keeps the kind the copy already has.\n ...artifactCreateListKeys('components', {\n displayName: listing.displayName,\n // The description written above — or, on a re-install (a merge),\n // the one the copy already has — so the stored-null default\n // (AGL-3680) never overwrites it.\n description:\n listing.description ||\n (existing.empty ? null : (existing.docs[0].get('description') ?? null)),\n kind: existing.empty ? undefined : existing.docs[0].get('kind'),\n }),\n marketplace: {\n listingId,\n profileId: listing.profileId,\n version: listing.latestVersion,\n },\n installedFrom: provenance.installedFrom,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n // Per-version tally (AGL-1036): a re-install moves this copy off whatever\n // version it was on and onto the latest.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'component',\n from: existing.empty\n ? null\n : (existing.docs[0].get('installedFrom.version') ??\n existing.docs[0].get('marketplace.version')),\n to: listing.latestVersion,\n })\n // First installs count toward the browse \"Most installed\" sort\n // (AGL-95); updates don't inflate it.\n if (existing.empty) {\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n return res.status(200).json({\n installed: true,\n updated: !existing.empty,\n version: listing.latestVersion,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["artifactCreateListKeys","checkQuota","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","readPublishedProps","canActAsPublisher","hasDivergedFromBase","recordInstallProvenance","requirePurchase","recordVersionMove","isPublisherSecurityLocked","installHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","version","existing","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","org","componentsRef","where","limit","empty","used","Number","isFinite","select","docs","filter","entry","quota","allowed","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","nodes","rootId","props","componentRef","ref","diverged","sha256","current","mode","now","FieldValue","serverTimestamp","provenance","artifactType","content","set","displayName","description","Buffer","from","kind","marketplace","installedFrom","createdAt","updatedAt","merge","to","update","installCount","increment","catch","installed","updated","baseStored","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n checkQuota,\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { isPrivateListing } from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\nimport { canActAsPublisher } from './publisher-profile'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs (or updates) a marketplace listing into a host (AGL-44/46).\n * Server-side because version snapshots are not client-readable — paid\n * content would otherwise be free to anyone who read the subcollection.\n * Access: the listing is free, the caller bought it (webhook-written\n * purchase record), or the caller published it. Installing copies the\n * latest version into `hosts/{hostId}/components` with `marketplace` source\n * metadata; re-installing updates the same component doc in place.\n */\nexport const installHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n // Org-role permission gate (AGL-238).\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from 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 // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n // The SAME gate the console's own create path applies to this exact\n // document (AGL-2072). `/api/hosts/resources` creates\n // `hosts/{hostId}/components` and refuses past the plan's allowance,\n // because a reusable component renders on the LIVE SITE and the gate has\n // to be server-enforced rather than merely hidden in the console\n // (AGL-473). This route writes a byte-for-byte equivalent doc into the\n // same collection, so it asks the same question: before AGL-2072 it asked\n // nothing, and a free org got the working feature its own console would\n // have refused.\n //\n // The question is a COUNT since AGL-3615: `componentsPerHost`, Free 1 and\n // every paid plan unlimited, no longer the `reusableComponents` flag. A\n // re-install lands on the copy the site already holds, so it creates\n // nothing and is not counted; only a first install of the listing is.\n //\n // Ahead of `requirePurchase`, deliberately, and matching\n // `install-dataset-schema.ts:89`: refusing on plan AFTER taking someone's\n // money for the listing would be the worse order. The whole org doc is\n // read (`getOrgDoc` projects nothing), so `resolveEffectivePlan` sees the\n // subscription status and a LAPSED org is counted exactly as a free one\n // is — the gate is re-asked per install, never inherited from whatever\n // plan was in force when an earlier copy was installed.\n const org = (await getOrgForHost(hostId))?.org\n const componentsRef = hostRef.collection('components')\n const existing = await componentsRef\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\n if (existing.empty) {\n // LIVE components only, as `/api/hosts/resources` counts them: a\n // deletion stamps `deletedAt` and keeps the document. Nothing is read\n // on a plan whose allowance is unlimited.\n const limit = checkQuota(org as any, 'componentsPerHost', 0).limit\n const used = Number.isFinite(limit)\n ? (await componentsRef.select('deletedAt').get()).docs.filter(\n (entry) => entry.get('deletedAt') == null,\n ).length\n : 0\n const quota = checkQuota(org as any, 'componentsPerHost', used)\n if (!quota.allowed) {\n return res.status(403).json({\n error:\n `Your plan includes ${quota.limit} reusable component${quota.limit === 1 ? '' : 's'} — ` +\n 'upgrade in Billing for more',\n })\n }\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId))\n ) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // The publisher installs their own listing for free. Org-owned now\n // (AGL-652), so this is a role check — comparing a uid to an org id\n // would never match and would charge publishers for their own work.\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase no longer entitles (AGL-1546). The gate this\n // route once inlined is now shared (AGL-1699): it was the only one of\n // eight paid-content paths that had learned the refund rule.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const version = versionSnapshot.data() as any\n if (!version?.nodes || !version?.rootId) {\n return res.status(500).json({ error: 'Listing version missing' })\n }\n\n // The properties the tree binds to (AGL-2933), held to the sanitizer again\n // on the way in. `undefined` for a version published before they were\n // carried, which leaves a re-installed copy's own properties in place.\n const props = readPublishedProps(version.props)\n\n const componentRef = existing.empty\n ? componentsRef.doc(createResourceUid())\n : existing.docs[0].ref\n // Re-installing over a copy the workspace has EDITED is the silent\n // destructive overwrite AGL-1018 exists to stop. The install still happens\n // when it is asked for explicitly (`mode: 'replace'`, which the update\n // dialog sends after showing what goes), but it is never the default.\n if (!existing.empty) {\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: existing.docs[0].get('installedFrom.sha256'),\n current: {\n rootId: existing.docs[0].get('rootId'),\n // DECODED, because the comparison is by value: the base snapshot\n // recorded at install is a node map and `hasDivergedFromBase`\n // hashes a stable stringification of both sides. A stored `Bytes`\n // matches no map, so an untouched copy would read as edited and\n // every re-install would 409 (AGL-1151).\n nodes: decodeStoredNodes(existing.docs[0].get('nodes')),\n },\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'Your copy of this component has been edited since it was ' +\n 'installed. Review the update to see what would change.',\n diverged: true,\n })\n }\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Provenance + base snapshot (AGL-1015). The snapshot holds only the\n // vendored tree — the display name and description come from the listing\n // and the user may rename their copy, so including them would make every\n // rename read as a diverged component.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'component',\n content: { rootId: version.rootId, nodes: version.nodes },\n })\n await componentRef.set(\n {\n displayName: listing.displayName,\n ...(listing.description && { description: listing.description }),\n rootId: version.rootId,\n // Compressed at rest, matching what a promote writes onto this same\n // document (AGL-1151). The provenance base above keeps the decoded\n // map: it is compared by value, never rendered.\n nodes: Buffer.from(encodeStoredNodes(version.nodes ?? {})!),\n // Beside the tree, as a publish writes them onto this document. The\n // first version the console mints for this component copies them\n // from here (AGL-2932).\n ...(props && { props }),\n deletedAt: null,\n // The keys the components list finds it by (AGL-3321): the listing's\n // name, and — on a first install — the stored kind; a re-install\n // keeps the kind the copy already has.\n ...artifactCreateListKeys('components', {\n displayName: listing.displayName,\n // The description written above — or, on a re-install (a merge),\n // the one the copy already has — so the stored-null default\n // (AGL-3680) never overwrites it.\n description:\n listing.description ||\n (existing.empty ? null : (existing.docs[0].get('description') ?? null)),\n kind: existing.empty ? undefined : existing.docs[0].get('kind'),\n }),\n marketplace: {\n listingId,\n profileId: listing.profileId,\n version: listing.latestVersion,\n },\n installedFrom: provenance.installedFrom,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n // Per-version tally (AGL-1036): a re-install moves this copy off whatever\n // version it was on and onto the latest.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'component',\n from: existing.empty\n ? null\n : (existing.docs[0].get('installedFrom.version') ??\n existing.docs[0].get('marketplace.version')),\n to: listing.latestVersion,\n })\n // First installs count toward the browse \"Most installed\" sort\n // (AGL-95); updates don't inflate it.\n if (existing.empty) {\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n return res.status(200).json({\n installed: true,\n updated: !existing.empty,\n version: listing.latestVersion,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","artifactCreateListKeys","checkQuota","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","readPublishedProps","canActAsPublisher","hasDivergedFromBase","recordInstallProvenance","requirePurchase","recordVersionMove","isPublisherSecurityLocked","installHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","version","existing","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","org","componentsRef","where","limit","empty","used","Number","isFinite","select","docs","filter","entry","quota","allowed","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","nodes","rootId","props","componentRef","ref","diverged","sha256","current","mode","now","FieldValue","serverTimestamp","provenance","artifactType","content","set","displayName","description","Buffer","from","kind","marketplace","installedFrom","createdAt","updatedAt","merge","to","update","installCount","increment","catch","installed","updated","baseStored","console"],"mappings":"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"}
|
|
@@ -14,7 +14,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
17
|
+
*/ import { holdsStampedSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
18
|
+
import { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from "../model/marketplace-site-permissions.js";
|
|
19
|
+
import { displayNameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
|
|
18
20
|
import { CANVAS_ROOT_ELEMENT_ID, checkEntitlement, createResourceUid } from "@aglyn/aglyn/server";
|
|
19
21
|
import { decodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
|
|
20
22
|
import { inlineReusableBlocks } from "./publish-email-blocks.js";
|
|
@@ -78,7 +80,7 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
|
|
|
78
80
|
error: 'Unauthenticated'
|
|
79
81
|
});
|
|
80
82
|
try {
|
|
81
|
-
var
|
|
83
|
+
var _ref6, _existing_docs__get, _orgForHost_org, _screen_emailSubject, _screen_emailPreheader;
|
|
82
84
|
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
83
85
|
const membership = await resolveOrgPermissions(decoded.uid, {
|
|
84
86
|
hostId
|
|
@@ -96,10 +98,11 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
|
|
|
96
98
|
error: 'Unknown site'
|
|
97
99
|
});
|
|
98
100
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
// "Publish a site’s items to the marketplace" (AGL-4047): the site's stamp, else
|
|
102
|
+
// the preset of the member's site role, the Admin and Editor this asked for.
|
|
103
|
+
if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.sitePublish', MARKETPLACE_SITE_ROLE_PRESET)) {
|
|
101
104
|
return res.status(403).json({
|
|
102
|
-
error: '
|
|
105
|
+
error: marketplaceSitePermissionRefusal('marketplace.sitePublish')
|
|
103
106
|
});
|
|
104
107
|
}
|
|
105
108
|
const orgForHost = await getOrgForHost(hostId);
|