@aglyn/plugins-marketplace 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/listing-content.component.js +78 -36
  3. package/src/lib/components/listing-content.component.js.map +1 -1
  4. package/src/lib/components/listing-detail-page.component.js +22 -9
  5. package/src/lib/components/listing-detail-page.component.js.map +1 -1
  6. package/src/lib/components/listing-review-status.component.d.ts +8 -0
  7. package/src/lib/components/listing-review-status.component.js +23 -3
  8. package/src/lib/components/listing-review-status.component.js.map +1 -1
  9. package/src/lib/components/listing-verification-request.component.d.ts +8 -0
  10. package/src/lib/components/listing-verification-request.component.js +12 -5
  11. package/src/lib/components/listing-verification-request.component.js.map +1 -1
  12. package/src/lib/components/marketplace-hub.component.js +2 -1
  13. package/src/lib/components/marketplace-hub.component.js.map +1 -1
  14. package/src/lib/components/org-seller-panel.component.d.ts +10 -0
  15. package/src/lib/components/org-seller-panel.component.js +26 -4
  16. package/src/lib/components/org-seller-panel.component.js.map +1 -1
  17. package/src/lib/components/starter-picker-shelf.component.js +13 -8
  18. package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
  19. package/src/lib/declarations.d.ts +2 -1
  20. package/src/lib/declarations.js +5 -1
  21. package/src/lib/declarations.js.map +1 -1
  22. package/src/lib/model/marketplace-catalog.js +1 -1
  23. package/src/lib/model/marketplace-catalog.js.map +1 -1
  24. package/src/lib/model/marketplace-publisher-permissions.d.ts +26 -0
  25. package/src/lib/model/marketplace-publisher-permissions.js +64 -0
  26. package/src/lib/model/marketplace-publisher-permissions.js.map +1 -0
  27. package/src/lib/model/marketplace.js +2 -1
  28. package/src/lib/model/marketplace.js.map +1 -1
  29. package/src/lib/model/own-listing.d.ts +52 -0
  30. package/src/lib/model/own-listing.js +72 -0
  31. package/src/lib/model/own-listing.js.map +1 -0
  32. package/src/lib/model/starter-query.js +2 -0
  33. package/src/lib/model/starter-query.js.map +1 -1
  34. package/src/lib/server/connect.js +11 -5
  35. package/src/lib/server/connect.js.map +1 -1
  36. package/src/lib/server/install-dataset-schema.js +10 -2
  37. package/src/lib/server/install-dataset-schema.js.map +1 -1
  38. package/src/lib/server/install-email-starter.js +8 -2
  39. package/src/lib/server/install-email-starter.js.map +1 -1
  40. package/src/lib/server/install-email-template.js +8 -2
  41. package/src/lib/server/install-email-template.js.map +1 -1
  42. package/src/lib/server/install-layout.js +8 -2
  43. package/src/lib/server/install-layout.js.map +1 -1
  44. package/src/lib/server/install-plugin.js +24 -20
  45. package/src/lib/server/install-plugin.js.map +1 -1
  46. package/src/lib/server/install-template.js +8 -5
  47. package/src/lib/server/install-template.js.map +1 -1
  48. package/src/lib/server/install-theme.js +8 -2
  49. package/src/lib/server/install-theme.js.map +1 -1
  50. package/src/lib/server/install.js +10 -5
  51. package/src/lib/server/install.js.map +1 -1
  52. package/src/lib/server/listing-versions.js +24 -9
  53. package/src/lib/server/listing-versions.js.map +1 -1
  54. package/src/lib/server/own-listing.d.ts +46 -0
  55. package/src/lib/server/own-listing.js +52 -0
  56. package/src/lib/server/own-listing.js.map +1 -0
  57. package/src/lib/server/preview-image.d.ts +1 -1
  58. package/src/lib/server/preview-image.js +9 -8
  59. package/src/lib/server/preview-image.js.map +1 -1
  60. package/src/lib/server/publish-plugin.js +17 -19
  61. package/src/lib/server/publish-plugin.js.map +1 -1
  62. package/src/lib/server/publisher-permissions.d.ts +63 -0
  63. package/src/lib/server/publisher-permissions.js +51 -0
  64. package/src/lib/server/publisher-permissions.js.map +1 -0
  65. package/src/lib/server/publisher-profile-save.d.ts +4 -2
  66. package/src/lib/server/publisher-profile-save.js +12 -5
  67. package/src/lib/server/publisher-profile-save.js.map +1 -1
  68. package/src/lib/server/publisher-profile.d.ts +18 -6
  69. package/src/lib/server/publisher-profile.js +19 -8
  70. package/src/lib/server/publisher-profile.js.map +1 -1
  71. package/src/lib/server/reviews.js +6 -5
  72. package/src/lib/server/reviews.js.map +1 -1
  73. package/src/lib/server/sale-risk.d.ts +0 -6
  74. package/src/lib/server/sale-risk.js +0 -9
  75. package/src/lib/server/sale-risk.js.map +1 -1
  76. package/src/lib/server/sale.js +10 -9
  77. package/src/lib/server/sale.js.map +1 -1
  78. package/src/lib/server/site-starter-source.js +43 -17
  79. package/src/lib/server/site-starter-source.js.map +1 -1
  80. package/src/lib/server/update-artifact.js +11 -4
  81. package/src/lib/server/update-artifact.js.map +1 -1
  82. package/src/lib/server/verification-request.js +23 -7
  83. package/src/lib/server/verification-request.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-layout.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\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 decodeStoredNodes,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\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 layout into a host's template library (AGL-671).\n *\n * Lands as a `kind: 'layout'` template rather than a live layout, for the\n * same reason template installs do (AGL-669): installing must never change\n * a running site. Creating the actual layout is the deliberate second step\n * in the templates library.\n *\n * Access mirrors template installs: free, purchased, or your own listing.\n */\nexport const installLayoutHandler: 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 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\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) !== 'layout'\n ) {\n return res.status(404).json({ error: 'Unknown layout' })\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 layout = versionSnapshot.get('layout') as any\n if (!layout?.nodes || !Object.keys(layout.nodes).length) {\n return res.status(500).json({ error: 'Layout version missing' })\n }\n // The layout properties its tree binds to (AGL-2933), held to the\n // sanitizer again on the way in; `undefined` for a version published\n // before they were carried.\n const props = readPublishedProps(layout.props)\n\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 * The copy this listing already installed is about to be replaced, so\n * counting it would fail the quota on an update the user is entitled to.\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') && entry.get('source.listingId') !== listingId,\n ).length\n const overQuota = (limit: number) =>\n `Your plan allows ${limit} templates — see Billing to upgrade.`\n const existing = await templatesRef.get()\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(existing.docs),\n )\n if (!quota.allowed) {\n return res.status(403).json({ error: overQuota(quota.limit) })\n }\n }\n\n // Replacing a layout the workspace has edited destroys those edits — the\n // old doc is soft-deleted, so the change is invisible until someone goes\n // looking for it. Refuse unless the caller has been shown what goes and\n // asked for a replacement anyway (AGL-1018).\n const installedLayout = existing.docs.find(\n (entry) =>\n !entry.get('deletedAt') && entry.get('source.listingId') === listingId,\n )\n if (installedLayout) {\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: installedLayout.get('installedFrom.sha256'),\n current: {\n rootId: installedLayout.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 // never matches a map, so an untouched copy would read as edited\n // and every re-install would be refused as a divergence\n // (AGL-1151).\n nodes: decodeStoredNodes(installedLayout.get('nodes')),\n },\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'Your copy of this layout has been edited since it was installed. ' +\n '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 layout tree as published,\n // without the display name the user is free to change.\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: 'layout',\n content: { rootId: layout.rootId, nodes: layout.nodes },\n })\n /**\n * Re-install replaces the prior copy rather than stacking (AGL-671).\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 `hasDivergedFromBase` awaited, then\n * `recordInstallProvenance` awaited, and only then did a `WriteBatch`\n * commit. Every await is a yield, so N concurrent installs each read the\n * same pre-count, each found room, and each landed — and nothing re-counts\n * afterwards, so the extra templates were permanent. A batch is atomic but\n * NOT conditional on a read taken before it, which is the same lesson\n * 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 DIVERGENCE gate above deliberately stays outside. It is not a count:\n * it asks whether this workspace edited its own copy, which no concurrent\n * install of a different listing can change, and it reads a base snapshot\n * through a helper that does its own `get`.\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 const outcome = await firestore.runTransaction<\n { error: string } | { 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) =>\n !entry.get('deletedAt') && entry.get('source.listingId') === listingId,\n )\n for (const stale of superseded) {\n tx.update(stale.ref, {\n deletedAt: now,\n updatedAt: now,\n ...artifactDeleteListKeys('templates'),\n })\n }\n const layoutName = String(listing.displayName ?? 'Layout').slice(0, 80)\n tx.set(templatesRef.doc(createResourceUid()), {\n kind: 'layout',\n displayName: layoutName,\n ...(listing.description && { description: listing.description }),\n rootId: layout.rootId,\n // Compressed at rest, matching the template converter (AGL-1151).\n // The provenance base recorded above keeps the decoded map: it is\n // compared by value, never opened as a document.\n nodes: Buffer.from(encodeStoredNodes(layout.nodes ?? {})!),\n // Use template seeds the layout's first version with these\n // (AGL-2932), so a screen using it has its layout values to set.\n ...(props && { props }),\n source: {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n },\n installedFrom: provenance.installedFrom,\n // The keys the templates library finds it by (AGL-3321).\n ...artifactCreateListKeys('templates', {\n kind: 'layout',\n displayName: layoutName,\n source: { type: 'marketplace' },\n }),\n createdAt: now,\n updatedAt: now,\n })\n // Per-version tally input (AGL-1036): the replaced copy leaves its\n // version — read off the doc the transaction actually replaced, not off\n // the snapshot taken before it opened.\n return {\n replaced: superseded.length,\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 { replaced } = outcome\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'layout',\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: 1,\n replaced,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Layout install failed' })\n }\n}\n\nexport default installLayoutHandler\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","artifactCreateListKeys","artifactDeleteListKeys","checkQuota","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","listingArtifactType","readPublishedProps","canActAsPublisher","requirePurchase","hasDivergedFromBase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installLayoutHandler","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","layout","nodes","Object","keys","props","org","templatesRef","slotsAfterInstall","docs","filter","entry","overQuota","limit","existing","quota","allowed","installedLayout","find","diverged","sha256","current","rootId","mode","now","FieldValue","serverTimestamp","provenance","version","artifactType","content","outcome","runTransaction","tx","superseded","live","stale","update","ref","updatedAt","layoutName","displayName","set","kind","description","Buffer","from","source","type","installedFrom","createdAt","replaced","to","installCount","increment","catch","installed","templates","baseStored","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-layout.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\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 decodeStoredNodes,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\nimport { ownsListingHere } from './own-listing'\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 layout into a host's template library (AGL-671).\n *\n * Lands as a `kind: 'layout'` template rather than a live layout, for the\n * same reason template installs do (AGL-669): installing must never change\n * a running site. Creating the actual layout is the deliberate second step\n * in the templates library.\n *\n * Access mirrors template installs: free, purchased, or your own listing.\n */\nexport const installLayoutHandler: 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 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\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) !== 'layout'\n ) {\n return res.status(404).json({ error: 'Unknown layout' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // Free for the publisher's members on its OWN sites only (AGL-2484,\n // AGL-4047); into another workspace they are buyers like anyone else.\n const ownsListing = await ownsListingHere(firestore, {\n uid: decoded.uid,\n publisherOrgId: listing.profileId,\n targetOrgId: membership.orgId,\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 layout = versionSnapshot.get('layout') as any\n if (!layout?.nodes || !Object.keys(layout.nodes).length) {\n return res.status(500).json({ error: 'Layout version missing' })\n }\n // The layout properties its tree binds to (AGL-2933), held to the\n // sanitizer again on the way in; `undefined` for a version published\n // before they were carried.\n const props = readPublishedProps(layout.props)\n\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 * The copy this listing already installed is about to be replaced, so\n * counting it would fail the quota on an update the user is entitled to.\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') && entry.get('source.listingId') !== listingId,\n ).length\n const overQuota = (limit: number) =>\n `Your plan allows ${limit} templates — see Billing to upgrade.`\n const existing = await templatesRef.get()\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(existing.docs),\n )\n if (!quota.allowed) {\n return res.status(403).json({ error: overQuota(quota.limit) })\n }\n }\n\n // Replacing a layout the workspace has edited destroys those edits — the\n // old doc is soft-deleted, so the change is invisible until someone goes\n // looking for it. Refuse unless the caller has been shown what goes and\n // asked for a replacement anyway (AGL-1018).\n const installedLayout = existing.docs.find(\n (entry) =>\n !entry.get('deletedAt') && entry.get('source.listingId') === listingId,\n )\n if (installedLayout) {\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: installedLayout.get('installedFrom.sha256'),\n current: {\n rootId: installedLayout.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 // never matches a map, so an untouched copy would read as edited\n // and every re-install would be refused as a divergence\n // (AGL-1151).\n nodes: decodeStoredNodes(installedLayout.get('nodes')),\n },\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'Your copy of this layout has been edited since it was installed. ' +\n '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 layout tree as published,\n // without the display name the user is free to change.\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: 'layout',\n content: { rootId: layout.rootId, nodes: layout.nodes },\n })\n /**\n * Re-install replaces the prior copy rather than stacking (AGL-671).\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 `hasDivergedFromBase` awaited, then\n * `recordInstallProvenance` awaited, and only then did a `WriteBatch`\n * commit. Every await is a yield, so N concurrent installs each read the\n * same pre-count, each found room, and each landed — and nothing re-counts\n * afterwards, so the extra templates were permanent. A batch is atomic but\n * NOT conditional on a read taken before it, which is the same lesson\n * 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 DIVERGENCE gate above deliberately stays outside. It is not a count:\n * it asks whether this workspace edited its own copy, which no concurrent\n * install of a different listing can change, and it reads a base snapshot\n * through a helper that does its own `get`.\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 const outcome = await firestore.runTransaction<\n { error: string } | { 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) =>\n !entry.get('deletedAt') && entry.get('source.listingId') === listingId,\n )\n for (const stale of superseded) {\n tx.update(stale.ref, {\n deletedAt: now,\n updatedAt: now,\n ...artifactDeleteListKeys('templates'),\n })\n }\n const layoutName = String(listing.displayName ?? 'Layout').slice(0, 80)\n tx.set(templatesRef.doc(createResourceUid()), {\n kind: 'layout',\n displayName: layoutName,\n ...(listing.description && { description: listing.description }),\n rootId: layout.rootId,\n // Compressed at rest, matching the template converter (AGL-1151).\n // The provenance base recorded above keeps the decoded map: it is\n // compared by value, never opened as a document.\n nodes: Buffer.from(encodeStoredNodes(layout.nodes ?? {})!),\n // Use template seeds the layout's first version with these\n // (AGL-2932), so a screen using it has its layout values to set.\n ...(props && { props }),\n source: {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n },\n installedFrom: provenance.installedFrom,\n // The keys the templates library finds it by (AGL-3321).\n ...artifactCreateListKeys('templates', {\n kind: 'layout',\n displayName: layoutName,\n source: { type: 'marketplace' },\n }),\n createdAt: now,\n updatedAt: now,\n })\n // Per-version tally input (AGL-1036): the replaced copy leaves its\n // version — read off the doc the transaction actually replaced, not off\n // the snapshot taken before it opened.\n return {\n replaced: superseded.length,\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 { replaced } = outcome\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'layout',\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: 1,\n replaced,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Layout install failed' })\n }\n}\n\nexport default installLayoutHandler\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","artifactCreateListKeys","artifactDeleteListKeys","checkQuota","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","listingArtifactType","readPublishedProps","ownsListingHere","requirePurchase","hasDivergedFromBase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installLayoutHandler","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","publisherOrgId","targetOrgId","orgId","unpaid","buyerUid","buyerOrgId","versionSnapshot","latestVersion","layout","nodes","Object","keys","props","org","templatesRef","slotsAfterInstall","docs","filter","entry","overQuota","limit","existing","quota","allowed","installedLayout","find","diverged","sha256","current","rootId","mode","now","FieldValue","serverTimestamp","provenance","version","artifactType","content","outcome","runTransaction","tx","superseded","live","stale","update","ref","updatedAt","layoutName","displayName","set","kind","description","Buffer","from","source","type","installedFrom","createdAt","replaced","to","installCount","increment","catch","installed","templates","baseStored","console"],"mappings":"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"}
@@ -20,7 +20,7 @@ import { resolveOrgIdForHost, firebaseAdmin } from "@aglyn/tenant-data-admin";
20
20
  import { isCompatibleHostAbi, isFirstPartyPlugin, isPluginRevoked, newestInstallableVersion, PLUGIN_HOST_ABI_VERSION } from "@aglyn/aglyn/server";
21
21
  import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
22
22
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
23
- import { canActAsPublisher } from "./publisher-profile.js";
23
+ import { ownsListingHere } from "./own-listing.js";
24
24
  import { requirePurchase } from "./purchase-entitlement.js";
25
25
  import { pinnedProvenance } from "./provenance.js";
26
26
  import { recordVersionMove } from "./version-stats.js";
@@ -117,7 +117,7 @@ export const installPluginHandler = async (req, res)=>{
117
117
  error: 'Unauthenticated'
118
118
  });
119
119
  try {
120
- var _listing_profileId, _membership_orgId, _listing_priceUsd, _membership_orgId1;
120
+ var _listing_priceUsd, _membership_orgId;
121
121
  var _req_body3, _req_body4, _versionData_manifest;
122
122
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
123
123
  const membership = await resolveOrgPermissions(decoded.uid, {
@@ -212,25 +212,29 @@ export const installPluginHandler = async (req, res)=>{
212
212
  }
213
213
  // The publisher installs their own listing for free, and may install
214
214
  // an unapproved version of it to test. Org-owned now (AGL-652), so this
215
- // is a role check — comparing a uid to an org id would never match.
215
+ // asks the publishing org's members (comparing a uid to an org id would
216
+ // never match). Membership, not a permission (AGL-4047): it is a fact
217
+ // about who published the listing.
216
218
  //
217
- // TWO facts, correlated (AGL-2484). `canActAsPublisher` answers a
218
- // question about the PUBLISHER org alone; the install TARGET is a
219
- // different org, resolved from the host by the permission gate above.
220
- // Nothing tied them together, so one flag computed from org P waived
221
- // three gates on an install landing in org Q: review, version
222
- // resolution, and payment. A user who administers publisher org P and
223
- // holds `installPlugins` on a host in org Q could therefore push P's
224
- // pending — or rejected — bundle onto Q's live public site, for free.
219
+ // TWO facts, correlated (AGL-2484). Membership answers a question about
220
+ // the PUBLISHER org alone; the install TARGET is a different org,
221
+ // resolved from the host by the permission gate above. Nothing tied them
222
+ // together, so one flag computed from org P waived three gates on an
223
+ // install landing in org Q: review, version resolution, and payment. A
224
+ // user who administers publisher org P and holds `installPlugins` on a
225
+ // host in org Q could therefore push P's pending — or rejected — bundle
226
+ // onto Q's live public site, for free.
225
227
  //
226
- // Same-org is a NECESSARY condition, never a sufficient one: the role
227
- // check still has to pass, so this narrows the waiver and cannot widen
228
- // it. `strictNullChecks` is off repo-wide, hence the explicit `!== ''`
229
- // rather than a truthiness test — a listing with no `profileId` is
230
- // owned by nobody and must waive nothing.
231
- const publisherOrgId = String((_listing_profileId = listing.profileId) != null ? _listing_profileId : '');
232
- const installTargetOrgId = String((_membership_orgId = membership.orgId) != null ? _membership_orgId : '');
233
- const ownsListing = publisherOrgId !== '' && installTargetOrgId === publisherOrgId && await canActAsPublisher(firestore, decoded.uid, listing.profileId);
228
+ // Same-org is a NECESSARY condition, never a sufficient one: the
229
+ // membership check still has to pass, so this narrows the waiver and
230
+ // cannot widen it. A listing with no `profileId` is owned by nobody and
231
+ // waives nothing. Every install door asks this same definition now
232
+ // (`ownsListingHere`, AGL-4047); plugins were the first to need it.
233
+ const ownsListing = await ownsListingHere(firestore, {
234
+ uid: decoded.uid,
235
+ publisherOrgId: listing.profileId,
236
+ targetOrgId: membership.orgId
237
+ });
234
238
  // Private plugins are installable ONLY by the owning org (AGL-968).
235
239
  // This is what makes private mean private; the UI merely hides them.
236
240
  if (isPrivateListing(listing) && !ownsListing) {
@@ -263,7 +267,7 @@ export const installPluginHandler = async (req, res)=>{
263
267
  // permission gate above — never a request-body field — so this is the
264
268
  // workspace the install actually lands in, and the only one a purchase
265
269
  // can entitle here.
266
- buyerOrgId: (_membership_orgId1 = membership.orgId) != null ? _membership_orgId1 : '',
270
+ buyerOrgId: (_membership_orgId = membership.orgId) != null ? _membership_orgId : '',
267
271
  listingId,
268
272
  priceUsd,
269
273
  ownsListing
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport {\n resolveOrgIdForHost, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n isCompatibleHostAbi,\n isFirstPartyPlugin,\n isPluginRevoked,\n newestInstallableVersion,\n PLUGIN_HOST_ABI_VERSION,\n type PluginApiHandler,\n type PluginRevocation,\n} from '@aglyn/aglyn/server'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { pinnedProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\nimport {\n isListingBrowsable,\n isPrivateListing,\n isVersionApproved,\n listingArtifactType,\n} from '../model/marketplace'\nimport { compareArtifactVersions } from '../model/update-state'\n\n/**\n * Installs (or upgrades) a marketplace plugin into a host (AGL-45), pinning a\n * version per AGL-43 §3.4. Writes `hosts/{hostId}/installs/{listingId}`\n * with the exact `{version, sha256}` — upgrades are explicit (a new pin),\n * so a publisher can never swap the code a consumer runs. Access mirrors\n * component installs: free, purchased (webhook-written), or own listing.\n * The pin references the immutable artifact; the sandboxed loader fetches\n * and integrity-checks it at render time.\n *\n * Enablement sync (AGL-424): marketplace plugins ride the same\n * `org.enabledPlugins` switchboard first-party plugins use — but ONLY for\n * workspaces that have explicitly configured the field. Install appends\n * the listing id there; uninstall removes it once no pin (org or acting\n * host) remains. Workspaces that never touched the switchboard stay\n * default-open (absent field ⇒ installs load), so pre-sync installs keep\n * working.\n */\n\n/** Keeps `org.enabledPlugins` in step with install pins (AGL-424). */\nasync function syncEnabledPlugins(\n firestore: FirebaseFirestore.Firestore,\n orgId: string | null,\n hostId: string,\n listingId: string,\n action: 'add' | 'remove-if-unpinned',\n): Promise<void> {\n if (!orgId) return\n // `enabledPlugins` is a flat mix of first-party bundle ids and marketplace\n // listing ids (AGL-777). A listing id is a Firestore doc id, so it can't\n // normally collide with a bundle name — but guard anyway: an install sync\n // must NEVER arrayUnion/arrayRemove a bundle id, which would silently\n // enable or disable a platform plugin as a side effect of a purchase.\n if (isFirstPartyPlugin(listingId)) return\n const orgRef = firestore.collection('orgs').doc(orgId)\n const configured = (await orgRef.get()).get('enabledPlugins')\n // Absent field = default-open workspace; nothing to sync.\n if (!Array.isArray(configured)) return\n if (action === 'add') {\n if (configured.includes(listingId)) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayUnion(listingId),\n })\n return\n }\n const [orgPin, hostPin] = await Promise.all([\n orgRef.collection('installs').doc(listingId).get(),\n firestore\n .collection('hosts')\n .doc(hostId)\n .collection('installs')\n .doc(listingId)\n .get(),\n ])\n if (orgPin.exists || hostPin.exists) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayRemove(listingId),\n })\n}\n/**\n * Tells the live sites a pin changed (AGL-3741).\n *\n * The tenant caches each site's resolved plugins under its\n * `tenant-data:{hostId}` tag, and an install or uninstall used to reach live\n * pages only when that entry expired — which is why it expired every minute,\n * re-reading the install pins, listings, versions and revocations of every\n * site sixty times an hour. An org-tier pin applies to every site in the org,\n * so it is those sites that are told. Best effort and never throws, like the\n * revocation fan-out it shares: the pin itself is already written.\n */\nasync function announcePinChange(\n firestore: FirebaseFirestore.Firestore,\n options: { scope: 'org' | 'host'; hostId: string; orgId: string | null; reason: string },\n): Promise<void> {\n try {\n const hostIds =\n options.scope === 'org' && options.orgId\n ? (\n await firestore.collection('hosts').where('orgId', '==', options.orgId).get()\n ).docs.map((doc) => doc.id)\n : [options.hostId]\n await dropPluginSiteCache({ hostIds, reason: options.reason })\n } catch (error) {\n console.error('[marketplace] the sites running a changed pin were not told', error)\n }\n}\n\nexport const installPluginHandler: 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 // Optional explicit version for upgrade/downgrade; defaults to latest.\n const requestedVersion = req.body?.version\n ? String(req.body.version)\n : undefined\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 // Uninstalls (AGL-237/424). Org pins are API-managed (rules deny\n // client writes); host uninstalls also come through here so the\n // enablement sync runs. Plugin-owned data is never deleted — only the\n // pin (and the switchboard entry when no pin remains anywhere).\n if (req.body?.action === 'uninstall') {\n const orgId = await resolveOrgIdForHost(hostId)\n if (req.body?.scope === 'org' && !orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n const pinRef =\n req.body?.scope === 'org'\n ? orgId\n ? firestore.collection('orgs').doc(orgId).collection('installs').doc(listingId)\n : null\n : firestore.collection('hosts').doc(hostId).collection('installs').doc(listingId)\n // Active-install accounting (AGL-880): decrement only when a pin actually\n // existed, so a repeat/no-op uninstall never over-counts. `installCount`\n // is a cumulative all-time total and is deliberately left untouched.\n const pinSnapshot = pinRef ? await pinRef.get() : null\n const pinExisted = pinSnapshot?.exists ?? false\n // Read before the delete: the version being left is what the per-version\n // tally has to decrement, and it is gone the moment the pin is.\n const pinnedVersion = pinExisted ? pinSnapshot?.get('version') : null\n if (pinRef) await pinRef.delete()\n if (pinExisted) {\n await announcePinChange(firestore, {\n scope: req.body?.scope === 'org' ? 'org' : 'host',\n hostId,\n orgId,\n reason: `marketplace listing ${listingId} uninstalled`,\n })\n }\n if (pinExisted) {\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: pinnedVersion,\n to: null,\n })\n await firestore\n .runTransaction(async (tx) => {\n const snapshot = await tx.get(listingRef)\n const current = Number(snapshot.get('activeInstalls') ?? 0)\n tx.update(listingRef, { activeInstalls: Math.max(0, current - 1) })\n })\n .catch(() => undefined)\n }\n await syncEnabledPlugins(\n firestore,\n orgId,\n hostId,\n listingId,\n 'remove-if-unpinned',\n )\n return res.status(200).json({ ok: true })\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 too (AGL-948) — the revocation\n // check below stops a hidden listing anyway, but only while the\n // revocation stands, and a takedown is not conditional on it.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'plugin'\n ) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // The publisher installs their own listing for free, and may install\n // an unapproved version of it to test. Org-owned now (AGL-652), so this\n // is a role check — comparing a uid to an org id would never match.\n //\n // TWO facts, correlated (AGL-2484). `canActAsPublisher` answers a\n // question about the PUBLISHER org alone; the install TARGET is a\n // different org, resolved from the host by the permission gate above.\n // Nothing tied them together, so one flag computed from org P waived\n // three gates on an install landing in org Q: review, version\n // resolution, and payment. A user who administers publisher org P and\n // holds `installPlugins` on a host in org Q could therefore push P's\n // pending — or rejected — bundle onto Q's live public site, for free.\n //\n // Same-org is a NECESSARY condition, never a sufficient one: the role\n // check still has to pass, so this narrows the waiver and cannot widen\n // it. `strictNullChecks` is off repo-wide, hence the explicit `!== ''`\n // rather than a truthiness test — a listing with no `profileId` is\n // owned by nobody and must waive nothing.\n const publisherOrgId = String(listing.profileId ?? '')\n const installTargetOrgId = String(membership.orgId ?? '')\n const ownsListing =\n publisherOrgId !== '' &&\n installTargetOrgId === publisherOrgId &&\n (await canActAsPublisher(firestore, decoded.uid, listing.profileId))\n\n // Private plugins are installable ONLY by the owning org (AGL-968).\n // This is what makes private mean private; the UI merely hides them.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // Installs enforce the same review state the marketplace browses by\n // (AGL-965). `isListingBrowsable` used to be applied ONLY by the browse\n // UI, so a listing that was never reviewed, was rejected, or has since\n // been de-listed could still be installed by anyone who knew its id and\n // posted here directly — which also made \"delist\" a suggestion rather\n // than a control. The publisher keeps installing their own listing to\n // test it before review.\n if (!isListingBrowsable(listing) && !isPrivateListing(listing) && !ownsListing) {\n return res\n .status(409)\n .json({ error: 'This plugin is not available for install' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\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 // Installs resolve the newest APPROVED version, never `latestVersion`\n // (AGL-966). Before this, publishing v1.0.1 to a verified listing put\n // unreviewed code in front of every workspace immediately: the listing\n // kept its status, the queue never surfaced the update, and this line\n // pinned whatever had just been uploaded. A pending version is now\n // simply not offered, and the previously approved one keeps installing.\n //\n // The publisher may still install their own unapproved version — that\n // is how you test a plugin before submitting it. It reaches only their\n // own sites because `ownsListing` now REQUIRES the install to land in\n // the publishing org (AGL-2484); this sentence used to be an assertion\n // about code that did not check.\n // Read the kill switch BEFORE resolving, not after (AGL-2368).\n //\n // The order used to be: pick the newest APPROVED version, then 409 if that\n // one happens to be revoked. Revocation was never an input to the choice,\n // only a veto on it — so stopping one build stopped the whole listing.\n // v2.0.0 revoked with v1.0.0 approved and installable resolved to v2.0.0\n // and answered 409, and `requirePurchase` above has already taken the\n // buyer's money by then. A per-version revocation is deliberately NOT\n // listing-wide (`isListingWideRevocation`); that path made it so anyway.\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n\n let version = requestedVersion ?? ''\n let versionData: any\n if (version) {\n versionData = (\n await listingRef.collection('pluginVersions').doc(version).get()\n ).data()\n if (!isVersionApproved(versionData) && !ownsListing) {\n return res.status(409).json({\n error: 'That version has not passed review yet',\n reviewState: versionData?.reviewState ?? 'unknown',\n })\n }\n } else {\n const approvedSnapshot = await listingRef\n .collection('pluginVersions')\n .orderBy('publishedAt', 'desc')\n .limit(25)\n .get()\n const candidates = approvedSnapshot.docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n reviewState: doc.get('reviewState'),\n publishedAt: doc.get('publishedAt'),\n data: doc.data(),\n }))\n // The SHARED predicate (AGL-2306/2368): approved AND not revoked, which\n // is the same function `repairLatestApprovedVersion` writes the listing\n // mirror from. Two readings of \"what is on offer\" is how the badge and\n // the route came to disagree; one function is how they cannot.\n //\n // Note this also orders by version rather than by `publishedAt`, which\n // is what the mirror has always done — so the version this route hands\n // over is now the version the listing page advertised.\n const newestVersion = newestInstallableVersion(\n candidates,\n revocation,\n compareArtifactVersions,\n )\n const newest = newestVersion\n ? (candidates.find((entry) => entry.version === newestVersion) as\n | { version: string; data: any }\n | undefined)\n : undefined\n // The publisher testing their own listing falls back to latest.\n const fallback = ownsListing\n ? candidates.find(\n (entry) => entry.version === String(listing.latestVersion ?? ''),\n )\n : undefined\n const chosen = newest ?? fallback\n if (!chosen) {\n return res.status(409).json({\n error: 'This plugin has no reviewed version available to install',\n })\n }\n version = chosen.version\n versionData = (chosen as { data?: unknown }).data\n }\n if (!versionData?.sha256 || !versionData?.manifest) {\n return res.status(404).json({ error: 'Unknown plugin version' })\n }\n\n // Kill switch (AGL-43 §3.5): a revoked version can't be installed.\n // Shares the predicate with the loaders (AGL-1085) so \"installable\" and\n // \"runnable\" cannot drift apart.\n //\n // Still checked after resolution, and still load-bearing: the unpinned\n // branch can no longer choose a revoked version, but an EXPLICIT pin and\n // the publisher's `latestVersion` fallback both can, and a listing-wide\n // takedown revokes whatever either of them names.\n if (isPluginRevoked(revocation, version)) {\n return res\n .status(409)\n .json({ error: 'This plugin version has been revoked' })\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Install tier (AGL-237): org-extending plugins install once for every\n // host in the org; host-only plugins stay pinned to the host.\n const scope = req.body?.scope === 'org' ? 'org' : 'host'\n let installRef = hostRef.collection('installs').doc(listingId)\n let pinOrgId: string | null = null\n if (scope === 'org') {\n const orgId = await resolveOrgIdForHost(hostId)\n pinOrgId = orgId ?? null\n if (!orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n installRef = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('installs')\n .doc(listingId)\n }\n const existing = await installRef.get()\n await installRef.set(\n {\n listingId,\n profileId: listing.profileId,\n displayName: listing.displayName,\n pluginId: versionData.manifest.id,\n version,\n sha256: versionData.sha256,\n objectPath: versionData.objectPath,\n manifest: versionData.manifest,\n // The same provenance stamp every other artifact type now carries\n // (AGL-1015), so one reader answers \"where did this come from\" for\n // pinned and copied artifacts alike. The flat pin fields stay: they\n // are what the loader reads.\n installedFrom: pinnedProvenance({\n listingId,\n listing,\n version,\n sha256: versionData.sha256,\n artifactType: 'plugin',\n }),\n ...(existing.exists ? {} : { installedAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n await announcePinChange(firestore, {\n scope,\n hostId,\n orgId: pinOrgId,\n reason: `marketplace listing ${listingId} installed at ${version}`,\n })\n if (!existing.exists) {\n await listingRef\n .update({\n // installCount is the cumulative all-time total; activeInstalls\n // mirrors live pins and is decremented on uninstall (AGL-880).\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n activeInstalls: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n // Per-version tally (AGL-1036). This runs on EVERY pin write, not just the\n // first: a re-pin is the common case and the one the listing-level counters\n // above are blind to — the pin already exists, so nothing there moves while\n // the install changes version underneath.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: existing.exists ? existing.get('version') : null,\n to: version,\n })\n\n // Enablement sync (AGL-424): the new install loads on next visit for\n // workspaces with a configured switchboard.\n await syncEnabledPlugins(\n firestore,\n await resolveOrgIdForHost(hostId),\n hostId,\n listingId,\n 'add',\n )\n\n // ABI heads-up (AGL-429): the pin succeeds (explicit choice), but the\n // loaders will refuse an incompatible bundle — tell the installer now.\n const manifestHostAbi = Number(versionData.manifest?.hostAbi)\n const hostAbiWarning =\n Number.isInteger(manifestHostAbi) &&\n manifestHostAbi > 0 &&\n !isCompatibleHostAbi(manifestHostAbi)\n ? `Built for host ABI ${manifestHostAbi}; this platform runs ` +\n `${PLUGIN_HOST_ABI_VERSION}. The plugin will not load until the ` +\n 'publisher ships a compatible version.'\n : undefined\n\n return res.status(200).json({\n installed: true,\n upgraded: existing.exists,\n version,\n ...(hostAbiWarning ? { hostAbiWarning } : {}),\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","resolveOrgIdForHost","firebaseAdmin","isCompatibleHostAbi","isFirstPartyPlugin","isPluginRevoked","newestInstallableVersion","PLUGIN_HOST_ABI_VERSION","dropPluginSiteCache","resolveOrgPermissions","canActAsPublisher","requirePurchase","pinnedProvenance","recordVersionMove","isPublisherSecurityLocked","isListingBrowsable","isPrivateListing","isVersionApproved","listingArtifactType","compareArtifactVersions","syncEnabledPlugins","firestore","orgId","hostId","listingId","action","orgRef","collection","doc","configured","get","Array","isArray","includes","update","enabledPlugins","FieldValue","arrayUnion","orgPin","hostPin","Promise","all","exists","arrayRemove","announcePinChange","options","hostIds","scope","where","docs","map","id","reason","error","console","installPluginHandler","req","res","method","status","json","String","body","requestedVersion","version","undefined","authorization","headers","idToken","startsWith","slice","length","listing","membership","versionData","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","hostRef","hostSnapshot","pinRef","pinSnapshot","pinExisted","pinnedVersion","delete","listingRef","artifactType","from","to","runTransaction","tx","snapshot","current","Number","activeInstalls","Math","max","catch","ok","listingSnapshot","data","deletedAt","hiddenAt","profileId","publisherOrgId","installTargetOrgId","ownsListing","priceUsd","unpaid","buyerUid","buyerOrgId","revocation","reviewState","approvedSnapshot","orderBy","limit","candidates","publishedAt","newestVersion","newest","find","entry","fallback","latestVersion","chosen","sha256","manifest","now","serverTimestamp","installRef","pinOrgId","existing","set","displayName","pluginId","objectPath","installedFrom","installedAt","updatedAt","merge","installCount","increment","manifestHostAbi","hostAbi","hostAbiWarning","isInteger","installed","upgraded"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport {\n resolveOrgIdForHost, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n isCompatibleHostAbi,\n isFirstPartyPlugin,\n isPluginRevoked,\n newestInstallableVersion,\n PLUGIN_HOST_ABI_VERSION,\n type PluginApiHandler,\n type PluginRevocation,\n} from '@aglyn/aglyn/server'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { ownsListingHere } from './own-listing'\nimport { requirePurchase } from './purchase-entitlement'\nimport { pinnedProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\nimport {\n isListingBrowsable,\n isPrivateListing,\n isVersionApproved,\n listingArtifactType,\n} from '../model/marketplace'\nimport { compareArtifactVersions } from '../model/update-state'\n\n/**\n * Installs (or upgrades) a marketplace plugin into a host (AGL-45), pinning a\n * version per AGL-43 §3.4. Writes `hosts/{hostId}/installs/{listingId}`\n * with the exact `{version, sha256}` — upgrades are explicit (a new pin),\n * so a publisher can never swap the code a consumer runs. Access mirrors\n * component installs: free, purchased (webhook-written), or own listing.\n * The pin references the immutable artifact; the sandboxed loader fetches\n * and integrity-checks it at render time.\n *\n * Enablement sync (AGL-424): marketplace plugins ride the same\n * `org.enabledPlugins` switchboard first-party plugins use — but ONLY for\n * workspaces that have explicitly configured the field. Install appends\n * the listing id there; uninstall removes it once no pin (org or acting\n * host) remains. Workspaces that never touched the switchboard stay\n * default-open (absent field ⇒ installs load), so pre-sync installs keep\n * working.\n */\n\n/** Keeps `org.enabledPlugins` in step with install pins (AGL-424). */\nasync function syncEnabledPlugins(\n firestore: FirebaseFirestore.Firestore,\n orgId: string | null,\n hostId: string,\n listingId: string,\n action: 'add' | 'remove-if-unpinned',\n): Promise<void> {\n if (!orgId) return\n // `enabledPlugins` is a flat mix of first-party bundle ids and marketplace\n // listing ids (AGL-777). A listing id is a Firestore doc id, so it can't\n // normally collide with a bundle name — but guard anyway: an install sync\n // must NEVER arrayUnion/arrayRemove a bundle id, which would silently\n // enable or disable a platform plugin as a side effect of a purchase.\n if (isFirstPartyPlugin(listingId)) return\n const orgRef = firestore.collection('orgs').doc(orgId)\n const configured = (await orgRef.get()).get('enabledPlugins')\n // Absent field = default-open workspace; nothing to sync.\n if (!Array.isArray(configured)) return\n if (action === 'add') {\n if (configured.includes(listingId)) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayUnion(listingId),\n })\n return\n }\n const [orgPin, hostPin] = await Promise.all([\n orgRef.collection('installs').doc(listingId).get(),\n firestore\n .collection('hosts')\n .doc(hostId)\n .collection('installs')\n .doc(listingId)\n .get(),\n ])\n if (orgPin.exists || hostPin.exists) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayRemove(listingId),\n })\n}\n/**\n * Tells the live sites a pin changed (AGL-3741).\n *\n * The tenant caches each site's resolved plugins under its\n * `tenant-data:{hostId}` tag, and an install or uninstall used to reach live\n * pages only when that entry expired — which is why it expired every minute,\n * re-reading the install pins, listings, versions and revocations of every\n * site sixty times an hour. An org-tier pin applies to every site in the org,\n * so it is those sites that are told. Best effort and never throws, like the\n * revocation fan-out it shares: the pin itself is already written.\n */\nasync function announcePinChange(\n firestore: FirebaseFirestore.Firestore,\n options: { scope: 'org' | 'host'; hostId: string; orgId: string | null; reason: string },\n): Promise<void> {\n try {\n const hostIds =\n options.scope === 'org' && options.orgId\n ? (\n await firestore.collection('hosts').where('orgId', '==', options.orgId).get()\n ).docs.map((doc) => doc.id)\n : [options.hostId]\n await dropPluginSiteCache({ hostIds, reason: options.reason })\n } catch (error) {\n console.error('[marketplace] the sites running a changed pin were not told', error)\n }\n}\n\nexport const installPluginHandler: 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 // Optional explicit version for upgrade/downgrade; defaults to latest.\n const requestedVersion = req.body?.version\n ? String(req.body.version)\n : undefined\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 // Uninstalls (AGL-237/424). Org pins are API-managed (rules deny\n // client writes); host uninstalls also come through here so the\n // enablement sync runs. Plugin-owned data is never deleted — only the\n // pin (and the switchboard entry when no pin remains anywhere).\n if (req.body?.action === 'uninstall') {\n const orgId = await resolveOrgIdForHost(hostId)\n if (req.body?.scope === 'org' && !orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n const pinRef =\n req.body?.scope === 'org'\n ? orgId\n ? firestore.collection('orgs').doc(orgId).collection('installs').doc(listingId)\n : null\n : firestore.collection('hosts').doc(hostId).collection('installs').doc(listingId)\n // Active-install accounting (AGL-880): decrement only when a pin actually\n // existed, so a repeat/no-op uninstall never over-counts. `installCount`\n // is a cumulative all-time total and is deliberately left untouched.\n const pinSnapshot = pinRef ? await pinRef.get() : null\n const pinExisted = pinSnapshot?.exists ?? false\n // Read before the delete: the version being left is what the per-version\n // tally has to decrement, and it is gone the moment the pin is.\n const pinnedVersion = pinExisted ? pinSnapshot?.get('version') : null\n if (pinRef) await pinRef.delete()\n if (pinExisted) {\n await announcePinChange(firestore, {\n scope: req.body?.scope === 'org' ? 'org' : 'host',\n hostId,\n orgId,\n reason: `marketplace listing ${listingId} uninstalled`,\n })\n }\n if (pinExisted) {\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: pinnedVersion,\n to: null,\n })\n await firestore\n .runTransaction(async (tx) => {\n const snapshot = await tx.get(listingRef)\n const current = Number(snapshot.get('activeInstalls') ?? 0)\n tx.update(listingRef, { activeInstalls: Math.max(0, current - 1) })\n })\n .catch(() => undefined)\n }\n await syncEnabledPlugins(\n firestore,\n orgId,\n hostId,\n listingId,\n 'remove-if-unpinned',\n )\n return res.status(200).json({ ok: true })\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 too (AGL-948) — the revocation\n // check below stops a hidden listing anyway, but only while the\n // revocation stands, and a takedown is not conditional on it.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'plugin'\n ) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // The publisher installs their own listing for free, and may install\n // an unapproved version of it to test. Org-owned now (AGL-652), so this\n // asks the publishing org's members (comparing a uid to an org id would\n // never match). Membership, not a permission (AGL-4047): it is a fact\n // about who published the listing.\n //\n // TWO facts, correlated (AGL-2484). Membership answers a question about\n // the PUBLISHER org alone; the install TARGET is a different org,\n // resolved from the host by the permission gate above. Nothing tied them\n // together, so one flag computed from org P waived three gates on an\n // install landing in org Q: review, version resolution, and payment. A\n // user who administers publisher org P and holds `installPlugins` on a\n // host in org Q could therefore push P's pending — or rejected — bundle\n // onto Q's live public site, for free.\n //\n // Same-org is a NECESSARY condition, never a sufficient one: the\n // membership check still has to pass, so this narrows the waiver and\n // cannot widen it. A listing with no `profileId` is owned by nobody and\n // waives nothing. Every install door asks this same definition now\n // (`ownsListingHere`, AGL-4047); plugins were the first to need it.\n const ownsListing = await ownsListingHere(firestore, {\n uid: decoded.uid,\n publisherOrgId: listing.profileId,\n targetOrgId: membership.orgId,\n })\n\n // Private plugins are installable ONLY by the owning org (AGL-968).\n // This is what makes private mean private; the UI merely hides them.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // Installs enforce the same review state the marketplace browses by\n // (AGL-965). `isListingBrowsable` used to be applied ONLY by the browse\n // UI, so a listing that was never reviewed, was rejected, or has since\n // been de-listed could still be installed by anyone who knew its id and\n // posted here directly — which also made \"delist\" a suggestion rather\n // than a control. The publisher keeps installing their own listing to\n // test it before review.\n if (!isListingBrowsable(listing) && !isPrivateListing(listing) && !ownsListing) {\n return res\n .status(409)\n .json({ error: 'This plugin is not available for install' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\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 // Installs resolve the newest APPROVED version, never `latestVersion`\n // (AGL-966). Before this, publishing v1.0.1 to a verified listing put\n // unreviewed code in front of every workspace immediately: the listing\n // kept its status, the queue never surfaced the update, and this line\n // pinned whatever had just been uploaded. A pending version is now\n // simply not offered, and the previously approved one keeps installing.\n //\n // The publisher may still install their own unapproved version — that\n // is how you test a plugin before submitting it. It reaches only their\n // own sites because `ownsListing` now REQUIRES the install to land in\n // the publishing org (AGL-2484); this sentence used to be an assertion\n // about code that did not check.\n // Read the kill switch BEFORE resolving, not after (AGL-2368).\n //\n // The order used to be: pick the newest APPROVED version, then 409 if that\n // one happens to be revoked. Revocation was never an input to the choice,\n // only a veto on it — so stopping one build stopped the whole listing.\n // v2.0.0 revoked with v1.0.0 approved and installable resolved to v2.0.0\n // and answered 409, and `requirePurchase` above has already taken the\n // buyer's money by then. A per-version revocation is deliberately NOT\n // listing-wide (`isListingWideRevocation`); that path made it so anyway.\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n\n let version = requestedVersion ?? ''\n let versionData: any\n if (version) {\n versionData = (\n await listingRef.collection('pluginVersions').doc(version).get()\n ).data()\n if (!isVersionApproved(versionData) && !ownsListing) {\n return res.status(409).json({\n error: 'That version has not passed review yet',\n reviewState: versionData?.reviewState ?? 'unknown',\n })\n }\n } else {\n const approvedSnapshot = await listingRef\n .collection('pluginVersions')\n .orderBy('publishedAt', 'desc')\n .limit(25)\n .get()\n const candidates = approvedSnapshot.docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n reviewState: doc.get('reviewState'),\n publishedAt: doc.get('publishedAt'),\n data: doc.data(),\n }))\n // The SHARED predicate (AGL-2306/2368): approved AND not revoked, which\n // is the same function `repairLatestApprovedVersion` writes the listing\n // mirror from. Two readings of \"what is on offer\" is how the badge and\n // the route came to disagree; one function is how they cannot.\n //\n // Note this also orders by version rather than by `publishedAt`, which\n // is what the mirror has always done — so the version this route hands\n // over is now the version the listing page advertised.\n const newestVersion = newestInstallableVersion(\n candidates,\n revocation,\n compareArtifactVersions,\n )\n const newest = newestVersion\n ? (candidates.find((entry) => entry.version === newestVersion) as\n | { version: string; data: any }\n | undefined)\n : undefined\n // The publisher testing their own listing falls back to latest.\n const fallback = ownsListing\n ? candidates.find(\n (entry) => entry.version === String(listing.latestVersion ?? ''),\n )\n : undefined\n const chosen = newest ?? fallback\n if (!chosen) {\n return res.status(409).json({\n error: 'This plugin has no reviewed version available to install',\n })\n }\n version = chosen.version\n versionData = (chosen as { data?: unknown }).data\n }\n if (!versionData?.sha256 || !versionData?.manifest) {\n return res.status(404).json({ error: 'Unknown plugin version' })\n }\n\n // Kill switch (AGL-43 §3.5): a revoked version can't be installed.\n // Shares the predicate with the loaders (AGL-1085) so \"installable\" and\n // \"runnable\" cannot drift apart.\n //\n // Still checked after resolution, and still load-bearing: the unpinned\n // branch can no longer choose a revoked version, but an EXPLICIT pin and\n // the publisher's `latestVersion` fallback both can, and a listing-wide\n // takedown revokes whatever either of them names.\n if (isPluginRevoked(revocation, version)) {\n return res\n .status(409)\n .json({ error: 'This plugin version has been revoked' })\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Install tier (AGL-237): org-extending plugins install once for every\n // host in the org; host-only plugins stay pinned to the host.\n const scope = req.body?.scope === 'org' ? 'org' : 'host'\n let installRef = hostRef.collection('installs').doc(listingId)\n let pinOrgId: string | null = null\n if (scope === 'org') {\n const orgId = await resolveOrgIdForHost(hostId)\n pinOrgId = orgId ?? null\n if (!orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n installRef = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('installs')\n .doc(listingId)\n }\n const existing = await installRef.get()\n await installRef.set(\n {\n listingId,\n profileId: listing.profileId,\n displayName: listing.displayName,\n pluginId: versionData.manifest.id,\n version,\n sha256: versionData.sha256,\n objectPath: versionData.objectPath,\n manifest: versionData.manifest,\n // The same provenance stamp every other artifact type now carries\n // (AGL-1015), so one reader answers \"where did this come from\" for\n // pinned and copied artifacts alike. The flat pin fields stay: they\n // are what the loader reads.\n installedFrom: pinnedProvenance({\n listingId,\n listing,\n version,\n sha256: versionData.sha256,\n artifactType: 'plugin',\n }),\n ...(existing.exists ? {} : { installedAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n await announcePinChange(firestore, {\n scope,\n hostId,\n orgId: pinOrgId,\n reason: `marketplace listing ${listingId} installed at ${version}`,\n })\n if (!existing.exists) {\n await listingRef\n .update({\n // installCount is the cumulative all-time total; activeInstalls\n // mirrors live pins and is decremented on uninstall (AGL-880).\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n activeInstalls: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n // Per-version tally (AGL-1036). This runs on EVERY pin write, not just the\n // first: a re-pin is the common case and the one the listing-level counters\n // above are blind to — the pin already exists, so nothing there moves while\n // the install changes version underneath.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: existing.exists ? existing.get('version') : null,\n to: version,\n })\n\n // Enablement sync (AGL-424): the new install loads on next visit for\n // workspaces with a configured switchboard.\n await syncEnabledPlugins(\n firestore,\n await resolveOrgIdForHost(hostId),\n hostId,\n listingId,\n 'add',\n )\n\n // ABI heads-up (AGL-429): the pin succeeds (explicit choice), but the\n // loaders will refuse an incompatible bundle — tell the installer now.\n const manifestHostAbi = Number(versionData.manifest?.hostAbi)\n const hostAbiWarning =\n Number.isInteger(manifestHostAbi) &&\n manifestHostAbi > 0 &&\n !isCompatibleHostAbi(manifestHostAbi)\n ? `Built for host ABI ${manifestHostAbi}; this platform runs ` +\n `${PLUGIN_HOST_ABI_VERSION}. The plugin will not load until the ` +\n 'publisher ships a compatible version.'\n : undefined\n\n return res.status(200).json({\n installed: true,\n upgraded: existing.exists,\n version,\n ...(hostAbiWarning ? { hostAbiWarning } : {}),\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","resolveOrgIdForHost","firebaseAdmin","isCompatibleHostAbi","isFirstPartyPlugin","isPluginRevoked","newestInstallableVersion","PLUGIN_HOST_ABI_VERSION","dropPluginSiteCache","resolveOrgPermissions","ownsListingHere","requirePurchase","pinnedProvenance","recordVersionMove","isPublisherSecurityLocked","isListingBrowsable","isPrivateListing","isVersionApproved","listingArtifactType","compareArtifactVersions","syncEnabledPlugins","firestore","orgId","hostId","listingId","action","orgRef","collection","doc","configured","get","Array","isArray","includes","update","enabledPlugins","FieldValue","arrayUnion","orgPin","hostPin","Promise","all","exists","arrayRemove","announcePinChange","options","hostIds","scope","where","docs","map","id","reason","error","console","installPluginHandler","req","res","method","status","json","String","body","requestedVersion","version","undefined","authorization","headers","idToken","startsWith","slice","length","listing","membership","versionData","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","hostRef","hostSnapshot","pinRef","pinSnapshot","pinExisted","pinnedVersion","delete","listingRef","artifactType","from","to","runTransaction","tx","snapshot","current","Number","activeInstalls","Math","max","catch","ok","listingSnapshot","data","deletedAt","hiddenAt","profileId","ownsListing","publisherOrgId","targetOrgId","priceUsd","unpaid","buyerUid","buyerOrgId","revocation","reviewState","approvedSnapshot","orderBy","limit","candidates","publishedAt","newestVersion","newest","find","entry","fallback","latestVersion","chosen","sha256","manifest","now","serverTimestamp","installRef","pinOrgId","existing","set","displayName","pluginId","objectPath","installedFrom","installedAt","updatedAt","merge","installCount","increment","manifestHostAbi","hostAbi","hostAbiWarning","isInteger","installed","upgraded"],"mappings":"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"}
@@ -23,7 +23,7 @@ import { artifactCreateListKeys, artifactDeleteListKeys } from "@aglyn/aglyn/app
23
23
  import { checkQuota, createResourceUid, encodeStoredNodes } from "@aglyn/aglyn/server";
24
24
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
25
25
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
26
- import { canActAsPublisher } from "./publisher-profile.js";
26
+ import { ownsListingHere } from "./own-listing.js";
27
27
  import { requirePurchase } from "./purchase-entitlement.js";
28
28
  import { recordInstallProvenance } from "./provenance.js";
29
29
  import { TEMPLATE_SOURCE_TYPE } from "../constants/template-source.js";
@@ -119,10 +119,13 @@ import { isPrivateListing, listingArtifactType } from "../model/marketplace.js";
119
119
  });
120
120
  }
121
121
  const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
122
- // The publisher installs their own listing for free. Org-owned now
123
- // (AGL-652), so this is a role check — comparing a uid to an org id
124
- // would never match and would charge publishers for their own work.
125
- const ownsListing = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
122
+ // Free for the publisher's members on its OWN sites only (AGL-2484,
123
+ // AGL-4047); into another workspace they are buyers like anyone else.
124
+ const ownsListing = await ownsListingHere(firestore, {
125
+ uid: decoded.uid,
126
+ publisherOrgId: listing.profileId,
127
+ targetOrgId: membership.orgId
128
+ });
126
129
  // Private listings install ONLY for the owning org (AGL-2290).
127
130
  //
128
131
  // `install-plugin.ts` has carried this since AGL-968; the other six never
@@ -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 { 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
+ {"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 { ownsListingHere } from './own-listing'\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 // Free for the publisher's members on its OWN sites only (AGL-2484,\n // AGL-4047); into another workspace they are buyers like anyone else.\n const ownsListing = await ownsListingHere(firestore, {\n uid: decoded.uid,\n publisherOrgId: listing.profileId,\n targetOrgId: membership.orgId,\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","ownsListingHere","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","publisherOrgId","targetOrgId","orgId","unpaid","buyerUid","buyerOrgId","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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"}
@@ -22,7 +22,7 @@ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
22
22
  import { runThemeLibraryAction } from "@aglyn/tenant-data-admin/server/theme-library-write";
23
23
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
24
24
  import { isPrivateListing, listingArtifactType } from "../model/marketplace.js";
25
- import { canActAsPublisher } from "./publisher-profile.js";
25
+ import { ownsListingHere } from "./own-listing.js";
26
26
  import { requirePurchase } from "./purchase-entitlement.js";
27
27
  import { hasDivergedFromBase, recordInstallProvenance } from "./provenance.js";
28
28
  import { recordVersionMove } from "./version-stats.js";
@@ -210,7 +210,13 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
210
210
  });
211
211
  }
212
212
  const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
213
- const ownsListing = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
213
+ // Free for the publisher's members on its OWN sites only (AGL-2484,
214
+ // AGL-4047); into another workspace they are buyers like anyone else.
215
+ const ownsListing = await ownsListingHere(firestore, {
216
+ uid: decoded.uid,
217
+ publisherOrgId: listing.profileId,
218
+ targetOrgId: membership.orgId
219
+ });
214
220
  // Private listings install ONLY for the owning org (AGL-2290).
215
221
  //
216
222
  // `install-plugin.ts` has carried this since AGL-968; the other six never