@aglyn/plugins-marketplace 1.15.2 → 1.17.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 (32) hide show
  1. package/package.json +15 -15
  2. package/src/lib/components/listing-content.component.js +32 -14
  3. package/src/lib/components/listing-content.component.js.map +1 -1
  4. package/src/lib/components/site-plugin-connections-card.component.js +4 -3
  5. package/src/lib/components/site-plugin-connections-card.component.js.map +1 -1
  6. package/src/lib/model/editor-connections.d.ts +1 -1
  7. package/src/lib/model/editor-connections.js +13 -2
  8. package/src/lib/model/editor-connections.js.map +1 -1
  9. package/src/lib/model/marketplace.d.ts +11 -0
  10. package/src/lib/model/marketplace.js +12 -2
  11. package/src/lib/model/marketplace.js.map +1 -1
  12. package/src/lib/model/plugin-connection-card.d.ts +6 -0
  13. package/src/lib/model/plugin-connection-card.js +8 -0
  14. package/src/lib/model/plugin-connection-card.js.map +1 -1
  15. package/src/lib/model/plugin-connections.d.ts +14 -0
  16. package/src/lib/model/plugin-connections.js +18 -1
  17. package/src/lib/model/plugin-connections.js.map +1 -1
  18. package/src/lib/native-nav.js +26 -1
  19. package/src/lib/native-nav.js.map +1 -1
  20. package/src/lib/server/admin-reviews.js +9 -0
  21. package/src/lib/server/admin-reviews.js.map +1 -1
  22. package/src/lib/server/admin-trust.js +11 -0
  23. package/src/lib/server/admin-trust.js.map +1 -1
  24. package/src/lib/server/connections/site-records-platform.js +8 -5
  25. package/src/lib/server/connections/site-records-platform.js.map +1 -1
  26. package/src/lib/server/install-plugin.js +34 -19
  27. package/src/lib/server/install-plugin.js.map +1 -1
  28. package/src/lib/server/listing-versions.js +7 -2
  29. package/src/lib/server/listing-versions.js.map +1 -1
  30. package/src/lib/server/revoke-cache-drop.d.ts +29 -34
  31. package/src/lib/server/revoke-cache-drop.js +43 -49
  32. package/src/lib/server/revoke-cache-drop.js.map +1 -1
@@ -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 {\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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"}
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 // The services the version declares connections to (AGL-4197), by\n // label only: the listing says which plan connecting them needs. The\n // site's own connection card already shows these labels to installers.\n ...(Array.isArray(doc.get('manifest')?.connections)\n ? {\n connections: (doc.get('manifest').connections as unknown[])\n .map((entry) =>\n String((entry as { label?: unknown })?.label ?? '').slice(0, 80),\n )\n .filter(Boolean)\n .slice(0, 10),\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","connections","label","newest","stored","storedString","FieldValue","delete","merge","catch","console"],"mappings":"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"}
@@ -17,43 +17,38 @@
17
17
  import { type PluginSiteCacheResult } from '@aglyn/aglyn/plugin-manager/plugin-site-cache';
18
18
  import type { Firestore } from 'firebase-admin/firestore';
19
19
  /**
20
- * WHICH SITES ARE RUNNING THIS LISTING (AGL-1152, moved here by AGL-3080).
20
+ * A PLATFORM EVENT CHANGED WHICH PLUGINS MAY LOAD (AGL-1152, AGL-4121).
21
21
  *
22
22
  * ## Why a revocation needs it
23
23
  *
24
24
  * The tenant stamps each `marketplacePlugin` node with its pinned install
25
- * and its kill-switch state AT COMPOSE TIME, so a page cached before a
26
- * revocation goes on serving the pre-revocation answer until it re-renders.
27
- * The per-install revocation read is deliberately on a short TTL as a
28
- * security bound — but that bound is only consulted DURING a render, so on a
29
- * page nobody is requesting it bounds nothing at all. Dropping the cache is
30
- * what actually makes a kill switch take effect on already-cached HTML.
31
- *
32
- * ## Scope
33
- *
34
- * Installs live at BOTH `orgs/{orgId}/installs/{listingId}` (org-tier,
35
- * applies to every host in the org — AGL-237) and
36
- * `hosts/{hostId}/installs/{listingId}`, so the collection-group query finds
37
- * both and org hits are expanded to their hosts. Backed by the
38
- * `installs.listingId` COLLECTION_GROUP field override; Firestore
39
- * auto-creates single-field indexes at COLLECTION scope only.
40
- *
41
- * ## Why this half is the plugin's
42
- *
43
- * `installs` is this plugin's own collection — its `hostCollections` entry
44
- * names it — and what an org-tier pin means is its rule. It used to sit in
45
- * the console's `tenant-revalidate.ts` beside the fan-out, which meant the
46
- * console knew what an install pin was. The fan-out stayed there and is
47
- * reached through `dropPluginSiteCache`: dropping a site's pages takes the
48
- * tenant's revalidation paths and cache tags, which are the app's.
49
- *
50
- * BEST EFFORT, and never throws: the tenant still refuses a revoked plugin
51
- * at render time. This only shrinks the window in which cached HTML shows
52
- * the old answer. Read `complete` before reading `dropped` — see
53
- * {@link dropPluginSiteCache}.
25
+ * and its kill-switch state AT COMPOSE TIME, and resolves the signed realm
26
+ * installs once per site, so a page cached before a takedown, a revoked
27
+ * version or withdrawn realm trust goes on serving the old answer until it
28
+ * re-renders. Dropping the cache is what makes the stop take effect on
29
+ * already-cached HTML.
30
+ *
31
+ * ## One tag, not a host walk (AGL-4121)
32
+ *
33
+ * This used to walk the `installs` collection group, expand org-tier pins to
34
+ * their hosts and fan out a whole-site drop to each — capped at 200 sites, and
35
+ * only as good as the walk. Every read that decides which plugins load now
36
+ * carries the platform-wide plugin tag, and so does every page that made one,
37
+ * so a single `plugins: 'everywhere'` drop expires exactly those, on every
38
+ * site, under every cache key, with nothing to walk and nothing to cap. Sites
39
+ * that never ran the listing regenerate their plugin-reading pages once on
40
+ * their next visit; takedowns are rare enough for that to be the right trade.
41
+ *
42
+ * ## It must not be lost
43
+ *
44
+ * The app stages the drop in its durable publish outbox before it sends it,
45
+ * so a refused call is retried by the drain. When neither landed, this
46
+ * THROWS: the stop itself is already written, and the staff action that made
47
+ * it must come back as a failure to retry rather than a success that left the
48
+ * stopped bundle running on cached pages for the week-long backstop.
54
49
  */
55
- export interface RevokeCacheDropResult extends PluginSiteCacheResult {
56
- /** Install pins matched by the collection-group query. */
57
- installsFound: number;
50
+ export declare class PluginCacheDropLostError extends Error {
51
+ readonly listingId: string;
52
+ constructor(listingId: string);
58
53
  }
59
- export declare function dropCachesForListing(firestore: Firestore, listingId: string): Promise<RevokeCacheDropResult>;
54
+ export declare function dropCachesForListing(_firestore: Firestore, listingId: string): Promise<PluginSiteCacheResult>;
@@ -1,4 +1,3 @@
1
- import { _ as _extends } from "@swc/helpers/_/_extends";
2
1
  /**
3
2
  * @license
4
3
  * Copyright 2026 Aglyn LLC
@@ -15,60 +14,55 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
14
  * See the License for the specific language governing permissions and
16
15
  * limitations under the License.
17
16
  */ import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
18
- export async function dropCachesForListing(firestore, listingId) {
19
- const nothing = {
20
- installsFound: 0,
17
+ /**
18
+ * A PLATFORM EVENT CHANGED WHICH PLUGINS MAY LOAD (AGL-1152, AGL-4121).
19
+ *
20
+ * ## Why a revocation needs it
21
+ *
22
+ * The tenant stamps each `marketplacePlugin` node with its pinned install
23
+ * and its kill-switch state AT COMPOSE TIME, and resolves the signed realm
24
+ * installs once per site, so a page cached before a takedown, a revoked
25
+ * version or withdrawn realm trust goes on serving the old answer until it
26
+ * re-renders. Dropping the cache is what makes the stop take effect on
27
+ * already-cached HTML.
28
+ *
29
+ * ## One tag, not a host walk (AGL-4121)
30
+ *
31
+ * This used to walk the `installs` collection group, expand org-tier pins to
32
+ * their hosts and fan out a whole-site drop to each — capped at 200 sites, and
33
+ * only as good as the walk. Every read that decides which plugins load now
34
+ * carries the platform-wide plugin tag, and so does every page that made one,
35
+ * so a single `plugins: 'everywhere'` drop expires exactly those, on every
36
+ * site, under every cache key, with nothing to walk and nothing to cap. Sites
37
+ * that never ran the listing regenerate their plugin-reading pages once on
38
+ * their next visit; takedowns are rare enough for that to be the right trade.
39
+ *
40
+ * ## It must not be lost
41
+ *
42
+ * The app stages the drop in its durable publish outbox before it sends it,
43
+ * so a refused call is retried by the drain. When neither landed, this
44
+ * THROWS: the stop itself is already written, and the staff action that made
45
+ * it must come back as a failure to retry rather than a success that left the
46
+ * stopped bundle running on cached pages for the week-long backstop.
47
+ */ export class PluginCacheDropLostError extends Error {
48
+ constructor(listingId){
49
+ super(`The live sites could not be told that marketplace listing ${listingId} ` + 'was stopped, and the drop could not be queued for a retry. The stop ' + 'is saved; retry the action to drop the cached pages.'), this.listingId = listingId;
50
+ this.name = 'PluginCacheDropLostError';
51
+ }
52
+ }
53
+ export async function dropCachesForListing(_firestore, listingId) {
54
+ if (!listingId) return {
21
55
  dropped: 0,
22
56
  skipped: 0,
23
57
  complete: true
24
58
  };
25
- if (!listingId) return nothing;
26
- let hostIds;
27
- let installsFound;
28
- try {
29
- const installs = await firestore.collectionGroup('installs').where('listingId', '==', listingId).get();
30
- if (installs.empty) return nothing;
31
- installsFound = installs.size;
32
- const hosts = new Set();
33
- const orgIds = new Set();
34
- for (const doc of installs.docs){
35
- const owner = doc.ref.parent.parent;
36
- if (!owner) continue;
37
- if (owner.parent.id === 'hosts') hosts.add(owner.id);
38
- else if (owner.parent.id === 'orgs') orgIds.add(owner.id);
39
- }
40
- // An org-tier pin applies to every host in the org, so it is the org's
41
- // hosts that hold the cached HTML — the org itself renders nothing.
42
- await Promise.all([
43
- ...orgIds
44
- ].map(async (orgId)=>{
45
- const owned = await firestore.collection('hosts').where('orgId', '==', orgId).get();
46
- for (const host of owned.docs)hosts.add(host.id);
47
- }));
48
- hostIds = [
49
- ...hosts
50
- ];
51
- } catch (error) {
52
- /*
53
- * The READ failed, so nothing is known about which sites are affected —
54
- * which is not the same as none being affected. `complete: false` for
55
- * the same reason the shell uses it: a zero that reads as an answer is
56
- * how a kill switch quietly does not take effect.
57
- */ console.error('[marketplace] could not find the sites running', listingId, error);
58
- return {
59
- installsFound: 0,
60
- dropped: 0,
61
- skipped: 0,
62
- complete: false
63
- };
64
- }
65
59
  const result = await dropPluginSiteCache({
66
- hostIds,
67
- reason: `marketplace listing ${listingId} revoked`
68
- });
69
- return _extends({}, result, {
70
- installsFound
60
+ hostIds: [],
61
+ plugins: 'everywhere',
62
+ reason: `marketplace listing ${listingId} stopped`
71
63
  });
64
+ if (!result.complete) throw new PluginCacheDropLostError(listingId);
65
+ return result;
72
66
  }
73
67
 
74
68
  //# sourceMappingURL=revoke-cache-drop.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/revoke-cache-drop.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 dropPluginSiteCache,\n type PluginSiteCacheResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type { Firestore } from 'firebase-admin/firestore'\n\n/**\n * WHICH SITES ARE RUNNING THIS LISTING (AGL-1152, moved here by AGL-3080).\n *\n * ## Why a revocation needs it\n *\n * The tenant stamps each `marketplacePlugin` node with its pinned install\n * and its kill-switch state AT COMPOSE TIME, so a page cached before a\n * revocation goes on serving the pre-revocation answer until it re-renders.\n * The per-install revocation read is deliberately on a short TTL as a\n * security bound — but that bound is only consulted DURING a render, so on a\n * page nobody is requesting it bounds nothing at all. Dropping the cache is\n * what actually makes a kill switch take effect on already-cached HTML.\n *\n * ## Scope\n *\n * Installs live at BOTH `orgs/{orgId}/installs/{listingId}` (org-tier,\n * applies to every host in the org — AGL-237) and\n * `hosts/{hostId}/installs/{listingId}`, so the collection-group query finds\n * both and org hits are expanded to their hosts. Backed by the\n * `installs.listingId` COLLECTION_GROUP field override; Firestore\n * auto-creates single-field indexes at COLLECTION scope only.\n *\n * ## Why this half is the plugin's\n *\n * `installs` is this plugin's own collection — its `hostCollections` entry\n * names it — and what an org-tier pin means is its rule. It used to sit in\n * the console's `tenant-revalidate.ts` beside the fan-out, which meant the\n * console knew what an install pin was. The fan-out stayed there and is\n * reached through `dropPluginSiteCache`: dropping a site's pages takes the\n * tenant's revalidation paths and cache tags, which are the app's.\n *\n * BEST EFFORT, and never throws: the tenant still refuses a revoked plugin\n * at render time. This only shrinks the window in which cached HTML shows\n * the old answer. Read `complete` before reading `dropped` — see\n * {@link dropPluginSiteCache}.\n */\nexport interface RevokeCacheDropResult extends PluginSiteCacheResult {\n /** Install pins matched by the collection-group query. */\n installsFound: number\n}\n\nexport async function dropCachesForListing(\n firestore: Firestore,\n listingId: string,\n): Promise<RevokeCacheDropResult> {\n const nothing: RevokeCacheDropResult = {\n installsFound: 0,\n dropped: 0,\n skipped: 0,\n complete: true,\n }\n if (!listingId) return nothing\n let hostIds: string[]\n let installsFound: number\n try {\n const installs = await firestore\n .collectionGroup('installs')\n .where('listingId', '==', listingId)\n .get()\n if (installs.empty) return nothing\n installsFound = installs.size\n\n const hosts = new Set<string>()\n const orgIds = new Set<string>()\n for (const doc of installs.docs) {\n const owner = doc.ref.parent.parent\n if (!owner) continue\n if (owner.parent.id === 'hosts') hosts.add(owner.id)\n else if (owner.parent.id === 'orgs') orgIds.add(owner.id)\n }\n // An org-tier pin applies to every host in the org, so it is the org's\n // hosts that hold the cached HTML — the org itself renders nothing.\n await Promise.all(\n [...orgIds].map(async (orgId) => {\n const owned = await firestore\n .collection('hosts')\n .where('orgId', '==', orgId)\n .get()\n for (const host of owned.docs) hosts.add(host.id)\n }),\n )\n hostIds = [...hosts]\n } catch (error) {\n /*\n * The READ failed, so nothing is known about which sites are affected —\n * which is not the same as none being affected. `complete: false` for\n * the same reason the shell uses it: a zero that reads as an answer is\n * how a kill switch quietly does not take effect.\n */\n console.error(\n '[marketplace] could not find the sites running',\n listingId,\n error,\n )\n return { installsFound: 0, dropped: 0, skipped: 0, complete: false }\n }\n\n const result = await dropPluginSiteCache({\n hostIds,\n reason: `marketplace listing ${listingId} revoked`,\n })\n return { ...result, installsFound }\n}\n"],"names":["dropPluginSiteCache","dropCachesForListing","firestore","listingId","nothing","installsFound","dropped","skipped","complete","hostIds","installs","collectionGroup","where","get","empty","size","hosts","Set","orgIds","doc","docs","owner","ref","parent","id","add","Promise","all","map","orgId","owned","collection","host","error","console","result","reason"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,mBAAmB,QAEd,gDAA+C;AA4CtD,OAAO,eAAeC,qBACpBC,SAAoB,EACpBC,SAAiB;IAEjB,MAAMC,UAAiC;QACrCC,eAAe;QACfC,SAAS;QACTC,SAAS;QACTC,UAAU;IACZ;IACA,IAAI,CAACL,WAAW,OAAOC;IACvB,IAAIK;IACJ,IAAIJ;IACJ,IAAI;QACF,MAAMK,WAAW,MAAMR,UACpBS,eAAe,CAAC,YAChBC,KAAK,CAAC,aAAa,MAAMT,WACzBU,GAAG;QACN,IAAIH,SAASI,KAAK,EAAE,OAAOV;QAC3BC,gBAAgBK,SAASK,IAAI;QAE7B,MAAMC,QAAQ,IAAIC;QAClB,MAAMC,SAAS,IAAID;QACnB,KAAK,MAAME,OAAOT,SAASU,IAAI,CAAE;YAC/B,MAAMC,QAAQF,IAAIG,GAAG,CAACC,MAAM,CAACA,MAAM;YACnC,IAAI,CAACF,OAAO;YACZ,IAAIA,MAAME,MAAM,CAACC,EAAE,KAAK,SAASR,MAAMS,GAAG,CAACJ,MAAMG,EAAE;iBAC9C,IAAIH,MAAME,MAAM,CAACC,EAAE,KAAK,QAAQN,OAAOO,GAAG,CAACJ,MAAMG,EAAE;QAC1D;QACA,uEAAuE;QACvE,oEAAoE;QACpE,MAAME,QAAQC,GAAG,CACf;eAAIT;SAAO,CAACU,GAAG,CAAC,OAAOC;YACrB,MAAMC,QAAQ,MAAM5B,UACjB6B,UAAU,CAAC,SACXnB,KAAK,CAAC,SAAS,MAAMiB,OACrBhB,GAAG;YACN,KAAK,MAAMmB,QAAQF,MAAMV,IAAI,CAAEJ,MAAMS,GAAG,CAACO,KAAKR,EAAE;QAClD;QAEFf,UAAU;eAAIO;SAAM;IACtB,EAAE,OAAOiB,OAAO;QACd;;;;;KAKC,GACDC,QAAQD,KAAK,CACX,kDACA9B,WACA8B;QAEF,OAAO;YAAE5B,eAAe;YAAGC,SAAS;YAAGC,SAAS;YAAGC,UAAU;QAAM;IACrE;IAEA,MAAM2B,SAAS,MAAMnC,oBAAoB;QACvCS;QACA2B,QAAQ,CAAC,oBAAoB,EAAEjC,UAAU,QAAQ,CAAC;IACpD;IACA,OAAO,aAAKgC;QAAQ9B;;AACtB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/revoke-cache-drop.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 dropPluginSiteCache,\n type PluginSiteCacheResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type { Firestore } from 'firebase-admin/firestore'\n\n/**\n * A PLATFORM EVENT CHANGED WHICH PLUGINS MAY LOAD (AGL-1152, AGL-4121).\n *\n * ## Why a revocation needs it\n *\n * The tenant stamps each `marketplacePlugin` node with its pinned install\n * and its kill-switch state AT COMPOSE TIME, and resolves the signed realm\n * installs once per site, so a page cached before a takedown, a revoked\n * version or withdrawn realm trust goes on serving the old answer until it\n * re-renders. Dropping the cache is what makes the stop take effect on\n * already-cached HTML.\n *\n * ## One tag, not a host walk (AGL-4121)\n *\n * This used to walk the `installs` collection group, expand org-tier pins to\n * their hosts and fan out a whole-site drop to each — capped at 200 sites, and\n * only as good as the walk. Every read that decides which plugins load now\n * carries the platform-wide plugin tag, and so does every page that made one,\n * so a single `plugins: 'everywhere'` drop expires exactly those, on every\n * site, under every cache key, with nothing to walk and nothing to cap. Sites\n * that never ran the listing regenerate their plugin-reading pages once on\n * their next visit; takedowns are rare enough for that to be the right trade.\n *\n * ## It must not be lost\n *\n * The app stages the drop in its durable publish outbox before it sends it,\n * so a refused call is retried by the drain. When neither landed, this\n * THROWS: the stop itself is already written, and the staff action that made\n * it must come back as a failure to retry rather than a success that left the\n * stopped bundle running on cached pages for the week-long backstop.\n */\nexport class PluginCacheDropLostError extends Error {\n constructor(readonly listingId: string) {\n super(\n `The live sites could not be told that marketplace listing ${listingId} ` +\n 'was stopped, and the drop could not be queued for a retry. The stop ' +\n 'is saved; retry the action to drop the cached pages.',\n )\n this.name = 'PluginCacheDropLostError'\n }\n}\n\nexport async function dropCachesForListing(\n _firestore: Firestore,\n listingId: string,\n): Promise<PluginSiteCacheResult> {\n if (!listingId) return { dropped: 0, skipped: 0, complete: true }\n const result = await dropPluginSiteCache({\n hostIds: [],\n plugins: 'everywhere',\n reason: `marketplace listing ${listingId} stopped`,\n })\n if (!result.complete) throw new PluginCacheDropLostError(listingId)\n return result\n}\n"],"names":["dropPluginSiteCache","PluginCacheDropLostError","Error","listingId","name","dropCachesForListing","_firestore","dropped","skipped","complete","result","hostIds","plugins","reason"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,mBAAmB,QAEd,gDAA+C;AAGtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,MAAMC,iCAAiCC;IAC5C,YAAY,AAASC,SAAiB,CAAE;QACtC,KAAK,CACH,CAAC,0DAA0D,EAAEA,UAAU,CAAC,CAAC,GACvE,yEACA,8DAJeA,YAAAA;QAMnB,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,eAAeC,qBACpBC,UAAqB,EACrBH,SAAiB;IAEjB,IAAI,CAACA,WAAW,OAAO;QAAEI,SAAS;QAAGC,SAAS;QAAGC,UAAU;IAAK;IAChE,MAAMC,SAAS,MAAMV,oBAAoB;QACvCW,SAAS,EAAE;QACXC,SAAS;QACTC,QAAQ,CAAC,oBAAoB,EAAEV,UAAU,QAAQ,CAAC;IACpD;IACA,IAAI,CAACO,OAAOD,QAAQ,EAAE,MAAM,IAAIR,yBAAyBE;IACzD,OAAOO;AACT"}