@aglyn/plugins-marketplace 1.10.0 → 1.12.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 (81) 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-publisher-permissions.d.ts +26 -0
  23. package/src/lib/model/marketplace-publisher-permissions.js +64 -0
  24. package/src/lib/model/marketplace-publisher-permissions.js.map +1 -0
  25. package/src/lib/model/marketplace.js +2 -1
  26. package/src/lib/model/marketplace.js.map +1 -1
  27. package/src/lib/model/own-listing.d.ts +52 -0
  28. package/src/lib/model/own-listing.js +72 -0
  29. package/src/lib/model/own-listing.js.map +1 -0
  30. package/src/lib/model/starter-query.js +2 -0
  31. package/src/lib/model/starter-query.js.map +1 -1
  32. package/src/lib/server/connect.js +11 -5
  33. package/src/lib/server/connect.js.map +1 -1
  34. package/src/lib/server/install-dataset-schema.js +10 -2
  35. package/src/lib/server/install-dataset-schema.js.map +1 -1
  36. package/src/lib/server/install-email-starter.js +8 -2
  37. package/src/lib/server/install-email-starter.js.map +1 -1
  38. package/src/lib/server/install-email-template.js +8 -2
  39. package/src/lib/server/install-email-template.js.map +1 -1
  40. package/src/lib/server/install-layout.js +8 -2
  41. package/src/lib/server/install-layout.js.map +1 -1
  42. package/src/lib/server/install-plugin.js +24 -20
  43. package/src/lib/server/install-plugin.js.map +1 -1
  44. package/src/lib/server/install-template.js +8 -5
  45. package/src/lib/server/install-template.js.map +1 -1
  46. package/src/lib/server/install-theme.js +8 -2
  47. package/src/lib/server/install-theme.js.map +1 -1
  48. package/src/lib/server/install.js +10 -5
  49. package/src/lib/server/install.js.map +1 -1
  50. package/src/lib/server/listing-versions.js +24 -9
  51. package/src/lib/server/listing-versions.js.map +1 -1
  52. package/src/lib/server/own-listing.d.ts +46 -0
  53. package/src/lib/server/own-listing.js +52 -0
  54. package/src/lib/server/own-listing.js.map +1 -0
  55. package/src/lib/server/preview-image.d.ts +1 -1
  56. package/src/lib/server/preview-image.js +9 -8
  57. package/src/lib/server/preview-image.js.map +1 -1
  58. package/src/lib/server/publish-plugin.js +17 -19
  59. package/src/lib/server/publish-plugin.js.map +1 -1
  60. package/src/lib/server/publisher-permissions.d.ts +63 -0
  61. package/src/lib/server/publisher-permissions.js +51 -0
  62. package/src/lib/server/publisher-permissions.js.map +1 -0
  63. package/src/lib/server/publisher-profile-save.d.ts +4 -2
  64. package/src/lib/server/publisher-profile-save.js +12 -5
  65. package/src/lib/server/publisher-profile-save.js.map +1 -1
  66. package/src/lib/server/publisher-profile.d.ts +18 -6
  67. package/src/lib/server/publisher-profile.js +19 -8
  68. package/src/lib/server/publisher-profile.js.map +1 -1
  69. package/src/lib/server/reviews.js +6 -5
  70. package/src/lib/server/reviews.js.map +1 -1
  71. package/src/lib/server/sale-risk.d.ts +0 -6
  72. package/src/lib/server/sale-risk.js +0 -9
  73. package/src/lib/server/sale-risk.js.map +1 -1
  74. package/src/lib/server/sale.js +10 -9
  75. package/src/lib/server/sale.js.map +1 -1
  76. package/src/lib/server/site-starter-source.js +43 -17
  77. package/src/lib/server/site-starter-source.js.map +1 -1
  78. package/src/lib/server/update-artifact.js +11 -4
  79. package/src/lib/server/update-artifact.js.map +1 -1
  80. package/src/lib/server/verification-request.js +23 -7
  81. package/src/lib/server/verification-request.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-theme.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { describeTheme } from '@aglyn/aglyn/app-utils/site-theme'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { runThemeLibraryAction } from '@aglyn/tenant-data-admin/server/theme-library-write'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs a marketplace theme onto a site (AGL-1020).\n *\n * Themes are the exception to \"installing never changes a running site\". A\n * template or a layout can land inert in a library because it is one thing\n * among many; a theme IS the site's appearance, and installing one into a\n * library where it does nothing would be a control that appears to work and\n * does not. So this writes `hosts/{hostId}.theme` and the site repaints.\n *\n * Which makes reversibility the requirement, not a nicety. Two things provide\n * it, and neither is a confirmation dialog:\n *\n * * The theme being replaced is kept verbatim in `themeReplaced`, so `revert`\n * restores exactly what was there — including a hand-built theme that was\n * never a marketplace artifact and exists nowhere else.\n * * `reset` clears the theme entirely, which is the \"use the default theme\"\n * the issue asks to keep first-class. It is a distinct action from revert\n * because \"back to how it was\" and \"back to stock\" are different intents and\n * guessing between them is how people lose work.\n *\n * `preview` changes nothing and returns what the swap would do, so the\n * confirmation can name it rather than showing a JSON blob.\n *\n * Every action that writes the theme then drops the site's cached pages\n * (AGL-3386). The theme styles every page and nothing publishes it, so\n * without the drop \"the site repaints\" meant \"within the hour\".\n */\nexport const installThemeHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const action = ['revert', 'reset', 'preview', 'clear-overrides'].includes(\n req.body?.action,\n )\n ? (req.body.action as\n | 'revert'\n | 'reset'\n | 'preview'\n | 'clear-overrides')\n : 'install'\n const listingId = String(req.body?.listingId ?? '')\n if (!hostId || (action === 'install' && !listingId)) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const currentTheme = hostSnapshot.get('theme') ?? null\n // Awaited before the response, because a serverless function may be\n // frozen once it has answered. Never throws: a drop that fails leaves the\n // theme saved and the hour-long cache as the backstop.\n const repaintLiveSite = (action: string) =>\n dropPluginSiteCache({\n hostIds: [hostId],\n reason: `marketplace theme ${action}`,\n })\n\n // ---- reset: back to the platform default ----\n // Picking the default from the site's theme library (AGL-3404): the theme\n // being left is filed in the library, edits and all, so resetting stays\n // reversible without a second copy of it on the host.\n if (action === 'reset') {\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, {\n action: 'select',\n target: { kind: 'default' },\n }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n await repaintLiveSite('reset')\n return res.status(200).json({ reset: true })\n }\n\n // ---- clear-overrides: drop this site's changes, keep the theme ----\n // The third way back, and the narrowest: \"reset to the publisher's\n // version\" is deleting the patch — the whole point of owning the patch\n // rather than the copy (AGL-1019). The theme itself is untouched.\n //\n // The library's `restore`, which keeps the site's own dark-scheme setting\n // (AGL-3404): that is a decision about the site, not an edit to the theme.\n if (action === 'clear-overrides') {\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, { action: 'restore' }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n await repaintLiveSite('overrides cleared')\n return res.status(200).json({ cleared: true })\n }\n\n // ---- revert: back to whatever this site had before the last swap ----\n if (action === 'revert') {\n const replaced = hostSnapshot.get('themeReplaced')\n if (!replaced) {\n return res.status(409).json({\n error: 'There is no previous theme to go back to on this site.',\n })\n }\n await hostRef.set(\n {\n theme: replaced.theme ?? firebaseAdmin.firestore.FieldValue.delete(),\n ...(replaced.installedFrom\n ? { themeInstalledFrom: replaced.installedFrom }\n : { themeInstalledFrom: firebaseAdmin.firestore.FieldValue.delete() }),\n // The overrides that were live alongside that theme come back with\n // it. Reverting the base and keeping a patch authored against the\n // theme being reverted FROM is the one combination nobody asked for.\n ...(replaced.override\n ? { themeOverride: replaced.override }\n : { themeOverride: firebaseAdmin.firestore.FieldValue.delete() }),\n themeReplaced: firebaseAdmin.firestore.FieldValue.delete(),\n // A `themeReplaced` predates the library (every library switch\n // clears it), so the theme it restores is read the way a\n // pre-library site's is — from the fields above, not a selection.\n themeSelection: firebaseAdmin.firestore.FieldValue.delete(),\n updatedAt: now,\n },\n { merge: true },\n )\n await repaintLiveSite('reverted')\n return res.status(200).json({ reverted: true })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'theme'\n ) {\n return res.status(404).json({ error: 'Unknown theme' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const theme = versionSnapshot.get('theme')\n if (!theme || !Object.keys(theme).length) {\n return res.status(500).json({ error: 'Theme version missing' })\n }\n\n // ---- preview: say what the swap changes, change nothing ----\n if (action === 'preview') {\n return res.status(200).json({\n incoming: describeTheme(theme),\n current: describeTheme(currentTheme),\n // A site with no theme is on the platform default, so there is nothing\n // to lose and the confirmation should not imply there is.\n replaces: Boolean(currentTheme && Object.keys(currentTheme).length),\n version: listing.latestVersion ?? null,\n })\n }\n\n // ---- install ----\n // Replacing a theme the workspace has edited destroys those edits (AGL-1018).\n // Unlike a component this is not recoverable by looking in a library — the\n // theme lives on one field on one document — so the refusal matters more.\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: hostSnapshot.get('themeInstalledFrom.sha256'),\n current: currentTheme,\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'This site’s theme has been edited since it was installed. Review ' +\n 'the update to see what would change.',\n diverged: true,\n })\n }\n\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'theme',\n content: theme,\n })\n const previousVersion =\n hostSnapshot.get('themeInstalledFrom.version') ?? null\n\n // The theme joins the site's library and is picked (AGL-3404). The theme\n // it replaces is filed in the library with its edits, so installing is\n // reversible by picking it again — the job `themeReplaced` used to do\n // for one theme, for every theme.\n //\n // Edits belong to the theme they were made on. They used to SURVIVE a\n // swap (AGL-1021), because the theme being replaced was about to exist\n // nowhere and carrying its patch forward was the lesser loss. With the\n // library keeping it, a patch tuned for one design no longer lands on\n // another: each theme gets its own back when it is picked again. An\n // UPDATE of the theme already picked keeps its patch exactly as stored,\n // hash included, so the editor can still say it predates this version.\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, {\n action: 'install',\n listingId,\n name: String(listing.displayName ?? '') || 'Marketplace theme',\n theme,\n installedFrom: { ...provenance.installedFrom },\n }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n\n await repaintLiveSite('installed')\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'theme',\n from: previousVersion,\n to: listing.latestVersion,\n })\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n\n return res.status(200).json({\n installed: true,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n applied: describeTheme(theme),\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Theme install failed' })\n }\n}\n\nexport default installThemeHandler\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","describeTheme","dropPluginSiteCache","firebaseAdmin","runThemeLibraryAction","resolveOrgPermissions","isPrivateListing","listingArtifactType","canActAsPublisher","requirePurchase","hasDivergedFromBase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installThemeHandler","req","res","method","status","json","error","hostId","String","body","action","includes","listingId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","now","FieldValue","serverTimestamp","currentTheme","repaintLiveSite","hostIds","reason","plan","runTransaction","tx","target","kind","ok","reset","cleared","replaced","set","theme","delete","installedFrom","themeInstalledFrom","override","themeOverride","themeReplaced","themeSelection","updatedAt","merge","reverted","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","Object","keys","incoming","current","replaces","Boolean","version","diverged","sha256","mode","provenance","artifactType","content","previousVersion","name","displayName","from","to","update","installCount","increment","catch","installed","baseStored","applied","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-theme.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { describeTheme } from '@aglyn/aglyn/app-utils/site-theme'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { runThemeLibraryAction } from '@aglyn/tenant-data-admin/server/theme-library-write'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport { 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 theme onto a site (AGL-1020).\n *\n * Themes are the exception to \"installing never changes a running site\". A\n * template or a layout can land inert in a library because it is one thing\n * among many; a theme IS the site's appearance, and installing one into a\n * library where it does nothing would be a control that appears to work and\n * does not. So this writes `hosts/{hostId}.theme` and the site repaints.\n *\n * Which makes reversibility the requirement, not a nicety. Two things provide\n * it, and neither is a confirmation dialog:\n *\n * * The theme being replaced is kept verbatim in `themeReplaced`, so `revert`\n * restores exactly what was there — including a hand-built theme that was\n * never a marketplace artifact and exists nowhere else.\n * * `reset` clears the theme entirely, which is the \"use the default theme\"\n * the issue asks to keep first-class. It is a distinct action from revert\n * because \"back to how it was\" and \"back to stock\" are different intents and\n * guessing between them is how people lose work.\n *\n * `preview` changes nothing and returns what the swap would do, so the\n * confirmation can name it rather than showing a JSON blob.\n *\n * Every action that writes the theme then drops the site's cached pages\n * (AGL-3386). The theme styles every page and nothing publishes it, so\n * without the drop \"the site repaints\" meant \"within the hour\".\n */\nexport const installThemeHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const action = ['revert', 'reset', 'preview', 'clear-overrides'].includes(\n req.body?.action,\n )\n ? (req.body.action as\n | 'revert'\n | 'reset'\n | 'preview'\n | 'clear-overrides')\n : 'install'\n const listingId = String(req.body?.listingId ?? '')\n if (!hostId || (action === 'install' && !listingId)) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const currentTheme = hostSnapshot.get('theme') ?? null\n // Awaited before the response, because a serverless function may be\n // frozen once it has answered. Never throws: a drop that fails leaves the\n // theme saved and the hour-long cache as the backstop.\n const repaintLiveSite = (action: string) =>\n dropPluginSiteCache({\n hostIds: [hostId],\n reason: `marketplace theme ${action}`,\n })\n\n // ---- reset: back to the platform default ----\n // Picking the default from the site's theme library (AGL-3404): the theme\n // being left is filed in the library, edits and all, so resetting stays\n // reversible without a second copy of it on the host.\n if (action === 'reset') {\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, {\n action: 'select',\n target: { kind: 'default' },\n }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n await repaintLiveSite('reset')\n return res.status(200).json({ reset: true })\n }\n\n // ---- clear-overrides: drop this site's changes, keep the theme ----\n // The third way back, and the narrowest: \"reset to the publisher's\n // version\" is deleting the patch — the whole point of owning the patch\n // rather than the copy (AGL-1019). The theme itself is untouched.\n //\n // The library's `restore`, which keeps the site's own dark-scheme setting\n // (AGL-3404): that is a decision about the site, not an edit to the theme.\n if (action === 'clear-overrides') {\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, { action: 'restore' }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n await repaintLiveSite('overrides cleared')\n return res.status(200).json({ cleared: true })\n }\n\n // ---- revert: back to whatever this site had before the last swap ----\n if (action === 'revert') {\n const replaced = hostSnapshot.get('themeReplaced')\n if (!replaced) {\n return res.status(409).json({\n error: 'There is no previous theme to go back to on this site.',\n })\n }\n await hostRef.set(\n {\n theme: replaced.theme ?? firebaseAdmin.firestore.FieldValue.delete(),\n ...(replaced.installedFrom\n ? { themeInstalledFrom: replaced.installedFrom }\n : { themeInstalledFrom: firebaseAdmin.firestore.FieldValue.delete() }),\n // The overrides that were live alongside that theme come back with\n // it. Reverting the base and keeping a patch authored against the\n // theme being reverted FROM is the one combination nobody asked for.\n ...(replaced.override\n ? { themeOverride: replaced.override }\n : { themeOverride: firebaseAdmin.firestore.FieldValue.delete() }),\n themeReplaced: firebaseAdmin.firestore.FieldValue.delete(),\n // A `themeReplaced` predates the library (every library switch\n // clears it), so the theme it restores is read the way a\n // pre-library site's is — from the fields above, not a selection.\n themeSelection: firebaseAdmin.firestore.FieldValue.delete(),\n updatedAt: now,\n },\n { merge: true },\n )\n await repaintLiveSite('reverted')\n return res.status(200).json({ reverted: true })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'theme'\n ) {\n return res.status(404).json({ error: 'Unknown theme' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // 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 theme = versionSnapshot.get('theme')\n if (!theme || !Object.keys(theme).length) {\n return res.status(500).json({ error: 'Theme version missing' })\n }\n\n // ---- preview: say what the swap changes, change nothing ----\n if (action === 'preview') {\n return res.status(200).json({\n incoming: describeTheme(theme),\n current: describeTheme(currentTheme),\n // A site with no theme is on the platform default, so there is nothing\n // to lose and the confirmation should not imply there is.\n replaces: Boolean(currentTheme && Object.keys(currentTheme).length),\n version: listing.latestVersion ?? null,\n })\n }\n\n // ---- install ----\n // Replacing a theme the workspace has edited destroys those edits (AGL-1018).\n // Unlike a component this is not recoverable by looking in a library — the\n // theme lives on one field on one document — so the refusal matters more.\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: hostSnapshot.get('themeInstalledFrom.sha256'),\n current: currentTheme,\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'This site’s theme has been edited since it was installed. Review ' +\n 'the update to see what would change.',\n diverged: true,\n })\n }\n\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'theme',\n content: theme,\n })\n const previousVersion =\n hostSnapshot.get('themeInstalledFrom.version') ?? null\n\n // The theme joins the site's library and is picked (AGL-3404). The theme\n // it replaces is filed in the library with its edits, so installing is\n // reversible by picking it again — the job `themeReplaced` used to do\n // for one theme, for every theme.\n //\n // Edits belong to the theme they were made on. They used to SURVIVE a\n // swap (AGL-1021), because the theme being replaced was about to exist\n // nowhere and carrying its patch forward was the lesser loss. With the\n // library keeping it, a patch tuned for one design no longer lands on\n // another: each theme gets its own back when it is picked again. An\n // UPDATE of the theme already picked keeps its patch exactly as stored,\n // hash included, so the editor can still say it predates this version.\n const plan = await firestore.runTransaction((tx) =>\n runThemeLibraryAction(tx, hostRef, {\n action: 'install',\n listingId,\n name: String(listing.displayName ?? '') || 'Marketplace theme',\n theme,\n installedFrom: { ...provenance.installedFrom },\n }),\n )\n if (plan.ok === false) return res.status(plan.status).json({ error: plan.error })\n\n await repaintLiveSite('installed')\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'theme',\n from: previousVersion,\n to: listing.latestVersion,\n })\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n\n return res.status(200).json({\n installed: true,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n applied: describeTheme(theme),\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Theme install failed' })\n }\n}\n\nexport default installThemeHandler\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","describeTheme","dropPluginSiteCache","firebaseAdmin","runThemeLibraryAction","resolveOrgPermissions","isPrivateListing","listingArtifactType","ownsListingHere","requirePurchase","hasDivergedFromBase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installThemeHandler","req","res","method","status","json","error","hostId","String","body","action","includes","listingId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","now","FieldValue","serverTimestamp","currentTheme","repaintLiveSite","hostIds","reason","plan","runTransaction","tx","target","kind","ok","reset","cleared","replaced","set","theme","delete","installedFrom","themeInstalledFrom","override","themeOverride","themeReplaced","themeSelection","updatedAt","merge","reverted","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","publisherOrgId","targetOrgId","orgId","unpaid","buyerUid","buyerOrgId","versionSnapshot","latestVersion","Object","keys","incoming","current","replaces","Boolean","version","diverged","sha256","mode","provenance","artifactType","content","previousVersion","name","displayName","from","to","update","installCount","increment","catch","installed","baseStored","applied","console"],"mappings":"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"}
@@ -25,7 +25,7 @@ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
25
25
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
26
26
  import { isPrivateListing } from "../model/marketplace.js";
27
27
  import { readPublishedProps } from "../model/marketplace-props.js";
28
- import { canActAsPublisher } from "./publisher-profile.js";
28
+ import { ownsListingHere } from "./own-listing.js";
29
29
  import { hasDivergedFromBase, recordInstallProvenance } from "./provenance.js";
30
30
  import { requirePurchase } from "./purchase-entitlement.js";
31
31
  import { recordVersionMove } from "./version-stats.js";
@@ -145,10 +145,15 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
145
145
  });
146
146
  }
147
147
  const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
148
- // The publisher installs their own listing for free. Org-owned now
149
- // (AGL-652), so this is a role check — comparing a uid to an org id
150
- // would never match and would charge publishers for their own work.
151
- const ownsListing = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
148
+ // The publisher's members install its own listing free, on its OWN sites
149
+ // only (AGL-2484, AGL-4047): a member installing it into another
150
+ // workspace is a buyer there. `membership.orgId` is the workspace the
151
+ // install lands in, resolved from the site by the gate above.
152
+ const ownsListing = await ownsListingHere(firestore, {
153
+ uid: decoded.uid,
154
+ publisherOrgId: listing.profileId,
155
+ targetOrgId: membership.orgId
156
+ });
152
157
  // Private listings install ONLY for the owning org (AGL-2290).
153
158
  //
154
159
  // `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.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n checkQuota,\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { isPrivateListing } from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\nimport { canActAsPublisher } from './publisher-profile'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs (or updates) a marketplace listing into a host (AGL-44/46).\n * Server-side because version snapshots are not client-readable — paid\n * content would otherwise be free to anyone who read the subcollection.\n * Access: the listing is free, the caller bought it (webhook-written\n * purchase record), or the caller published it. Installing copies the\n * latest version into `hosts/{hostId}/components` with `marketplace` source\n * metadata; re-installing updates the same component doc in place.\n */\nexport const installHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n // Org-role permission gate (AGL-238).\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n // The SAME gate the console's own create path applies to this exact\n // document (AGL-2072). `/api/hosts/resources` creates\n // `hosts/{hostId}/components` and refuses past the plan's allowance,\n // because a reusable component renders on the LIVE SITE and the gate has\n // to be server-enforced rather than merely hidden in the console\n // (AGL-473). This route writes a byte-for-byte equivalent doc into the\n // same collection, so it asks the same question: before AGL-2072 it asked\n // nothing, and a free org got the working feature its own console would\n // have refused.\n //\n // The question is a COUNT since AGL-3615: `componentsPerHost`, Free 1 and\n // every paid plan unlimited, no longer the `reusableComponents` flag. A\n // re-install lands on the copy the site already holds, so it creates\n // nothing and is not counted; only a first install of the listing is.\n //\n // Ahead of `requirePurchase`, deliberately, and matching\n // `install-dataset-schema.ts:89`: refusing on plan AFTER taking someone's\n // money for the listing would be the worse order. The whole org doc is\n // read (`getOrgDoc` projects nothing), so `resolveEffectivePlan` sees the\n // subscription status and a LAPSED org is counted exactly as a free one\n // is — the gate is re-asked per install, never inherited from whatever\n // plan was in force when an earlier copy was installed.\n const org = (await getOrgForHost(hostId))?.org\n const componentsRef = hostRef.collection('components')\n const existing = await componentsRef\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\n if (existing.empty) {\n // LIVE components only, as `/api/hosts/resources` counts them: a\n // deletion stamps `deletedAt` and keeps the document. Nothing is read\n // on a plan whose allowance is unlimited.\n const limit = checkQuota(org as any, 'componentsPerHost', 0).limit\n const used = Number.isFinite(limit)\n ? (await componentsRef.select('deletedAt').get()).docs.filter(\n (entry) => entry.get('deletedAt') == null,\n ).length\n : 0\n const quota = checkQuota(org as any, 'componentsPerHost', used)\n if (!quota.allowed) {\n return res.status(403).json({\n error:\n `Your plan includes ${quota.limit} reusable component${quota.limit === 1 ? '' : 's'} — ` +\n 'upgrade in Billing for more',\n })\n }\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId))\n ) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // The publisher installs their own listing for free. Org-owned now\n // (AGL-652), so this is a role check — comparing a uid to an org id\n // would never match and would charge publishers for their own work.\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase no longer entitles (AGL-1546). The gate this\n // route once inlined is now shared (AGL-1699): it was the only one of\n // eight paid-content paths that had learned the refund rule.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const version = versionSnapshot.data() as any\n if (!version?.nodes || !version?.rootId) {\n return res.status(500).json({ error: 'Listing version missing' })\n }\n\n // The properties the tree binds to (AGL-2933), held to the sanitizer again\n // on the way in. `undefined` for a version published before they were\n // carried, which leaves a re-installed copy's own properties in place.\n const props = readPublishedProps(version.props)\n\n const componentRef = existing.empty\n ? componentsRef.doc(createResourceUid())\n : existing.docs[0].ref\n // Re-installing over a copy the workspace has EDITED is the silent\n // destructive overwrite AGL-1018 exists to stop. The install still happens\n // when it is asked for explicitly (`mode: 'replace'`, which the update\n // dialog sends after showing what goes), but it is never the default.\n if (!existing.empty) {\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: existing.docs[0].get('installedFrom.sha256'),\n current: {\n rootId: existing.docs[0].get('rootId'),\n // DECODED, because the comparison is by value: the base snapshot\n // recorded at install is a node map and `hasDivergedFromBase`\n // hashes a stable stringification of both sides. A stored `Bytes`\n // matches no map, so an untouched copy would read as edited and\n // every re-install would 409 (AGL-1151).\n nodes: decodeStoredNodes(existing.docs[0].get('nodes')),\n },\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'Your copy of this component has been edited since it was ' +\n 'installed. Review the update to see what would change.',\n diverged: true,\n })\n }\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Provenance + base snapshot (AGL-1015). The snapshot holds only the\n // vendored tree — the display name and description come from the listing\n // and the user may rename their copy, so including them would make every\n // rename read as a diverged component.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'component',\n content: { rootId: version.rootId, nodes: version.nodes },\n })\n await componentRef.set(\n {\n displayName: listing.displayName,\n ...(listing.description && { description: listing.description }),\n rootId: version.rootId,\n // Compressed at rest, matching what a promote writes onto this same\n // document (AGL-1151). The provenance base above keeps the decoded\n // map: it is compared by value, never rendered.\n nodes: Buffer.from(encodeStoredNodes(version.nodes ?? {})!),\n // Beside the tree, as a publish writes them onto this document. The\n // first version the console mints for this component copies them\n // from here (AGL-2932).\n ...(props && { props }),\n deletedAt: null,\n // The keys the components list finds it by (AGL-3321): the listing's\n // name, and — on a first install — the stored kind; a re-install\n // keeps the kind the copy already has.\n ...artifactCreateListKeys('components', {\n displayName: listing.displayName,\n // The description written above — or, on a re-install (a merge),\n // the one the copy already has — so the stored-null default\n // (AGL-3680) never overwrites it.\n description:\n listing.description ||\n (existing.empty ? null : (existing.docs[0].get('description') ?? null)),\n kind: existing.empty ? undefined : existing.docs[0].get('kind'),\n }),\n marketplace: {\n listingId,\n profileId: listing.profileId,\n version: listing.latestVersion,\n },\n installedFrom: provenance.installedFrom,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n // Per-version tally (AGL-1036): a re-install moves this copy off whatever\n // version it was on and onto the latest.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'component',\n from: existing.empty\n ? null\n : (existing.docs[0].get('installedFrom.version') ??\n existing.docs[0].get('marketplace.version')),\n to: listing.latestVersion,\n })\n // First installs count toward the browse \"Most installed\" sort\n // (AGL-95); updates don't inflate it.\n if (existing.empty) {\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n return res.status(200).json({\n installed: true,\n updated: !existing.empty,\n version: listing.latestVersion,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","artifactCreateListKeys","checkQuota","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","readPublishedProps","canActAsPublisher","hasDivergedFromBase","recordInstallProvenance","requirePurchase","recordVersionMove","isPublisherSecurityLocked","installHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","version","existing","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","org","componentsRef","where","limit","empty","used","Number","isFinite","select","docs","filter","entry","quota","allowed","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","nodes","rootId","props","componentRef","ref","diverged","sha256","current","mode","now","FieldValue","serverTimestamp","provenance","artifactType","content","set","displayName","description","Buffer","from","kind","marketplace","installedFrom","createdAt","updatedAt","merge","to","update","installCount","increment","catch","installed","updated","baseStored","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n checkQuota,\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { isPrivateListing } from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\nimport { ownsListingHere } from './own-listing'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs (or updates) a marketplace listing into a host (AGL-44/46).\n * Server-side because version snapshots are not client-readable — paid\n * content would otherwise be free to anyone who read the subcollection.\n * Access: the listing is free, the caller bought it (webhook-written\n * purchase record), or the caller published it. Installing copies the\n * latest version into `hosts/{hostId}/components` with `marketplace` source\n * metadata; re-installing updates the same component doc in place.\n */\nexport const installHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n // Org-role permission gate (AGL-238).\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n // The SAME gate the console's own create path applies to this exact\n // document (AGL-2072). `/api/hosts/resources` creates\n // `hosts/{hostId}/components` and refuses past the plan's allowance,\n // because a reusable component renders on the LIVE SITE and the gate has\n // to be server-enforced rather than merely hidden in the console\n // (AGL-473). This route writes a byte-for-byte equivalent doc into the\n // same collection, so it asks the same question: before AGL-2072 it asked\n // nothing, and a free org got the working feature its own console would\n // have refused.\n //\n // The question is a COUNT since AGL-3615: `componentsPerHost`, Free 1 and\n // every paid plan unlimited, no longer the `reusableComponents` flag. A\n // re-install lands on the copy the site already holds, so it creates\n // nothing and is not counted; only a first install of the listing is.\n //\n // Ahead of `requirePurchase`, deliberately, and matching\n // `install-dataset-schema.ts:89`: refusing on plan AFTER taking someone's\n // money for the listing would be the worse order. The whole org doc is\n // read (`getOrgDoc` projects nothing), so `resolveEffectivePlan` sees the\n // subscription status and a LAPSED org is counted exactly as a free one\n // is — the gate is re-asked per install, never inherited from whatever\n // plan was in force when an earlier copy was installed.\n const org = (await getOrgForHost(hostId))?.org\n const componentsRef = hostRef.collection('components')\n const existing = await componentsRef\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\n if (existing.empty) {\n // LIVE components only, as `/api/hosts/resources` counts them: a\n // deletion stamps `deletedAt` and keeps the document. Nothing is read\n // on a plan whose allowance is unlimited.\n const limit = checkQuota(org as any, 'componentsPerHost', 0).limit\n const used = Number.isFinite(limit)\n ? (await componentsRef.select('deletedAt').get()).docs.filter(\n (entry) => entry.get('deletedAt') == null,\n ).length\n : 0\n const quota = checkQuota(org as any, 'componentsPerHost', used)\n if (!quota.allowed) {\n return res.status(403).json({\n error:\n `Your plan includes ${quota.limit} reusable component${quota.limit === 1 ? '' : 's'} — ` +\n 'upgrade in Billing for more',\n })\n }\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId))\n ) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // The publisher's members install its own listing free, on its OWN sites\n // only (AGL-2484, AGL-4047): a member installing it into another\n // workspace is a buyer there. `membership.orgId` is the workspace the\n // install lands in, resolved from the site by the gate above.\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 no longer entitles (AGL-1546). The gate this\n // route once inlined is now shared (AGL-1699): it was the only one of\n // eight paid-content paths that had learned the refund rule.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const version = versionSnapshot.data() as any\n if (!version?.nodes || !version?.rootId) {\n return res.status(500).json({ error: 'Listing version missing' })\n }\n\n // The properties the tree binds to (AGL-2933), held to the sanitizer again\n // on the way in. `undefined` for a version published before they were\n // carried, which leaves a re-installed copy's own properties in place.\n const props = readPublishedProps(version.props)\n\n const componentRef = existing.empty\n ? componentsRef.doc(createResourceUid())\n : existing.docs[0].ref\n // Re-installing over a copy the workspace has EDITED is the silent\n // destructive overwrite AGL-1018 exists to stop. The install still happens\n // when it is asked for explicitly (`mode: 'replace'`, which the update\n // dialog sends after showing what goes), but it is never the default.\n if (!existing.empty) {\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: existing.docs[0].get('installedFrom.sha256'),\n current: {\n rootId: existing.docs[0].get('rootId'),\n // DECODED, because the comparison is by value: the base snapshot\n // recorded at install is a node map and `hasDivergedFromBase`\n // hashes a stable stringification of both sides. A stored `Bytes`\n // matches no map, so an untouched copy would read as edited and\n // every re-install would 409 (AGL-1151).\n nodes: decodeStoredNodes(existing.docs[0].get('nodes')),\n },\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'Your copy of this component has been edited since it was ' +\n 'installed. Review the update to see what would change.',\n diverged: true,\n })\n }\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Provenance + base snapshot (AGL-1015). The snapshot holds only the\n // vendored tree — the display name and description come from the listing\n // and the user may rename their copy, so including them would make every\n // rename read as a diverged component.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'component',\n content: { rootId: version.rootId, nodes: version.nodes },\n })\n await componentRef.set(\n {\n displayName: listing.displayName,\n ...(listing.description && { description: listing.description }),\n rootId: version.rootId,\n // Compressed at rest, matching what a promote writes onto this same\n // document (AGL-1151). The provenance base above keeps the decoded\n // map: it is compared by value, never rendered.\n nodes: Buffer.from(encodeStoredNodes(version.nodes ?? {})!),\n // Beside the tree, as a publish writes them onto this document. The\n // first version the console mints for this component copies them\n // from here (AGL-2932).\n ...(props && { props }),\n deletedAt: null,\n // The keys the components list finds it by (AGL-3321): the listing's\n // name, and — on a first install — the stored kind; a re-install\n // keeps the kind the copy already has.\n ...artifactCreateListKeys('components', {\n displayName: listing.displayName,\n // The description written above — or, on a re-install (a merge),\n // the one the copy already has — so the stored-null default\n // (AGL-3680) never overwrites it.\n description:\n listing.description ||\n (existing.empty ? null : (existing.docs[0].get('description') ?? null)),\n kind: existing.empty ? undefined : existing.docs[0].get('kind'),\n }),\n marketplace: {\n listingId,\n profileId: listing.profileId,\n version: listing.latestVersion,\n },\n installedFrom: provenance.installedFrom,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n // Per-version tally (AGL-1036): a re-install moves this copy off whatever\n // version it was on and onto the latest.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'component',\n from: existing.empty\n ? null\n : (existing.docs[0].get('installedFrom.version') ??\n existing.docs[0].get('marketplace.version')),\n to: listing.latestVersion,\n })\n // First installs count toward the browse \"Most installed\" sort\n // (AGL-95); updates don't inflate it.\n if (existing.empty) {\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n return res.status(200).json({\n installed: true,\n updated: !existing.empty,\n version: listing.latestVersion,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","artifactCreateListKeys","checkQuota","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","readPublishedProps","ownsListingHere","hasDivergedFromBase","recordInstallProvenance","requirePurchase","recordVersionMove","isPublisherSecurityLocked","installHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","version","existing","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","org","componentsRef","where","limit","empty","used","Number","isFinite","select","docs","filter","entry","quota","allowed","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","ownsListing","publisherOrgId","targetOrgId","orgId","unpaid","buyerUid","buyerOrgId","versionSnapshot","latestVersion","nodes","rootId","props","componentRef","ref","diverged","sha256","current","mode","now","FieldValue","serverTimestamp","provenance","artifactType","content","set","displayName","description","Buffer","from","kind","marketplace","installedFrom","createdAt","updatedAt","merge","to","update","installCount","increment","catch","installed","updated","baseStored","console"],"mappings":"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"}
@@ -19,7 +19,8 @@ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
19
  import { isVersionApproved, listingArtifactType } from "../model/marketplace.js";
20
20
  import { reconcileInstallTallies, versionCollectionFor } from "./version-stats.js";
21
21
  import { verifiedLivePins } from "./install-pin-counts.js";
22
- import { canActAsPublisher } from "./publisher-profile.js";
22
+ import { MARKETPLACE_MANAGE_LISTINGS_PERMISSION, marketplacePublisherPermissionRefusal } from "../model/marketplace-publisher-permissions.js";
23
+ import { publisherPermissionVerdict } from "./publisher-permissions.js";
23
24
  import { attestationsForBytes } from "../model/publisher-attestation.js";
24
25
  import { compareArtifactVersions } from "../model/update-state.js";
25
26
  /**
@@ -83,14 +84,28 @@ import { compareArtifactVersions } from "../model/update-state.js";
83
84
  error: 'Unknown listing'
84
85
  });
85
86
  }
86
- // Listings are org-owned (AGL-652), so this is an org-membership question,
87
- // not a uid comparison — the same gate the publish and edit paths use.
88
- const isPublisher = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
89
- if (!isPublisher && decoded['staff'] !== true) {
90
- // 404, not 403: whether a listing exists is not a stranger's business.
91
- return res.status(404).json({
92
- error: 'Unknown listing'
93
- });
87
+ // Listings are org-owned (AGL-652), so this is a question about the
88
+ // publishing org, not a uid comparison. It asks "Manage published
89
+ // listings" (`marketplace.manageListings`, AGL-4047), the key the edit
90
+ // paths ask, whose default is the owner and admins this asked for by role
91
+ // before. Staff read any listing's versions, as before.
92
+ if (decoded['staff'] !== true) {
93
+ const verdict = await publisherPermissionVerdict(decoded.uid, listing.profileId, MARKETPLACE_MANAGE_LISTINGS_PERMISSION);
94
+ if (verdict === 'not-a-member') {
95
+ // 404, not 403: whether a listing exists is not a stranger's business.
96
+ return res.status(404).json({
97
+ error: 'Unknown listing'
98
+ });
99
+ }
100
+ if (verdict === 'refused') {
101
+ // A member sees the listing in their own workspace already, so the
102
+ // refusal names the permission they are missing.
103
+ return res.status(403).json({
104
+ error: marketplacePublisherPermissionRefusal(MARKETPLACE_MANAGE_LISTINGS_PERMISSION),
105
+ reason: 'permission',
106
+ permission: MARKETPLACE_MANAGE_LISTINGS_PERMISSION
107
+ });
108
+ }
94
109
  }
95
110
  const artifactType = listingArtifactType(listing);
96
111
  const snapshot = await listingRef.collection(versionCollectionFor(artifactType)).orderBy('publishedAt', 'desc').limit(20).get();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/listing-versions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isPluginRevoked,\n newestInstallableVersion,\n type PluginApiHandler,\n type PluginRevocation,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n isVersionApproved,\n listingArtifactType,\n} from '../model/marketplace'\n\nimport {\n reconcileInstallTallies,\n versionCollectionFor,\n type LivePinTally,\n type ReconciledInstallTallies,\n} from './version-stats'\nimport { verifiedLivePins } from './install-pin-counts'\nimport { canActAsPublisher } from './publisher-profile'\nimport { attestationsForBytes } from '../model/publisher-attestation'\nimport { compareArtifactVersions } from '../model/update-state'\n\n/**\n * One reconciled tally per version id, plus the totals (AGL-1418).\n *\n * Built from EVERY version document, never from a filtered subset: the buyer\n * branch shows approved versions only, and reconciling against that subset\n * would hand a pending version's installs to an approved one.\n */\nfunction reconcileFromSnapshot(\n docs: Array<{ id: string; get: (field: string) => unknown }>,\n listing: Record<string, unknown> | undefined,\n pins?: LivePinTally | null,\n): { byVersion: Map<string, { installCount: number; activeInstalls: number }> } & ReconciledInstallTallies {\n const reconciled = reconcileInstallTallies(\n docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n installCount: Number(doc.get('installCount') ?? 0),\n activeInstalls: Number(doc.get('activeInstalls') ?? 0),\n })),\n {\n installCount: Number(listing?.['installCount'] ?? 0),\n activeInstalls: Number(listing?.['activeInstalls'] ?? 0),\n },\n pins,\n )\n const byVersion = new Map<string, { installCount: number; activeInstalls: number }>()\n reconciled.versions.forEach((entry) =>\n byVersion.set(entry.version, {\n installCount: entry.installCount,\n activeInstalls: entry.activeInstalls,\n }),\n )\n return { ...reconciled, byVersion }\n}\n\n/**\n * The publisher's own view of their versions (AGL-1079).\n *\n * AGL-966 made review per-version and got the mechanics right — a new\n * version enters `pending`, the previously approved one keeps installing,\n * nothing ships past a reviewer. The publisher was told none of it: an\n * email on the outcome, and in between a listing row identical to the one\n * they had before they published.\n *\n * Separate from the buyer projection above on purpose. `reviewState`, the\n * rejection reason, the sha and what was attested are the publisher's own\n * business and nobody else's — a buyer has no need to read why a version\n * they will never be offered was sent back. So this branch demands a token\n * and org membership rather than widening what the public route returns.\n */\nconst publisherVersions: PluginApiHandler = async (req, res) => {\n const listingId = String(req.query?.listingId ?? '')\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data()\n if (!listing || listing.deletedAt) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // Listings are org-owned (AGL-652), so this is an org-membership question,\n // not a uid comparison — the same gate the publish and edit paths use.\n const isPublisher = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n if (!isPublisher && decoded['staff'] !== true) {\n // 404, not 403: whether a listing exists is not a stranger's business.\n return res.status(404).json({ error: 'Unknown listing' })\n }\n const artifactType = listingArtifactType(listing)\n const snapshot = await listingRef\n .collection(versionCollectionFor(artifactType))\n .orderBy('publishedAt', 'desc')\n .limit(20)\n .get()\n /**\n * The kill switch, on the publisher's own view of their versions\n * (AGL-2328).\n *\n * Staff are REQUIRED to type a reason to revoke a version. It was stored\n * and nothing read it: every reader in the repo consulted `versions` and\n * stopped, and this branch did not read the document at all — so a\n * publisher whose version had been pulled saw the row it had before,\n * still labelled by its REVIEW state. A version can be approved and\n * revoked at once, and \"Approved\" was the only word on it.\n *\n * ONE document read for the whole card, not one per version: the switch\n * is listing-scoped. The predicate is `isPluginRevoked`, shared with the\n * installer and the loaders (AGL-1085), so what a publisher is told is\n * disabled and what actually refuses to install cannot drift apart.\n */\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n // The counters the page prints, derived from the pins where there are pins\n // to derive from (AGL-1419) and reconciled against the listing totals\n // otherwise (AGL-1418), so this card cannot contradict the header above it.\n const pins = await verifiedLivePins({\n firestore: firestore as never,\n listingRef: listingRef as never,\n listingId,\n listing,\n artifactType,\n versionIds: snapshot.docs.map((doc) => String(doc.get('version') ?? doc.id)),\n updateTime: listingSnapshot.updateTime,\n })\n const tallies = reconcileFromSnapshot(snapshot.docs as never, listing, pins)\n const versions = snapshot.docs.map((doc) => {\n const sha256 = String(doc.get('sha256') ?? '')\n const versionId = String(doc.get('version') ?? doc.id)\n return {\n version: String(doc.get('version') ?? doc.id),\n publishedAtMs: doc.get('publishedAt')?.toMillis?.() ?? null,\n sha256,\n // Absent on every version published before AGL-966, which is what\n // `grandfathered` marks — those were never reviewed and saying\n // \"pending\" about them would be a lie in both directions.\n reviewState: String(doc.get('reviewState') ?? ''),\n grandfathered: Boolean(doc.get('grandfathered')),\n signed: Boolean(doc.get('signature')),\n changelog: String(doc.get('changelog') ?? ''),\n // Only reached an email before this (AGL-1079), where it got buried\n // under everything else in an inbox.\n rejectionReason: String(doc.get('reviewRejectionReason') ?? ''),\n // Per VERSION, because a listing-wide takedown and a single revoked\n // version are the same document and only the predicate tells them\n // apart.\n revoked: isPluginRevoked(revocation, versionId),\n activeInstalls: tallies.byVersion.get(versionId)?.activeInstalls ?? 0,\n installCount: tallies.byVersion.get(versionId)?.installCount ?? 0,\n // What they claimed about THESE bytes (AGL-969) — so they can see it\n // before they claim it again on the next version.\n attestation: attestationsForBytes(\n doc.get('publisherAttestation') as never,\n sha256,\n ),\n }\n })\n return res.status(200).json({\n versions,\n // The same reconciled totals the buyer branch returns (AGL-1418), so the\n // Review status card and the listing header cannot disagree about how\n // many installs there are.\n installCount: tallies.installCount,\n activeInstalls: tallies.activeInstalls,\n untrackedInstallCount: tallies.untrackedInstallCount,\n untrackedActiveInstalls: tallies.untrackedActiveInstalls,\n // The version installs actually resolve to (AGL-1016/966). Deliberately\n // NOT `latestVersion`, which names a version installs may refuse.\n latestApprovedVersion: String(listing.latestApprovedVersion ?? ''),\n latestVersion: String(listing.latestVersion ?? ''),\n reviewStatus: String(listing.reviewStatus ?? ''),\n /**\n * Listing-scoped, like the document it comes from (AGL-2328) — one\n * reason covers whatever `versions` names, so two revoked rows show the\n * same sentence. That is what was recorded; inventing a per-version\n * reason would be inventing a fact.\n *\n * `revokedBy` is deliberately NOT returned. It is a staff uid, and it\n * belongs in the audit log rather than on a publisher's screen — the\n * same line the consumer-facing card drew.\n */\n revocationReason: String(revocation?.reason ?? '').trim(),\n // The writers store `revokedAt` as a server timestamp; `atMs` is on the\n // TYPE but nothing writes it. Read both rather than trusting either,\n // and treat a revocation with no readable date as dateless rather than\n // printing an epoch.\n revokedAtMs: revocationTimeMs(revocation),\n })\n}\n\n/**\n * When the revocation was written, in millis, or null (AGL-2328).\n *\n * `PluginRevocation` declares `atMs?: number` and every writer in\n * `plugin-reviews/route.ts` writes `revokedAt: serverTimestamp()` instead.\n * A reader that trusted the type would print nothing for every revocation\n * that exists; one that trusted the writer would break if `atMs` is ever\n * the field that gets written. Both are read, admin `Timestamp` and the\n * seconds-shaped form a client SDK produces included.\n */\nfunction revocationTimeMs(\n revocation: (PluginRevocation & { revokedAt?: unknown }) | undefined,\n): number | null {\n const at = revocation?.revokedAt as\n | { toMillis?: () => number; seconds?: number }\n | undefined\n if (typeof at?.toMillis === 'function') return at.toMillis()\n if (typeof at?.seconds === 'number') return at.seconds * 1000\n return typeof revocation?.atMs === 'number' ? revocation.atMs : null\n}\n\n/**\n * Public version history for a plugin listing (AGL-431). The\n * `pluginVersions` docs are server-only (they carry publish internals),\n * but their changelog/trust/compat fields are exactly what a buyer needs\n * on the detail page — so this handler exposes THAT subset and nothing\n * else (no objectPath, no signature; sha stays out simply because the\n * client has no use for it).\n */\nexport const listingVersionsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.query?.listingId ?? '')\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\n try {\n // The publisher's own view (AGL-1079) — same collection, a different\n // question, and one that needs a token.\n if (String(req.query?.scope ?? '') === 'publisher') {\n return await publisherVersions(req, res)\n }\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n // Every artifact type has a version history now (AGL-1036), not just\n // plugins: the per-version install counts are worth the same to whoever\n // publishes a component as to whoever publishes a plugin. The collection\n // differs — plugins keep publish internals in `pluginVersions`, everything\n // else keeps content snapshots in `versions` — so the type decides which\n // to read, and the buyer-safe projection below stays the same either way.\n const listingSnapshot = await listingRef.get()\n const artifactType = listingArtifactType(listingSnapshot.data() ?? {})\n const isPlugin = artifactType === 'plugin'\n const snapshot = await listingRef\n .collection(versionCollectionFor(artifactType))\n .orderBy('publishedAt', 'desc')\n .limit(20)\n .get()\n // Buyers see APPROVED versions only (AGL-976).\n //\n // This handler returned every version, so the listing's Version history\n // advertised pending and rejected ones — with `Latest` on whichever was\n // newest. A version rejected for undeclared network access was shown to\n // buyers, by us, as the current release, changelog and all. Installing it\n // was already impossible (AGL-966 gates that), which made the card a\n // promise the product would not keep.\n //\n // Plugins only: `reviewState` is a plugin concept, and a copied artifact's\n // `versions` docs carry none — filtering those would empty every\n // component and template history.\n //\n // Through `isVersionApproved`, whose contract is the one that matters\n // here: ABSENT STATE IS NOT APPROVAL. That hides versions published\n // before review existed, which is the safe direction — the alternative is\n // showing unreviewed code as reviewed.\n const visibleDocs = isPlugin\n ? snapshot.docs.filter((doc) => isVersionApproved({\n reviewState: doc.get('reviewState'),\n }))\n : snapshot.docs\n // Derived from the live pins (AGL-1419), which are the only ground truth\n // and the only thing that can bring a count back DOWN — a tenant erase\n // sweeps `installs` without decrementing anything. Falls back to the\n // AGL-1418 reconciliation when there is nothing to derive from: a copied\n // artifact holds no pin, and a missing index must degrade to the previous\n // release rather than to zeros.\n //\n // Built from EVERY version doc rather than `visibleDocs`: the buyer filter\n // above drops unapproved versions, and reconciling against what survives\n // it would credit their installs to an approved version.\n const pins = await verifiedLivePins({\n firestore: firestore as never,\n listingRef: listingRef as never,\n listingId,\n listing: listingSnapshot.data(),\n artifactType,\n versionIds: snapshot.docs.map((doc) =>\n String(doc.get('version') ?? doc.id),\n ),\n updateTime: listingSnapshot.updateTime,\n })\n const tallies = reconcileFromSnapshot(\n snapshot.docs as never,\n listingSnapshot.data(),\n pins,\n )\n const versions = visibleDocs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n // Per-version install counts (AGL-1036). `installCount` is every install\n // that ever landed on this version; `activeInstalls` is how many are on\n // it now — the one that answers \"who is still on the old version\".\n installCount:\n tallies.byVersion.get(String(doc.get('version') ?? doc.id))\n ?.installCount ?? 0,\n activeInstalls:\n tallies.byVersion.get(String(doc.get('version') ?? doc.id))\n ?.activeInstalls ?? 0,\n ...(doc.get('changelog') ? { changelog: String(doc.get('changelog')) } : {}),\n ...(doc.get('trust') ? { trust: String(doc.get('trust')) } : {}),\n ...(Number.isInteger(doc.get('manifest')?.hostAbi)\n ? { hostAbi: Number(doc.get('manifest').hostAbi) }\n : {}),\n // Declared network allowlist (AGL-879 follow-up): the plugin origin\n // builds the /load CSP `connect-src` from this, so the sandbox only\n // reaches origins the manifest declared. Already buyer-visible in\n // review (\"Capabilities: network N\"), so exposing the origins here\n // leaks nothing new.\n ...(Array.isArray(doc.get('manifest')?.capabilities?.network)\n ? {\n network: (doc.get('manifest').capabilities.network as unknown[])\n .map((entry) => String(entry))\n .filter((entry) => /^https:\\/\\/[^\\s/]+$/.test(entry))\n .slice(0, 20),\n }\n : {}),\n publishedAtMs: doc.get('publishedAt')?.toMillis?.() ?? null,\n }))\n\n // Repair `latestApprovedVersion` when it disagrees with the versions\n // (AGL-1016, corrected by AGL-2368).\n //\n // The field is written by the approval route, so every plugin approved\n // before it existed carries none — and the console would report those as\n // \"no published version to compare against\" forever, which is a worse lie\n // than the one it replaced. The approved set is already in hand here and\n // `pluginVersions` is server-only, so this is the one place that can\n // repair it. Plugins only: `reviewState` and the approved-version\n // guarantee are a plugin concept, and a copied artifact's `versions` docs\n // carry neither.\n //\n // Two things were wrong with the original, and they compounded. It asked\n // `newestApprovedVersion` — approval only, blind to the kill switch — and\n // it \"only ever moved the value forward\". So it did not merely fail to\n // step the mirror back off a revoked version; it actively WROTE ONE BACK.\n // AGL-2306 taught the review route to repair the mirror on revoke, and the\n // very next hit on this handler undid it: stored v1.0.0 (correct), newest\n // approved v2.0.0 (revoked), v1.0.0 < v2.0.0, write. This endpoint is a\n // public unauthenticated GET, so \"the next hit\" is the next page view.\n //\n // Now derived from the whole set through the SHARED predicate the review\n // route repairs with, and applied whenever it differs — a derivation has\n // no direction to preserve. The monotonic rule belongs to the APPROVAL\n // writer, where it means \"a reviewer working through a backlog must not\n // walk the offer backwards\"; here it only ever preserved a wrong answer.\n const revocation = !isPlugin\n ? undefined\n : ((\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined)\n const newest = !isPlugin\n ? null\n : newestInstallableVersion(\n snapshot.docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n reviewState: doc.get('reviewState'),\n })),\n revocation,\n compareArtifactVersions,\n )\n if (isPlugin) {\n const stored = listingSnapshot.get('latestApprovedVersion')\n const storedString = stored == null ? '' : String(stored)\n if (storedString !== (newest ?? '')) {\n await listingRef\n .set(\n {\n // Deleted rather than emptied when nothing qualifies, matching\n // `repairLatestApprovedVersion`: an ABSENT mirror reads as \"no\n // update to offer\", an empty string is a value readers have to\n // interpret.\n latestApprovedVersion:\n newest ?? firebaseAdmin.firestore.FieldValue.delete(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n }\n }\n\n // The totals the header prints (AGL-1418). The listing document carries\n // its own copy of this pair and the client used to read it straight off\n // Firestore, which is how the header came to say `7 installs · 2 active`\n // over a version history that added up to `3 · 1`. Served from here so\n // one arithmetic answers the whole page.\n return res.status(200).json({\n versions,\n installCount: tallies.installCount,\n activeInstalls: tallies.activeInstalls,\n untrackedInstallCount: tallies.untrackedInstallCount,\n untrackedActiveInstalls: tallies.untrackedActiveInstalls,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Version lookup failed' })\n }\n}\n"],"names":["isPluginRevoked","newestInstallableVersion","firebaseAdmin","isVersionApproved","listingArtifactType","reconcileInstallTallies","versionCollectionFor","verifiedLivePins","canActAsPublisher","attestationsForBytes","compareArtifactVersions","reconcileFromSnapshot","docs","listing","pins","reconciled","map","doc","version","String","get","id","installCount","Number","activeInstalls","byVersion","Map","versions","forEach","entry","set","publisherVersions","req","res","listingId","query","authorization","headers","idToken","startsWith","slice","length","undefined","status","json","error","decoded","app","auth","verifyIdToken","firestore","listingRef","collection","listingSnapshot","data","deletedAt","isPublisher","uid","profileId","artifactType","snapshot","orderBy","limit","revocation","versionIds","updateTime","tallies","sha256","versionId","publishedAtMs","toMillis","reviewState","grandfathered","Boolean","signed","changelog","rejectionReason","revoked","attestation","untrackedInstallCount","untrackedActiveInstalls","latestApprovedVersion","latestVersion","reviewStatus","revocationReason","reason","trim","revokedAtMs","revocationTimeMs","at","revokedAt","seconds","atMs","listingVersionsHandler","method","scope","isPlugin","visibleDocs","filter","trust","isInteger","hostAbi","Array","isArray","capabilities","network","test","newest","stored","storedString","FieldValue","delete","merge","catch","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/listing-versions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isPluginRevoked,\n newestInstallableVersion,\n type PluginApiHandler,\n type PluginRevocation,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n isVersionApproved,\n listingArtifactType,\n} from '../model/marketplace'\n\nimport {\n reconcileInstallTallies,\n versionCollectionFor,\n type LivePinTally,\n type ReconciledInstallTallies,\n} from './version-stats'\nimport { verifiedLivePins } from './install-pin-counts'\nimport {\n MARKETPLACE_MANAGE_LISTINGS_PERMISSION,\n marketplacePublisherPermissionRefusal,\n} from '../model/marketplace-publisher-permissions'\nimport { publisherPermissionVerdict } from './publisher-permissions'\nimport { attestationsForBytes } from '../model/publisher-attestation'\nimport { compareArtifactVersions } from '../model/update-state'\n\n/**\n * One reconciled tally per version id, plus the totals (AGL-1418).\n *\n * Built from EVERY version document, never from a filtered subset: the buyer\n * branch shows approved versions only, and reconciling against that subset\n * would hand a pending version's installs to an approved one.\n */\nfunction reconcileFromSnapshot(\n docs: Array<{ id: string; get: (field: string) => unknown }>,\n listing: Record<string, unknown> | undefined,\n pins?: LivePinTally | null,\n): { byVersion: Map<string, { installCount: number; activeInstalls: number }> } & ReconciledInstallTallies {\n const reconciled = reconcileInstallTallies(\n docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n installCount: Number(doc.get('installCount') ?? 0),\n activeInstalls: Number(doc.get('activeInstalls') ?? 0),\n })),\n {\n installCount: Number(listing?.['installCount'] ?? 0),\n activeInstalls: Number(listing?.['activeInstalls'] ?? 0),\n },\n pins,\n )\n const byVersion = new Map<string, { installCount: number; activeInstalls: number }>()\n reconciled.versions.forEach((entry) =>\n byVersion.set(entry.version, {\n installCount: entry.installCount,\n activeInstalls: entry.activeInstalls,\n }),\n )\n return { ...reconciled, byVersion }\n}\n\n/**\n * The publisher's own view of their versions (AGL-1079).\n *\n * AGL-966 made review per-version and got the mechanics right — a new\n * version enters `pending`, the previously approved one keeps installing,\n * nothing ships past a reviewer. The publisher was told none of it: an\n * email on the outcome, and in between a listing row identical to the one\n * they had before they published.\n *\n * Separate from the buyer projection above on purpose. `reviewState`, the\n * rejection reason, the sha and what was attested are the publisher's own\n * business and nobody else's — a buyer has no need to read why a version\n * they will never be offered was sent back. So this branch demands a token\n * and org membership rather than widening what the public route returns.\n */\nconst publisherVersions: PluginApiHandler = async (req, res) => {\n const listingId = String(req.query?.listingId ?? '')\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data()\n if (!listing || listing.deletedAt) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // Listings are org-owned (AGL-652), so this is a question about the\n // publishing org, not a uid comparison. It asks \"Manage published\n // listings\" (`marketplace.manageListings`, AGL-4047), the key the edit\n // paths ask, whose default is the owner and admins this asked for by role\n // before. Staff read any listing's versions, as before.\n if (decoded['staff'] !== true) {\n const verdict = await publisherPermissionVerdict(\n decoded.uid,\n listing.profileId,\n MARKETPLACE_MANAGE_LISTINGS_PERMISSION,\n )\n if (verdict === 'not-a-member') {\n // 404, not 403: whether a listing exists is not a stranger's business.\n return res.status(404).json({ error: 'Unknown listing' })\n }\n if (verdict === 'refused') {\n // A member sees the listing in their own workspace already, so the\n // refusal names the permission they are missing.\n return res.status(403).json({\n error: marketplacePublisherPermissionRefusal(\n MARKETPLACE_MANAGE_LISTINGS_PERMISSION,\n ),\n reason: 'permission',\n permission: MARKETPLACE_MANAGE_LISTINGS_PERMISSION,\n })\n }\n }\n const artifactType = listingArtifactType(listing)\n const snapshot = await listingRef\n .collection(versionCollectionFor(artifactType))\n .orderBy('publishedAt', 'desc')\n .limit(20)\n .get()\n /**\n * The kill switch, on the publisher's own view of their versions\n * (AGL-2328).\n *\n * Staff are REQUIRED to type a reason to revoke a version. It was stored\n * and nothing read it: every reader in the repo consulted `versions` and\n * stopped, and this branch did not read the document at all — so a\n * publisher whose version had been pulled saw the row it had before,\n * still labelled by its REVIEW state. A version can be approved and\n * revoked at once, and \"Approved\" was the only word on it.\n *\n * ONE document read for the whole card, not one per version: the switch\n * is listing-scoped. The predicate is `isPluginRevoked`, shared with the\n * installer and the loaders (AGL-1085), so what a publisher is told is\n * disabled and what actually refuses to install cannot drift apart.\n */\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n // The counters the page prints, derived from the pins where there are pins\n // to derive from (AGL-1419) and reconciled against the listing totals\n // otherwise (AGL-1418), so this card cannot contradict the header above it.\n const pins = await verifiedLivePins({\n firestore: firestore as never,\n listingRef: listingRef as never,\n listingId,\n listing,\n artifactType,\n versionIds: snapshot.docs.map((doc) => String(doc.get('version') ?? doc.id)),\n updateTime: listingSnapshot.updateTime,\n })\n const tallies = reconcileFromSnapshot(snapshot.docs as never, listing, pins)\n const versions = snapshot.docs.map((doc) => {\n const sha256 = String(doc.get('sha256') ?? '')\n const versionId = String(doc.get('version') ?? doc.id)\n return {\n version: String(doc.get('version') ?? doc.id),\n publishedAtMs: doc.get('publishedAt')?.toMillis?.() ?? null,\n sha256,\n // Absent on every version published before AGL-966, which is what\n // `grandfathered` marks — those were never reviewed and saying\n // \"pending\" about them would be a lie in both directions.\n reviewState: String(doc.get('reviewState') ?? ''),\n grandfathered: Boolean(doc.get('grandfathered')),\n signed: Boolean(doc.get('signature')),\n changelog: String(doc.get('changelog') ?? ''),\n // Only reached an email before this (AGL-1079), where it got buried\n // under everything else in an inbox.\n rejectionReason: String(doc.get('reviewRejectionReason') ?? ''),\n // Per VERSION, because a listing-wide takedown and a single revoked\n // version are the same document and only the predicate tells them\n // apart.\n revoked: isPluginRevoked(revocation, versionId),\n activeInstalls: tallies.byVersion.get(versionId)?.activeInstalls ?? 0,\n installCount: tallies.byVersion.get(versionId)?.installCount ?? 0,\n // What they claimed about THESE bytes (AGL-969) — so they can see it\n // before they claim it again on the next version.\n attestation: attestationsForBytes(\n doc.get('publisherAttestation') as never,\n sha256,\n ),\n }\n })\n return res.status(200).json({\n versions,\n // The same reconciled totals the buyer branch returns (AGL-1418), so the\n // Review status card and the listing header cannot disagree about how\n // many installs there are.\n installCount: tallies.installCount,\n activeInstalls: tallies.activeInstalls,\n untrackedInstallCount: tallies.untrackedInstallCount,\n untrackedActiveInstalls: tallies.untrackedActiveInstalls,\n // The version installs actually resolve to (AGL-1016/966). Deliberately\n // NOT `latestVersion`, which names a version installs may refuse.\n latestApprovedVersion: String(listing.latestApprovedVersion ?? ''),\n latestVersion: String(listing.latestVersion ?? ''),\n reviewStatus: String(listing.reviewStatus ?? ''),\n /**\n * Listing-scoped, like the document it comes from (AGL-2328) — one\n * reason covers whatever `versions` names, so two revoked rows show the\n * same sentence. That is what was recorded; inventing a per-version\n * reason would be inventing a fact.\n *\n * `revokedBy` is deliberately NOT returned. It is a staff uid, and it\n * belongs in the audit log rather than on a publisher's screen — the\n * same line the consumer-facing card drew.\n */\n revocationReason: String(revocation?.reason ?? '').trim(),\n // The writers store `revokedAt` as a server timestamp; `atMs` is on the\n // TYPE but nothing writes it. Read both rather than trusting either,\n // and treat a revocation with no readable date as dateless rather than\n // printing an epoch.\n revokedAtMs: revocationTimeMs(revocation),\n })\n}\n\n/**\n * When the revocation was written, in millis, or null (AGL-2328).\n *\n * `PluginRevocation` declares `atMs?: number` and every writer in\n * `plugin-reviews/route.ts` writes `revokedAt: serverTimestamp()` instead.\n * A reader that trusted the type would print nothing for every revocation\n * that exists; one that trusted the writer would break if `atMs` is ever\n * the field that gets written. Both are read, admin `Timestamp` and the\n * seconds-shaped form a client SDK produces included.\n */\nfunction revocationTimeMs(\n revocation: (PluginRevocation & { revokedAt?: unknown }) | undefined,\n): number | null {\n const at = revocation?.revokedAt as\n | { toMillis?: () => number; seconds?: number }\n | undefined\n if (typeof at?.toMillis === 'function') return at.toMillis()\n if (typeof at?.seconds === 'number') return at.seconds * 1000\n return typeof revocation?.atMs === 'number' ? revocation.atMs : null\n}\n\n/**\n * Public version history for a plugin listing (AGL-431). The\n * `pluginVersions` docs are server-only (they carry publish internals),\n * but their changelog/trust/compat fields are exactly what a buyer needs\n * on the detail page — so this handler exposes THAT subset and nothing\n * else (no objectPath, no signature; sha stays out simply because the\n * client has no use for it).\n */\nexport const listingVersionsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.query?.listingId ?? '')\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\n try {\n // The publisher's own view (AGL-1079) — same collection, a different\n // question, and one that needs a token.\n if (String(req.query?.scope ?? '') === 'publisher') {\n return await publisherVersions(req, res)\n }\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n // Every artifact type has a version history now (AGL-1036), not just\n // plugins: the per-version install counts are worth the same to whoever\n // publishes a component as to whoever publishes a plugin. The collection\n // differs — plugins keep publish internals in `pluginVersions`, everything\n // else keeps content snapshots in `versions` — so the type decides which\n // to read, and the buyer-safe projection below stays the same either way.\n const listingSnapshot = await listingRef.get()\n const artifactType = listingArtifactType(listingSnapshot.data() ?? {})\n const isPlugin = artifactType === 'plugin'\n const snapshot = await listingRef\n .collection(versionCollectionFor(artifactType))\n .orderBy('publishedAt', 'desc')\n .limit(20)\n .get()\n // Buyers see APPROVED versions only (AGL-976).\n //\n // This handler returned every version, so the listing's Version history\n // advertised pending and rejected ones — with `Latest` on whichever was\n // newest. A version rejected for undeclared network access was shown to\n // buyers, by us, as the current release, changelog and all. Installing it\n // was already impossible (AGL-966 gates that), which made the card a\n // promise the product would not keep.\n //\n // Plugins only: `reviewState` is a plugin concept, and a copied artifact's\n // `versions` docs carry none — filtering those would empty every\n // component and template history.\n //\n // Through `isVersionApproved`, whose contract is the one that matters\n // here: ABSENT STATE IS NOT APPROVAL. That hides versions published\n // before review existed, which is the safe direction — the alternative is\n // showing unreviewed code as reviewed.\n const visibleDocs = isPlugin\n ? snapshot.docs.filter((doc) => isVersionApproved({\n reviewState: doc.get('reviewState'),\n }))\n : snapshot.docs\n // Derived from the live pins (AGL-1419), which are the only ground truth\n // and the only thing that can bring a count back DOWN — a tenant erase\n // sweeps `installs` without decrementing anything. Falls back to the\n // AGL-1418 reconciliation when there is nothing to derive from: a copied\n // artifact holds no pin, and a missing index must degrade to the previous\n // release rather than to zeros.\n //\n // Built from EVERY version doc rather than `visibleDocs`: the buyer filter\n // above drops unapproved versions, and reconciling against what survives\n // it would credit their installs to an approved version.\n const pins = await verifiedLivePins({\n firestore: firestore as never,\n listingRef: listingRef as never,\n listingId,\n listing: listingSnapshot.data(),\n artifactType,\n versionIds: snapshot.docs.map((doc) =>\n String(doc.get('version') ?? doc.id),\n ),\n updateTime: listingSnapshot.updateTime,\n })\n const tallies = reconcileFromSnapshot(\n snapshot.docs as never,\n listingSnapshot.data(),\n pins,\n )\n const versions = visibleDocs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n // Per-version install counts (AGL-1036). `installCount` is every install\n // that ever landed on this version; `activeInstalls` is how many are on\n // it now — the one that answers \"who is still on the old version\".\n installCount:\n tallies.byVersion.get(String(doc.get('version') ?? doc.id))\n ?.installCount ?? 0,\n activeInstalls:\n tallies.byVersion.get(String(doc.get('version') ?? doc.id))\n ?.activeInstalls ?? 0,\n ...(doc.get('changelog') ? { changelog: String(doc.get('changelog')) } : {}),\n ...(doc.get('trust') ? { trust: String(doc.get('trust')) } : {}),\n ...(Number.isInteger(doc.get('manifest')?.hostAbi)\n ? { hostAbi: Number(doc.get('manifest').hostAbi) }\n : {}),\n // Declared network allowlist (AGL-879 follow-up): the plugin origin\n // builds the /load CSP `connect-src` from this, so the sandbox only\n // reaches origins the manifest declared. Already buyer-visible in\n // review (\"Capabilities: network N\"), so exposing the origins here\n // leaks nothing new.\n ...(Array.isArray(doc.get('manifest')?.capabilities?.network)\n ? {\n network: (doc.get('manifest').capabilities.network as unknown[])\n .map((entry) => String(entry))\n .filter((entry) => /^https:\\/\\/[^\\s/]+$/.test(entry))\n .slice(0, 20),\n }\n : {}),\n publishedAtMs: doc.get('publishedAt')?.toMillis?.() ?? null,\n }))\n\n // Repair `latestApprovedVersion` when it disagrees with the versions\n // (AGL-1016, corrected by AGL-2368).\n //\n // The field is written by the approval route, so every plugin approved\n // before it existed carries none — and the console would report those as\n // \"no published version to compare against\" forever, which is a worse lie\n // than the one it replaced. The approved set is already in hand here and\n // `pluginVersions` is server-only, so this is the one place that can\n // repair it. Plugins only: `reviewState` and the approved-version\n // guarantee are a plugin concept, and a copied artifact's `versions` docs\n // carry neither.\n //\n // Two things were wrong with the original, and they compounded. It asked\n // `newestApprovedVersion` — approval only, blind to the kill switch — and\n // it \"only ever moved the value forward\". So it did not merely fail to\n // step the mirror back off a revoked version; it actively WROTE ONE BACK.\n // AGL-2306 taught the review route to repair the mirror on revoke, and the\n // very next hit on this handler undid it: stored v1.0.0 (correct), newest\n // approved v2.0.0 (revoked), v1.0.0 < v2.0.0, write. This endpoint is a\n // public unauthenticated GET, so \"the next hit\" is the next page view.\n //\n // Now derived from the whole set through the SHARED predicate the review\n // route repairs with, and applied whenever it differs — a derivation has\n // no direction to preserve. The monotonic rule belongs to the APPROVAL\n // writer, where it means \"a reviewer working through a backlog must not\n // walk the offer backwards\"; here it only ever preserved a wrong answer.\n const revocation = !isPlugin\n ? undefined\n : ((\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined)\n const newest = !isPlugin\n ? null\n : newestInstallableVersion(\n snapshot.docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n reviewState: doc.get('reviewState'),\n })),\n revocation,\n compareArtifactVersions,\n )\n if (isPlugin) {\n const stored = listingSnapshot.get('latestApprovedVersion')\n const storedString = stored == null ? '' : String(stored)\n if (storedString !== (newest ?? '')) {\n await listingRef\n .set(\n {\n // Deleted rather than emptied when nothing qualifies, matching\n // `repairLatestApprovedVersion`: an ABSENT mirror reads as \"no\n // update to offer\", an empty string is a value readers have to\n // interpret.\n latestApprovedVersion:\n newest ?? firebaseAdmin.firestore.FieldValue.delete(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n }\n }\n\n // The totals the header prints (AGL-1418). The listing document carries\n // its own copy of this pair and the client used to read it straight off\n // Firestore, which is how the header came to say `7 installs · 2 active`\n // over a version history that added up to `3 · 1`. Served from here so\n // one arithmetic answers the whole page.\n return res.status(200).json({\n versions,\n installCount: tallies.installCount,\n activeInstalls: tallies.activeInstalls,\n untrackedInstallCount: tallies.untrackedInstallCount,\n untrackedActiveInstalls: tallies.untrackedActiveInstalls,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Version lookup failed' })\n }\n}\n"],"names":["isPluginRevoked","newestInstallableVersion","firebaseAdmin","isVersionApproved","listingArtifactType","reconcileInstallTallies","versionCollectionFor","verifiedLivePins","MARKETPLACE_MANAGE_LISTINGS_PERMISSION","marketplacePublisherPermissionRefusal","publisherPermissionVerdict","attestationsForBytes","compareArtifactVersions","reconcileFromSnapshot","docs","listing","pins","reconciled","map","doc","version","String","get","id","installCount","Number","activeInstalls","byVersion","Map","versions","forEach","entry","set","publisherVersions","req","res","listingId","query","authorization","headers","idToken","startsWith","slice","length","undefined","status","json","error","decoded","app","auth","verifyIdToken","firestore","listingRef","collection","listingSnapshot","data","deletedAt","verdict","uid","profileId","reason","permission","artifactType","snapshot","orderBy","limit","revocation","versionIds","updateTime","tallies","sha256","versionId","publishedAtMs","toMillis","reviewState","grandfathered","Boolean","signed","changelog","rejectionReason","revoked","attestation","untrackedInstallCount","untrackedActiveInstalls","latestApprovedVersion","latestVersion","reviewStatus","revocationReason","trim","revokedAtMs","revocationTimeMs","at","revokedAt","seconds","atMs","listingVersionsHandler","method","scope","isPlugin","visibleDocs","filter","trust","isInteger","hostAbi","Array","isArray","capabilities","network","test","newest","stored","storedString","FieldValue","delete","merge","catch","console"],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Whether this install is the publisher's own (AGL-2484, AGL-4047): the
19
+ * person belongs to the workspace that publishes the listing AND the install
20
+ * lands in that workspace. The one answer every install door, `update-artifact`
21
+ * and the site starter source give to "is this free because it is theirs".
22
+ *
23
+ * `targetOrgId` is the workspace the install lands in, as the door resolved
24
+ * it from the site (or the org it installs into), and the same workspace a
25
+ * licence has to cover. Off the publisher's own sites the membership cannot
26
+ * change the answer, so it is not read.
27
+ */
28
+ export declare function ownsListingHere(firestore: FirebaseFirestore.Firestore, facts: {
29
+ /** The authenticated caller, never a value from the request body. */
30
+ uid: string;
31
+ /** The listing's `profileId`: the workspace that publishes it. */
32
+ publisherOrgId: unknown;
33
+ targetOrgId: unknown;
34
+ }): Promise<boolean>;
35
+ /**
36
+ * Whether the sale would refuse this person because they belong to the
37
+ * publisher (AGL-3365, AGL-4047), asked before they press Buy so a surface can
38
+ * say so. The refusal itself stays with `sale.ts`. On the publisher's own sites
39
+ * the listing is free instead, so the membership is not read.
40
+ */
41
+ export declare function memberOfPublisherElsewhere(firestore: FirebaseFirestore.Firestore, facts: {
42
+ /** The authenticated caller, never a value from the request body. */
43
+ uid: string;
44
+ publisherOrgId: unknown;
45
+ targetOrgId: unknown;
46
+ }): Promise<boolean>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { isOwnListingInstall, isPublisherMemberElsewhere, landsInPublisherWorkspace } from "../model/own-listing.js";
17
+ import { isPublisherMember } from "./publisher-profile.js";
18
+ /**
19
+ * Whether this install is the publisher's own (AGL-2484, AGL-4047): the
20
+ * person belongs to the workspace that publishes the listing AND the install
21
+ * lands in that workspace. The one answer every install door, `update-artifact`
22
+ * and the site starter source give to "is this free because it is theirs".
23
+ *
24
+ * `targetOrgId` is the workspace the install lands in, as the door resolved
25
+ * it from the site (or the org it installs into), and the same workspace a
26
+ * licence has to cover. Off the publisher's own sites the membership cannot
27
+ * change the answer, so it is not read.
28
+ */ export async function ownsListingHere(firestore, facts) {
29
+ const { uid, publisherOrgId, targetOrgId } = facts;
30
+ if (!landsInPublisherWorkspace(publisherOrgId, targetOrgId)) return false;
31
+ return isOwnListingInstall({
32
+ publisherOrgId,
33
+ targetOrgId,
34
+ publisherMember: await isPublisherMember(firestore, uid, String(publisherOrgId))
35
+ });
36
+ }
37
+ /**
38
+ * Whether the sale would refuse this person because they belong to the
39
+ * publisher (AGL-3365, AGL-4047), asked before they press Buy so a surface can
40
+ * say so. The refusal itself stays with `sale.ts`. On the publisher's own sites
41
+ * the listing is free instead, so the membership is not read.
42
+ */ export async function memberOfPublisherElsewhere(firestore, facts) {
43
+ const { uid, publisherOrgId, targetOrgId } = facts;
44
+ if (landsInPublisherWorkspace(publisherOrgId, targetOrgId)) return false;
45
+ return isPublisherMemberElsewhere({
46
+ publisherOrgId,
47
+ targetOrgId,
48
+ publisherMember: await isPublisherMember(firestore, uid, String(publisherOrgId != null ? publisherOrgId : ''))
49
+ });
50
+ }
51
+
52
+ //# sourceMappingURL=own-listing.js.map