@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
@@ -19,6 +19,7 @@ import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession } from "
19
19
  import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
20
20
  import { FieldValue } from "firebase-admin/firestore";
21
21
  import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
22
+ import { dropCachesForListing } from "./revoke-cache-drop.js";
22
23
  /**
23
24
  * Staff trust-signing for realm plugins (AGL-420), served at
24
25
  * `/api/marketplace/admin/trust` for this plugin's review queue.
@@ -102,6 +103,8 @@ import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write
102
103
  const versionRef = firebaseAdmin.app().firestore().collection('marketplaceListings').doc(listingId).collection('pluginVersions').doc(version);
103
104
  const snapshot = await versionRef.get();
104
105
  const sha256 = snapshot.get('sha256');
106
+ // Read before the write below clears it (AGL-4121).
107
+ const hadRealmTrust = Boolean(snapshot.get('trust'));
105
108
  if (typeof sha256 !== 'string' || !sha256) {
106
109
  return Response.json({
107
110
  error: 'Unknown plugin version'
@@ -165,6 +168,14 @@ import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write
165
168
  },
166
169
  at: FieldValue.serverTimestamp()
167
170
  });
171
+ // Every site running this version resolved its trust once and cached it
172
+ // with its pages, so withdrawing it has to reach those pages now, not at
173
+ // the week-long backstop (AGL-4121). After the audit, so a drop that could
174
+ // neither land nor be queued fails the request loudly without losing the
175
+ // record of the revoke it followed.
176
+ if (action === 'revoke' && hadRealmTrust) {
177
+ await dropCachesForListing(firebaseAdmin.app().firestore(), listingId);
178
+ }
168
179
  return Response.json({
169
180
  ok: true,
170
181
  action,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/admin-trust.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 { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport { signPluginTrust } from '@aglyn/aglyn/plugin-manager/plugin-trust-signing'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n/**\n * Staff trust-signing for realm plugins (AGL-420), served at\n * `/api/marketplace/admin/trust` for this plugin's review queue.\n *\n * It was `apps/console/app/api/admin/sign-plugin/route.ts` until AGL-3080.\n * What moved is everything that is the MARKETPLACE's: `marketplaceListings`\n * is its collection, \"a version must have passed review\" is its rule, and\n * the fields a grant writes are its documents.\n *\n * ⛔ WHAT DID NOT MOVE IS THE KEY. `PLUGIN_TRUST_PRIVATE_KEY` is deployed to\n * the console and nowhere else — never to a tenant runtime — and this\n * package is published to npm, where a self-hoster generates their own\n * pair. So the signature comes from the shell through\n * `signPluginTrust`, and this route refuses the grant outright when it\n * answers `signed: false`. A `trust: 'realm'` flag written without a\n * signature beside it is the one state the loaders cannot tell from\n * tampering.\n *\n * Granting writes\n * `{trust: 'realm', signature}` onto the listing's version doc, where the\n * signature is the platform Ed25519 key's signature over the version's\n * sha256 hex — the loaders (client blob-import + env-gated server import)\n * verify it against `PLUGIN_TRUST_PUBLIC_KEY` before a byte executes.\n * Super staff only: a realm grant lets marketplace code run in the app\n * realm of every workspace that installs it, the largest blast radius any\n * staff action has. Revoking removes the grant (already-loaded realms\n * pick it up on next load; use `revocations/{listingId}` for the hard\n * kill switch).\n *\n * `PLUGIN_TRUST_PRIVATE_KEY` is the base64 PKCS8 DER Ed25519 private key\n * (pair generated by `tools/scripts/generate-plugin-trust-key.mjs`) —\n * deployed ONLY to the console app, never to tenant runtimes.\n */\nasync function handler(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n if (String(decoded['staffRole'] ?? 'support') !== 'super') {\n return Response.json({ error: 'Requires the super staff role' }, { status: 403 })\n }\n\n const listingId = String(body?.listingId ?? '')\n const version = String(body?.version ?? '')\n const action = body?.action === 'revoke' ? 'revoke' : 'grant'\n if (!listingId || !version) {\n return Response.json({ error: 'Missing listingId or version' }, { status: 400 })\n }\n\n const versionRef = firebaseAdmin\n .app()\n .firestore()\n .collection('marketplaceListings')\n .doc(listingId)\n .collection('pluginVersions')\n .doc(version)\n const snapshot = await versionRef.get()\n const sha256 = snapshot.get('sha256')\n if (typeof sha256 !== 'string' || !sha256) {\n return Response.json({ error: 'Unknown plugin version' }, { status: 404 })\n }\n\n // Realm trust is the strongest grant in the platform — it drops a\n // bundle into the app realm, where the sandbox iframe and the manifest\n // CSP no longer stand between it and user data. It cannot be granted to\n // bytes that have not passed review (AGL-966).\n if (action === 'grant' && snapshot.get('reviewState') !== 'approved') {\n return Response.json(\n {\n error:\n 'That version has not passed review — approve it before granting realm trust',\n reviewState: snapshot.get('reviewState') ?? 'unknown',\n },\n { status: 409 },\n )\n }\n\n if (action === 'grant') {\n /*\n * The shell signs; this decides what is worth signing. A refusal here\n * is final — nothing is written, and the 501 is the same answer an\n * unconfigured deployment has always given, now reached through the\n * contract rather than by reading the key.\n */\n const { signed, signature, reason } = await signPluginTrust(sha256)\n if (!signed || !signature) {\n return Response.json(\n {\n error:\n reason ??\n 'Trust signing is not configured (missing PLUGIN_TRUST_PRIVATE_KEY)',\n },\n { status: 501 },\n )\n }\n await versionRef.set(\n {\n trust: 'realm',\n signature,\n trustGrantedBy: decoded.uid,\n trustGrantedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n } else {\n await versionRef.set(\n {\n trust: FieldValue.delete(),\n signature: FieldValue.delete(),\n trustGrantedBy: FieldValue.delete(),\n trustGrantedAt: FieldValue.delete(),\n },\n { merge: true },\n )\n }\n\n await addAdminAudit(firebaseAdmin.app().firestore(), {\n actorUid: decoded.uid,\n action: `plugins.trust.${action}`,\n target: `marketplaceListings/${listingId}/pluginVersions/${version}`,\n after: action === 'grant' ? { trust: 'realm' } : { trust: null },\n at: FieldValue.serverTimestamp(),\n })\n\n return Response.json({ ok: true, action, listingId, version }, { status: 200 })\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours\n // (AGL-1993). Null for anything else, so a real failure keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json({ error: 'Trust signing failed' }, { status: 500 })\n }\n}\n\nexport { handler as marketplaceAdminTrust }\n"],"names":["pluginRequestFromWeb","signPluginTrust","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","invalidIdTokenResponse","FieldValue","addAdminAudit","handler","request","headers","method","body","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","decoded","app","auth","verifyIdToken","email_verified","String","listingId","version","action","versionRef","firestore","collection","doc","snapshot","get","sha256","reviewState","signed","signature","reason","set","trust","trustGrantedBy","uid","trustGrantedAt","serverTimestamp","merge","delete","actorUid","target","after","at","ok","unauthenticated","console","marketplaceAdminTrust"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/admin-trust.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 { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport { signPluginTrust } from '@aglyn/aglyn/plugin-manager/plugin-trust-signing'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\nimport { dropCachesForListing } from './revoke-cache-drop'\n\n/**\n * Staff trust-signing for realm plugins (AGL-420), served at\n * `/api/marketplace/admin/trust` for this plugin's review queue.\n *\n * It was `apps/console/app/api/admin/sign-plugin/route.ts` until AGL-3080.\n * What moved is everything that is the MARKETPLACE's: `marketplaceListings`\n * is its collection, \"a version must have passed review\" is its rule, and\n * the fields a grant writes are its documents.\n *\n * ⛔ WHAT DID NOT MOVE IS THE KEY. `PLUGIN_TRUST_PRIVATE_KEY` is deployed to\n * the console and nowhere else — never to a tenant runtime — and this\n * package is published to npm, where a self-hoster generates their own\n * pair. So the signature comes from the shell through\n * `signPluginTrust`, and this route refuses the grant outright when it\n * answers `signed: false`. A `trust: 'realm'` flag written without a\n * signature beside it is the one state the loaders cannot tell from\n * tampering.\n *\n * Granting writes\n * `{trust: 'realm', signature}` onto the listing's version doc, where the\n * signature is the platform Ed25519 key's signature over the version's\n * sha256 hex — the loaders (client blob-import + env-gated server import)\n * verify it against `PLUGIN_TRUST_PUBLIC_KEY` before a byte executes.\n * Super staff only: a realm grant lets marketplace code run in the app\n * realm of every workspace that installs it, the largest blast radius any\n * staff action has. Revoking removes the grant (already-loaded realms\n * pick it up on next load; use `revocations/{listingId}` for the hard\n * kill switch).\n *\n * `PLUGIN_TRUST_PRIVATE_KEY` is the base64 PKCS8 DER Ed25519 private key\n * (pair generated by `tools/scripts/generate-plugin-trust-key.mjs`) —\n * deployed ONLY to the console app, never to tenant runtimes.\n */\nasync function handler(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n if (String(decoded['staffRole'] ?? 'support') !== 'super') {\n return Response.json({ error: 'Requires the super staff role' }, { status: 403 })\n }\n\n const listingId = String(body?.listingId ?? '')\n const version = String(body?.version ?? '')\n const action = body?.action === 'revoke' ? 'revoke' : 'grant'\n if (!listingId || !version) {\n return Response.json({ error: 'Missing listingId or version' }, { status: 400 })\n }\n\n const versionRef = firebaseAdmin\n .app()\n .firestore()\n .collection('marketplaceListings')\n .doc(listingId)\n .collection('pluginVersions')\n .doc(version)\n const snapshot = await versionRef.get()\n const sha256 = snapshot.get('sha256')\n // Read before the write below clears it (AGL-4121).\n const hadRealmTrust = Boolean(snapshot.get('trust'))\n if (typeof sha256 !== 'string' || !sha256) {\n return Response.json({ error: 'Unknown plugin version' }, { status: 404 })\n }\n\n // Realm trust is the strongest grant in the platform — it drops a\n // bundle into the app realm, where the sandbox iframe and the manifest\n // CSP no longer stand between it and user data. It cannot be granted to\n // bytes that have not passed review (AGL-966).\n if (action === 'grant' && snapshot.get('reviewState') !== 'approved') {\n return Response.json(\n {\n error:\n 'That version has not passed review — approve it before granting realm trust',\n reviewState: snapshot.get('reviewState') ?? 'unknown',\n },\n { status: 409 },\n )\n }\n\n if (action === 'grant') {\n /*\n * The shell signs; this decides what is worth signing. A refusal here\n * is final — nothing is written, and the 501 is the same answer an\n * unconfigured deployment has always given, now reached through the\n * contract rather than by reading the key.\n */\n const { signed, signature, reason } = await signPluginTrust(sha256)\n if (!signed || !signature) {\n return Response.json(\n {\n error:\n reason ??\n 'Trust signing is not configured (missing PLUGIN_TRUST_PRIVATE_KEY)',\n },\n { status: 501 },\n )\n }\n await versionRef.set(\n {\n trust: 'realm',\n signature,\n trustGrantedBy: decoded.uid,\n trustGrantedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n } else {\n await versionRef.set(\n {\n trust: FieldValue.delete(),\n signature: FieldValue.delete(),\n trustGrantedBy: FieldValue.delete(),\n trustGrantedAt: FieldValue.delete(),\n },\n { merge: true },\n )\n }\n\n await addAdminAudit(firebaseAdmin.app().firestore(), {\n actorUid: decoded.uid,\n action: `plugins.trust.${action}`,\n target: `marketplaceListings/${listingId}/pluginVersions/${version}`,\n after: action === 'grant' ? { trust: 'realm' } : { trust: null },\n at: FieldValue.serverTimestamp(),\n })\n\n // Every site running this version resolved its trust once and cached it\n // with its pages, so withdrawing it has to reach those pages now, not at\n // the week-long backstop (AGL-4121). After the audit, so a drop that could\n // neither land nor be queued fails the request loudly without losing the\n // record of the revoke it followed.\n if (action === 'revoke' && hadRealmTrust) {\n await dropCachesForListing(firebaseAdmin.app().firestore(), listingId)\n }\n\n return Response.json({ ok: true, action, listingId, version }, { status: 200 })\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours\n // (AGL-1993). Null for anything else, so a real failure keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json({ error: 'Trust signing failed' }, { status: 500 })\n }\n}\n\nexport { handler as marketplaceAdminTrust }\n"],"names":["pluginRequestFromWeb","signPluginTrust","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","invalidIdTokenResponse","FieldValue","addAdminAudit","dropCachesForListing","handler","request","headers","method","body","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","decoded","app","auth","verifyIdToken","email_verified","String","listingId","version","action","versionRef","firestore","collection","doc","snapshot","get","sha256","hadRealmTrust","Boolean","reviewState","signed","signature","reason","set","trust","trustGrantedBy","uid","trustGrantedAt","serverTimestamp","merge","delete","actorUid","target","after","at","ok","unauthenticated","console","marketplaceAdminTrust"],"mappings":"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"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { firebaseAdmin, getOrgForHost, getRealmPluginInstalls } from "@aglyn/tenant-data-admin";
17
- import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
17
+ import { PUBLISHED_SITE_DATA_TTL_SECONDS, TENANT_CACHE_BACKSTOP_SECONDS, tenantDataTag, tenantPluginGateTags, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
18
18
  import { createConnectionRecordsEnricher, createConnectionVariableComputer, createPluginImageHostsSource, readSiteConnectionRecords, siteDeclaredImageHosts } from "./site-records.js";
19
19
  /*
20
20
  * The site-side readers of declared connections (AGL-4195), wired to the
@@ -28,7 +28,12 @@ import { createConnectionRecordsEnricher, createConnectionVariableComputer, crea
28
28
  * a sync that writes a record should too. A shorter TTL here would cap every
29
29
  * page of the site at it (`render-cache.ts`).
30
30
  */ const SITE_CONNECTION_RECORDS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS;
31
- /** As the site's signed installs are cached (`realm-plugins.ts`), which this reads. */ const SITE_IMAGE_HOSTS_TTL_SECONDS = 300;
31
+ /**
32
+ * As the site's signed installs are cached (`realm-plugins.ts`), which this
33
+ * reads. It reads them INSIDE its own cached callback, where a nested cache
34
+ * adds no tags to the entry, so it carries the plugin tags itself: an
35
+ * uninstall or a takedown must expire the hosts it admitted too (AGL-4121).
36
+ */ const SITE_IMAGE_HOSTS_TTL_SECONDS = TENANT_CACHE_BACKSTOP_SECONDS;
32
37
  const realmInstalls = (hostId)=>getRealmPluginInstalls({
33
38
  hostId
34
39
  });
@@ -72,9 +77,7 @@ const realmInstalls = (hostId)=>getRealmPluginInstalls({
72
77
  hostId
73
78
  ],
74
79
  revalidate: SITE_IMAGE_HOSTS_TTL_SECONDS,
75
- tags: [
76
- tenantDataTag(hostId)
77
- ],
80
+ tags: tenantPluginGateTags(hostId),
78
81
  read
79
82
  });
80
83
  } catch (error) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/site-records-platform.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 { firebaseAdmin, getOrgForHost, getRealmPluginInstalls } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\nimport {\n createConnectionRecordsEnricher,\n createConnectionVariableComputer,\n createPluginImageHostsSource,\n readSiteConnectionRecords,\n siteDeclaredImageHosts,\n type SiteConnectionRecordEntry,\n} from './site-records'\n\n/*\n * The site-side readers of declared connections (AGL-4195), wired to the\n * platform: the Admin SDK, the tenant's render cache, and the page loader's\n * own read of the site's signed installs. Loaded with the first page that\n * needs one of them, never with the boot.\n */\n\nconst firestore = (): FirebaseFirestore.Firestore => firebaseAdmin.app().firestore()\n\n/**\n * As long as the page's own data, and dropped by the same tag: a publish, a\n * plan change and an install change all announce `tenant-data:{hostId}`, and\n * a sync that writes a record should too. A shorter TTL here would cap every\n * page of the site at it (`render-cache.ts`).\n */\nconst SITE_CONNECTION_RECORDS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/** As the site's signed installs are cached (`realm-plugins.ts`), which this reads. */\nconst SITE_IMAGE_HOSTS_TTL_SECONDS = 300\n\nconst realmInstalls = (hostId: string) => getRealmPluginInstalls({ hostId })\n\n/** The site's records, cached per site. A failed read shows none. */\nexport async function cachedSiteConnectionRecords(hostId: string): Promise<SiteConnectionRecordEntry[]> {\n const read = () =>\n readSiteConnectionRecords(\n {\n firestore: firestore(),\n orgForHost: async (id) => {\n const found = await getOrgForHost(id)\n return found ? { orgId: found.orgId, org: found.org as Record<string, unknown> } : null\n },\n },\n hostId,\n )\n try {\n return await withRenderCache({\n key: ['plugin-connection-records', hostId],\n revalidate: SITE_CONNECTION_RECORDS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s records were unreadable', error)\n return []\n }\n}\n\n/** The image hosts the site's signed plugins declare, cached per site. A failed read admits none. */\nexport async function cachedSiteImageHosts(hostId: string): Promise<string[]> {\n const read = () =>\n siteDeclaredImageHosts(\n {\n realmInstalls,\n declaredImageHosts: async (listingId, version) =>\n (\n await firestore()\n .collection('marketplaceListings')\n .doc(listingId)\n .collection('pluginVersions')\n .doc(version)\n .get()\n ).get('manifest.capabilities.imageHosts'),\n },\n hostId,\n )\n try {\n return await withRenderCache({\n key: ['plugin-image-hosts', hostId],\n revalidate: SITE_IMAGE_HOSTS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s plugin image hosts were unreadable', error)\n return []\n }\n}\n\nexport const connectionRecordsEnricher = createConnectionRecordsEnricher({\n realmInstalls,\n records: cachedSiteConnectionRecords,\n})\n\nexport const connectionVariableComputer = createConnectionVariableComputer({ records: cachedSiteConnectionRecords })\n\nexport const pluginImageHostsSource = createPluginImageHostsSource(cachedSiteImageHosts)\n"],"names":["firebaseAdmin","getOrgForHost","getRealmPluginInstalls","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","createConnectionRecordsEnricher","createConnectionVariableComputer","createPluginImageHostsSource","readSiteConnectionRecords","siteDeclaredImageHosts","firestore","app","SITE_CONNECTION_RECORDS_TTL_SECONDS","SITE_IMAGE_HOSTS_TTL_SECONDS","realmInstalls","hostId","cachedSiteConnectionRecords","read","orgForHost","id","found","orgId","org","key","revalidate","tags","error","console","cachedSiteImageHosts","declaredImageHosts","listingId","version","collection","doc","get","connectionRecordsEnricher","records","connectionVariableComputer","pluginImageHostsSource"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,EAAEC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AAC/F,SACEC,+BAA+B,EAC/BC,aAAa,EACbC,eAAe,QACV,wCAAuC;AAC9C,SACEC,+BAA+B,EAC/BC,gCAAgC,EAChCC,4BAA4B,EAC5BC,yBAAyB,EACzBC,sBAAsB,QAEjB,oBAAgB;AAEvB;;;;;CAKC,GAED,MAAMC,YAAY,IAAmCX,cAAcY,GAAG,GAAGD,SAAS;AAElF;;;;;CAKC,GACD,MAAME,sCAAsCV;AAE5C,qFAAqF,GACrF,MAAMW,+BAA+B;AAErC,MAAMC,gBAAgB,CAACC,SAAmBd,uBAAuB;QAAEc;IAAO;AAE1E,mEAAmE,GACnE,OAAO,eAAeC,4BAA4BD,MAAc;IAC9D,MAAME,OAAO,IACXT,0BACE;YACEE,WAAWA;YACXQ,YAAY,OAAOC;gBACjB,MAAMC,QAAQ,MAAMpB,cAAcmB;gBAClC,OAAOC,QAAQ;oBAAEC,OAAOD,MAAMC,KAAK;oBAAEC,KAAKF,MAAME,GAAG;gBAA4B,IAAI;YACrF;QACF,GACAP;IAEJ,IAAI;QACF,OAAO,MAAMX,gBAAgB;YAC3BmB,KAAK;gBAAC;gBAA6BR;aAAO;YAC1CS,YAAYZ;YACZa,MAAM;gBAACtB,cAAcY;aAAQ;YAC7BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,wDAAwDA;QACtE,OAAO,EAAE;IACX;AACF;AAEA,mGAAmG,GACnG,OAAO,eAAeE,qBAAqBb,MAAc;IACvD,MAAME,OAAO,IACXR,uBACE;YACEK;YACAe,oBAAoB,OAAOC,WAAWC,UACpC,AACE,CAAA,MAAMrB,YACHsB,UAAU,CAAC,uBACXC,GAAG,CAACH,WACJE,UAAU,CAAC,kBACXC,GAAG,CAACF,SACJG,GAAG,EAAC,EACPA,GAAG,CAAC;QACV,GACAnB;IAEJ,IAAI;QACF,OAAO,MAAMX,gBAAgB;YAC3BmB,KAAK;gBAAC;gBAAsBR;aAAO;YACnCS,YAAYX;YACZY,MAAM;gBAACtB,cAAcY;aAAQ;YAC7BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,mEAAmEA;QACjF,OAAO,EAAE;IACX;AACF;AAEA,OAAO,MAAMS,4BAA4B9B,gCAAgC;IACvES;IACAsB,SAASpB;AACX,GAAE;AAEF,OAAO,MAAMqB,6BAA6B/B,iCAAiC;IAAE8B,SAASpB;AAA4B,GAAE;AAEpH,OAAO,MAAMsB,yBAAyB/B,6BAA6BqB,sBAAqB"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/site-records-platform.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 { firebaseAdmin, getOrgForHost, getRealmPluginInstalls } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n TENANT_CACHE_BACKSTOP_SECONDS,\n tenantDataTag,\n tenantPluginGateTags,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\nimport {\n createConnectionRecordsEnricher,\n createConnectionVariableComputer,\n createPluginImageHostsSource,\n readSiteConnectionRecords,\n siteDeclaredImageHosts,\n type SiteConnectionRecordEntry,\n} from './site-records'\n\n/*\n * The site-side readers of declared connections (AGL-4195), wired to the\n * platform: the Admin SDK, the tenant's render cache, and the page loader's\n * own read of the site's signed installs. Loaded with the first page that\n * needs one of them, never with the boot.\n */\n\nconst firestore = (): FirebaseFirestore.Firestore => firebaseAdmin.app().firestore()\n\n/**\n * As long as the page's own data, and dropped by the same tag: a publish, a\n * plan change and an install change all announce `tenant-data:{hostId}`, and\n * a sync that writes a record should too. A shorter TTL here would cap every\n * page of the site at it (`render-cache.ts`).\n */\nconst SITE_CONNECTION_RECORDS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * As the site's signed installs are cached (`realm-plugins.ts`), which this\n * reads. It reads them INSIDE its own cached callback, where a nested cache\n * adds no tags to the entry, so it carries the plugin tags itself: an\n * uninstall or a takedown must expire the hosts it admitted too (AGL-4121).\n */\nconst SITE_IMAGE_HOSTS_TTL_SECONDS = TENANT_CACHE_BACKSTOP_SECONDS\n\nconst realmInstalls = (hostId: string) => getRealmPluginInstalls({ hostId })\n\n/** The site's records, cached per site. A failed read shows none. */\nexport async function cachedSiteConnectionRecords(hostId: string): Promise<SiteConnectionRecordEntry[]> {\n const read = () =>\n readSiteConnectionRecords(\n {\n firestore: firestore(),\n orgForHost: async (id) => {\n const found = await getOrgForHost(id)\n return found ? { orgId: found.orgId, org: found.org as Record<string, unknown> } : null\n },\n },\n hostId,\n )\n try {\n return await withRenderCache({\n key: ['plugin-connection-records', hostId],\n revalidate: SITE_CONNECTION_RECORDS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s records were unreadable', error)\n return []\n }\n}\n\n/** The image hosts the site's signed plugins declare, cached per site. A failed read admits none. */\nexport async function cachedSiteImageHosts(hostId: string): Promise<string[]> {\n const read = () =>\n siteDeclaredImageHosts(\n {\n realmInstalls,\n declaredImageHosts: async (listingId, version) =>\n (\n await firestore()\n .collection('marketplaceListings')\n .doc(listingId)\n .collection('pluginVersions')\n .doc(version)\n .get()\n ).get('manifest.capabilities.imageHosts'),\n },\n hostId,\n )\n try {\n return await withRenderCache({\n key: ['plugin-image-hosts', hostId],\n revalidate: SITE_IMAGE_HOSTS_TTL_SECONDS,\n tags: tenantPluginGateTags(hostId),\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s plugin image hosts were unreadable', error)\n return []\n }\n}\n\nexport const connectionRecordsEnricher = createConnectionRecordsEnricher({\n realmInstalls,\n records: cachedSiteConnectionRecords,\n})\n\nexport const connectionVariableComputer = createConnectionVariableComputer({ records: cachedSiteConnectionRecords })\n\nexport const pluginImageHostsSource = createPluginImageHostsSource(cachedSiteImageHosts)\n"],"names":["firebaseAdmin","getOrgForHost","getRealmPluginInstalls","PUBLISHED_SITE_DATA_TTL_SECONDS","TENANT_CACHE_BACKSTOP_SECONDS","tenantDataTag","tenantPluginGateTags","withRenderCache","createConnectionRecordsEnricher","createConnectionVariableComputer","createPluginImageHostsSource","readSiteConnectionRecords","siteDeclaredImageHosts","firestore","app","SITE_CONNECTION_RECORDS_TTL_SECONDS","SITE_IMAGE_HOSTS_TTL_SECONDS","realmInstalls","hostId","cachedSiteConnectionRecords","read","orgForHost","id","found","orgId","org","key","revalidate","tags","error","console","cachedSiteImageHosts","declaredImageHosts","listingId","version","collection","doc","get","connectionRecordsEnricher","records","connectionVariableComputer","pluginImageHostsSource"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,EAAEC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AAC/F,SACEC,+BAA+B,EAC/BC,6BAA6B,EAC7BC,aAAa,EACbC,oBAAoB,EACpBC,eAAe,QACV,wCAAuC;AAC9C,SACEC,+BAA+B,EAC/BC,gCAAgC,EAChCC,4BAA4B,EAC5BC,yBAAyB,EACzBC,sBAAsB,QAEjB,oBAAgB;AAEvB;;;;;CAKC,GAED,MAAMC,YAAY,IAAmCb,cAAcc,GAAG,GAAGD,SAAS;AAElF;;;;;CAKC,GACD,MAAME,sCAAsCZ;AAE5C;;;;;CAKC,GACD,MAAMa,+BAA+BZ;AAErC,MAAMa,gBAAgB,CAACC,SAAmBhB,uBAAuB;QAAEgB;IAAO;AAE1E,mEAAmE,GACnE,OAAO,eAAeC,4BAA4BD,MAAc;IAC9D,MAAME,OAAO,IACXT,0BACE;YACEE,WAAWA;YACXQ,YAAY,OAAOC;gBACjB,MAAMC,QAAQ,MAAMtB,cAAcqB;gBAClC,OAAOC,QAAQ;oBAAEC,OAAOD,MAAMC,KAAK;oBAAEC,KAAKF,MAAME,GAAG;gBAA4B,IAAI;YACrF;QACF,GACAP;IAEJ,IAAI;QACF,OAAO,MAAMX,gBAAgB;YAC3BmB,KAAK;gBAAC;gBAA6BR;aAAO;YAC1CS,YAAYZ;YACZa,MAAM;gBAACvB,cAAca;aAAQ;YAC7BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,wDAAwDA;QACtE,OAAO,EAAE;IACX;AACF;AAEA,mGAAmG,GACnG,OAAO,eAAeE,qBAAqBb,MAAc;IACvD,MAAME,OAAO,IACXR,uBACE;YACEK;YACAe,oBAAoB,OAAOC,WAAWC,UACpC,AACE,CAAA,MAAMrB,YACHsB,UAAU,CAAC,uBACXC,GAAG,CAACH,WACJE,UAAU,CAAC,kBACXC,GAAG,CAACF,SACJG,GAAG,EAAC,EACPA,GAAG,CAAC;QACV,GACAnB;IAEJ,IAAI;QACF,OAAO,MAAMX,gBAAgB;YAC3BmB,KAAK;gBAAC;gBAAsBR;aAAO;YACnCS,YAAYX;YACZY,MAAMtB,qBAAqBY;YAC3BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,mEAAmEA;QACjF,OAAO,EAAE;IACX;AACF;AAEA,OAAO,MAAMS,4BAA4B9B,gCAAgC;IACvES;IACAsB,SAASpB;AACX,GAAE;AAEF,OAAO,MAAMqB,6BAA6B/B,iCAAiC;IAAE8B,SAASpB;AAA4B,GAAE;AAEpH,OAAO,MAAMsB,yBAAyB/B,6BAA6BqB,sBAAqB"}
@@ -100,28 +100,37 @@ import { compareArtifactVersions } from "../model/update-state.js";
100
100
  }
101
101
  }
102
102
  /**
103
- * Tells the live sites a pin changed (AGL-3741).
103
+ * Tells the live sites a pin changed (AGL-3741, AGL-4121).
104
104
  *
105
- * The tenant caches each site's resolved plugins under its
106
- * `tenant-data:{hostId}` tag, and an install or uninstall used to reach live
107
- * pages only when that entry expired — which is why it expired every minute,
108
- * re-reading the install pins, listings, versions and revocations of every
109
- * site sixty times an hour. An org-tier pin applies to every site in the org,
110
- * so it is those sites that are told. Best effort and never throws, like the
111
- * revocation fan-out it shares: the pin itself is already written.
105
+ * The tenant caches each site's resolved plugins, and every page that read
106
+ * them, under the site's plugin tag, and an install or uninstall reaches live
107
+ * pages only through this drop — the cache behind it is held for the
108
+ * platform's week-long backstop. An org-tier pin applies to every site in the
109
+ * org, so it is those sites that are told.
110
+ *
111
+ * `plugins: 'host'` makes the tenant expire the plugin set immediately rather
112
+ * than stale-while-revalidate, so an uninstalled bundle does not run for one
113
+ * more visitor. The app stages each drop in its durable outbox first, so a
114
+ * refused call is retried; the answer is `false` only when a drop neither
115
+ * landed nor was queued, which the caller reports as a failure. Never throws:
116
+ * the pin itself is already written.
112
117
  */ async function announcePinChange(firestore, options) {
113
118
  try {
114
119
  const hostIds = options.scope === 'org' && options.orgId ? (await firestore.collection('hosts').where('orgId', '==', options.orgId).get()).docs.map((doc)=>doc.id) : [
115
120
  options.hostId
116
121
  ];
117
- await dropPluginSiteCache({
122
+ const result = await dropPluginSiteCache({
118
123
  hostIds,
119
- reason: options.reason
124
+ reason: options.reason,
125
+ plugins: 'host'
120
126
  });
127
+ return result.complete;
121
128
  } catch (error) {
122
129
  console.error('[marketplace] the sites running a changed pin were not told', error);
130
+ return false;
123
131
  }
124
132
  }
133
+ /** What an uninstall answers when its live-site drop was lost (AGL-4121). */ const PIN_DROP_LOST_ERROR = 'The plugin was removed, but the live site could not be refreshed. Try again to finish removing it from published pages.';
125
134
  export const installPluginHandler = async (req, res)=>{
126
135
  var _ref, _ref1, _req_headers_authorization;
127
136
  var _req_body, _req_body1, _req_body2;
@@ -178,7 +187,7 @@ export const installPluginHandler = async (req, res)=>{
178
187
  // one exception, a declared connection's grant and record (AGL-4194).
179
188
  if (((_req_body3 = req.body) == null ? void 0 : _req_body3.action) === 'uninstall') {
180
189
  var _ref2;
181
- var _req_body5, _req_body6;
190
+ var _req_body5, _req_body6, _req_body7;
182
191
  const orgId = await resolveOrgIdForHost(hostId);
183
192
  if (((_req_body5 = req.body) == null ? void 0 : _req_body5.scope) === 'org' && !orgId) {
184
193
  return res.status(409).json({
@@ -196,21 +205,24 @@ export const installPluginHandler = async (req, res)=>{
196
205
  const pinnedVersion = pinExisted ? pinSnapshot == null ? void 0 : pinSnapshot.get('version') : null;
197
206
  if (pinRef) await pinRef.delete();
198
207
  if (pinExisted) {
199
- var _req_body7, _req_body8;
208
+ var _req_body8;
200
209
  await forgetUndeclaredConnections(firestore, {
201
- scope: ((_req_body7 = req.body) == null ? void 0 : _req_body7.scope) === 'org' ? 'org' : 'host',
210
+ scope: ((_req_body8 = req.body) == null ? void 0 : _req_body8.scope) === 'org' ? 'org' : 'host',
202
211
  hostId,
203
212
  orgId,
204
213
  listingId,
205
214
  previousPin: pinSnapshot == null ? void 0 : pinSnapshot.data()
206
215
  });
207
- await announcePinChange(firestore, {
208
- scope: ((_req_body8 = req.body) == null ? void 0 : _req_body8.scope) === 'org' ? 'org' : 'host',
209
- hostId,
210
- orgId,
211
- reason: `marketplace listing ${listingId} uninstalled`
212
- });
213
216
  }
217
+ // Announced even when no pin was found (AGL-4121): a retry after a lost
218
+ // drop finds the pin already gone, and it is the retry that has to
219
+ // reach the live pages. A repeat drop is a no-op.
220
+ const told = await announcePinChange(firestore, {
221
+ scope: ((_req_body7 = req.body) == null ? void 0 : _req_body7.scope) === 'org' ? 'org' : 'host',
222
+ hostId,
223
+ orgId,
224
+ reason: `marketplace listing ${listingId} uninstalled`
225
+ });
214
226
  if (pinExisted) {
215
227
  const listingRef = firestore.collection('marketplaceListings').doc(listingId);
216
228
  await recordVersionMove({
@@ -230,6 +242,9 @@ export const installPluginHandler = async (req, res)=>{
230
242
  }).catch(()=>undefined);
231
243
  }
232
244
  await syncEnabledPlugins(firestore, orgId, hostId, listingId, 'remove-if-unpinned');
245
+ if (!told) return res.status(503).json({
246
+ error: PIN_DROP_LOST_ERROR
247
+ });
233
248
  return res.status(200).json({
234
249
  ok: true
235
250
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport {\n resolveOrgIdForHost, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n isCompatibleHostAbi,\n isFirstPartyPlugin,\n isPluginRevoked,\n newestInstallableVersion,\n PLUGIN_HOST_ABI_VERSION,\n type PluginApiHandler,\n type PluginRevocation,\n} from '@aglyn/aglyn/server'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { ownsListingHere } from './own-listing'\nimport { requirePurchase } from './purchase-entitlement'\nimport { pinnedProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\nimport {\n isListingBrowsable,\n isPrivateListing,\n isVersionApproved,\n listingArtifactType,\n} from '../model/marketplace'\nimport { compareArtifactVersions } from '../model/update-state'\n\n/**\n * Installs (or upgrades) a marketplace plugin into a host (AGL-45), pinning a\n * version per AGL-43 §3.4. Writes `hosts/{hostId}/installs/{listingId}`\n * with the exact `{version, sha256}` — upgrades are explicit (a new pin),\n * so a publisher can never swap the code a consumer runs. Access mirrors\n * component installs: free, purchased (webhook-written), or own listing.\n * The pin references the immutable artifact; the sandboxed loader fetches\n * and integrity-checks it at render time.\n *\n * Enablement sync (AGL-424): marketplace plugins ride the same\n * `org.enabledPlugins` switchboard first-party plugins use — but ONLY for\n * workspaces that have explicitly configured the field. Install appends\n * the listing id there; uninstall removes it once no pin (org or acting\n * host) remains. Workspaces that never touched the switchboard stay\n * default-open (absent field ⇒ installs load), so pre-sync installs keep\n * working.\n */\n\n/** Keeps `org.enabledPlugins` in step with install pins (AGL-424). */\nasync function syncEnabledPlugins(\n firestore: FirebaseFirestore.Firestore,\n orgId: string | null,\n hostId: string,\n listingId: string,\n action: 'add' | 'remove-if-unpinned',\n): Promise<void> {\n if (!orgId) return\n // `enabledPlugins` is a flat mix of first-party bundle ids and marketplace\n // listing ids (AGL-777). A listing id is a Firestore doc id, so it can't\n // normally collide with a bundle name — but guard anyway: an install sync\n // must NEVER arrayUnion/arrayRemove a bundle id, which would silently\n // enable or disable a platform plugin as a side effect of a purchase.\n if (isFirstPartyPlugin(listingId)) return\n const orgRef = firestore.collection('orgs').doc(orgId)\n const configured = (await orgRef.get()).get('enabledPlugins')\n // Absent field = default-open workspace; nothing to sync.\n if (!Array.isArray(configured)) return\n if (action === 'add') {\n if (configured.includes(listingId)) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayUnion(listingId),\n })\n return\n }\n const [orgPin, hostPin] = await Promise.all([\n orgRef.collection('installs').doc(listingId).get(),\n firestore\n .collection('hosts')\n .doc(hostId)\n .collection('installs')\n .doc(listingId)\n .get(),\n ])\n if (orgPin.exists || hostPin.exists) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayRemove(listingId),\n })\n}\n/**\n * A pin that moved takes with it the declared connections it no longer\n * declares (AGL-4194): an uninstall, or a re-pin to a version without one.\n * Each such connection's grant is revoked at the provider (best effort, with\n * the endpoint the PREVIOUS pin declared, the new one having none) and the\n * connection and its record are deleted. A site that still runs the listing\n * through its other pin (its own, or the workspace's) keeps its connection.\n * Never throws: the pin itself is already written.\n */\nasync function forgetUndeclaredConnections(\n firestore: FirebaseFirestore.Firestore,\n options: {\n scope: 'org' | 'host'\n hostId: string\n orgId: string | null\n listingId: string\n previousPin: Record<string, unknown> | undefined\n },\n): Promise<void> {\n try {\n const { pinConnections, reconcileListingConnections } = await import('./connections/cleanup')\n const previous = pinConnections(options.previousPin)\n if (!previous.length) return\n const orgId = options.orgId ?? (await resolveOrgIdForHost(options.hostId)) ?? null\n const hostIds =\n options.scope === 'org' && orgId\n ? (await firestore.collection('hosts').where('orgId', '==', orgId).get()).docs.map((doc) => doc.id)\n : [options.hostId]\n await reconcileListingConnections(firestore, { hostIds, orgId, listingId: options.listingId, previous })\n } catch (error) {\n console.error('[marketplace] the connections of a moved pin were not cleaned up', error)\n }\n}\n\n/**\n * Tells the live sites a pin changed (AGL-3741).\n *\n * The tenant caches each site's resolved plugins under its\n * `tenant-data:{hostId}` tag, and an install or uninstall used to reach live\n * pages only when that entry expired — which is why it expired every minute,\n * re-reading the install pins, listings, versions and revocations of every\n * site sixty times an hour. An org-tier pin applies to every site in the org,\n * so it is those sites that are told. Best effort and never throws, like the\n * revocation fan-out it shares: the pin itself is already written.\n */\nasync function announcePinChange(\n firestore: FirebaseFirestore.Firestore,\n options: { scope: 'org' | 'host'; hostId: string; orgId: string | null; reason: string },\n): Promise<void> {\n try {\n const hostIds =\n options.scope === 'org' && options.orgId\n ? (\n await firestore.collection('hosts').where('orgId', '==', options.orgId).get()\n ).docs.map((doc) => doc.id)\n : [options.hostId]\n await dropPluginSiteCache({ hostIds, reason: options.reason })\n } catch (error) {\n console.error('[marketplace] the sites running a changed pin were not told', error)\n }\n}\n\nexport const installPluginHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n // Optional explicit version for upgrade/downgrade; defaults to latest.\n const requestedVersion = req.body?.version\n ? String(req.body.version)\n : undefined\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n // Uninstalls (AGL-237/424). Org pins are API-managed (rules deny\n // client writes); host uninstalls also come through here so the\n // enablement sync runs. Plugin-owned data is never deleted — only the\n // pin (and the switchboard entry when no pin remains anywhere) and, the\n // one exception, a declared connection's grant and record (AGL-4194).\n if (req.body?.action === 'uninstall') {\n const orgId = await resolveOrgIdForHost(hostId)\n if (req.body?.scope === 'org' && !orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n const pinRef =\n req.body?.scope === 'org'\n ? orgId\n ? firestore.collection('orgs').doc(orgId).collection('installs').doc(listingId)\n : null\n : firestore.collection('hosts').doc(hostId).collection('installs').doc(listingId)\n // Active-install accounting (AGL-880): decrement only when a pin actually\n // existed, so a repeat/no-op uninstall never over-counts. `installCount`\n // is a cumulative all-time total and is deliberately left untouched.\n const pinSnapshot = pinRef ? await pinRef.get() : null\n const pinExisted = pinSnapshot?.exists ?? false\n // Read before the delete: the version being left is what the per-version\n // tally has to decrement, and it is gone the moment the pin is.\n const pinnedVersion = pinExisted ? pinSnapshot?.get('version') : null\n if (pinRef) await pinRef.delete()\n if (pinExisted) {\n await forgetUndeclaredConnections(firestore, {\n scope: req.body?.scope === 'org' ? 'org' : 'host',\n hostId,\n orgId,\n listingId,\n previousPin: pinSnapshot?.data(),\n })\n await announcePinChange(firestore, {\n scope: req.body?.scope === 'org' ? 'org' : 'host',\n hostId,\n orgId,\n reason: `marketplace listing ${listingId} uninstalled`,\n })\n }\n if (pinExisted) {\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: pinnedVersion,\n to: null,\n })\n await firestore\n .runTransaction(async (tx) => {\n const snapshot = await tx.get(listingRef)\n const current = Number(snapshot.get('activeInstalls') ?? 0)\n tx.update(listingRef, { activeInstalls: Math.max(0, current - 1) })\n })\n .catch(() => undefined)\n }\n await syncEnabledPlugins(\n firestore,\n orgId,\n hostId,\n listingId,\n 'remove-if-unpinned',\n )\n return res.status(200).json({ ok: true })\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs too (AGL-948) — the revocation\n // check below stops a hidden listing anyway, but only while the\n // revocation stands, and a takedown is not conditional on it.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'plugin'\n ) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // The publisher installs their own listing for free, and may install\n // an unapproved version of it to test. Org-owned now (AGL-652), so this\n // asks the publishing org's members (comparing a uid to an org id would\n // never match). Membership, not a permission (AGL-4047): it is a fact\n // about who published the listing.\n //\n // TWO facts, correlated (AGL-2484). Membership answers a question about\n // the PUBLISHER org alone; the install TARGET is a different org,\n // resolved from the host by the permission gate above. Nothing tied them\n // together, so one flag computed from org P waived three gates on an\n // install landing in org Q: review, version resolution, and payment. A\n // user who administers publisher org P and holds `installPlugins` on a\n // host in org Q could therefore push P's pending — or rejected — bundle\n // onto Q's live public site, for free.\n //\n // Same-org is a NECESSARY condition, never a sufficient one: the\n // membership check still has to pass, so this narrows the waiver and\n // cannot widen it. A listing with no `profileId` is owned by nobody and\n // waives nothing. Every install door asks this same definition now\n // (`ownsListingHere`, AGL-4047); plugins were the first to need it.\n const ownsListing = await ownsListingHere(firestore, {\n uid: decoded.uid,\n publisherOrgId: listing.profileId,\n targetOrgId: membership.orgId,\n })\n\n // Private plugins are installable ONLY by the owning org (AGL-968).\n // This is what makes private mean private; the UI merely hides them.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // Installs enforce the same review state the marketplace browses by\n // (AGL-965). `isListingBrowsable` used to be applied ONLY by the browse\n // UI, so a listing that was never reviewed, was rejected, or has since\n // been de-listed could still be installed by anyone who knew its id and\n // posted here directly — which also made \"delist\" a suggestion rather\n // than a control. The publisher keeps installing their own listing to\n // test it before review.\n if (!isListingBrowsable(listing) && !isPrivateListing(listing) && !ownsListing) {\n return res\n .status(409)\n .json({ error: 'This plugin is not available for install' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n // Installs resolve the newest APPROVED version, never `latestVersion`\n // (AGL-966). Before this, publishing v1.0.1 to a verified listing put\n // unreviewed code in front of every workspace immediately: the listing\n // kept its status, the queue never surfaced the update, and this line\n // pinned whatever had just been uploaded. A pending version is now\n // simply not offered, and the previously approved one keeps installing.\n //\n // The publisher may still install their own unapproved version — that\n // is how you test a plugin before submitting it. It reaches only their\n // own sites because `ownsListing` now REQUIRES the install to land in\n // the publishing org (AGL-2484); this sentence used to be an assertion\n // about code that did not check.\n // Read the kill switch BEFORE resolving, not after (AGL-2368).\n //\n // The order used to be: pick the newest APPROVED version, then 409 if that\n // one happens to be revoked. Revocation was never an input to the choice,\n // only a veto on it — so stopping one build stopped the whole listing.\n // v2.0.0 revoked with v1.0.0 approved and installable resolved to v2.0.0\n // and answered 409, and `requirePurchase` above has already taken the\n // buyer's money by then. A per-version revocation is deliberately NOT\n // listing-wide (`isListingWideRevocation`); that path made it so anyway.\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n\n let version = requestedVersion ?? ''\n let versionData: any\n if (version) {\n versionData = (\n await listingRef.collection('pluginVersions').doc(version).get()\n ).data()\n if (!isVersionApproved(versionData) && !ownsListing) {\n return res.status(409).json({\n error: 'That version has not passed review yet',\n reviewState: versionData?.reviewState ?? 'unknown',\n })\n }\n } else {\n const approvedSnapshot = await listingRef\n .collection('pluginVersions')\n .orderBy('publishedAt', 'desc')\n .limit(25)\n .get()\n const candidates = approvedSnapshot.docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n reviewState: doc.get('reviewState'),\n publishedAt: doc.get('publishedAt'),\n data: doc.data(),\n }))\n // The SHARED predicate (AGL-2306/2368): approved AND not revoked, which\n // is the same function `repairLatestApprovedVersion` writes the listing\n // mirror from. Two readings of \"what is on offer\" is how the badge and\n // the route came to disagree; one function is how they cannot.\n //\n // Note this also orders by version rather than by `publishedAt`, which\n // is what the mirror has always done — so the version this route hands\n // over is now the version the listing page advertised.\n const newestVersion = newestInstallableVersion(\n candidates,\n revocation,\n compareArtifactVersions,\n )\n const newest = newestVersion\n ? (candidates.find((entry) => entry.version === newestVersion) as\n | { version: string; data: any }\n | undefined)\n : undefined\n // The publisher testing their own listing falls back to latest.\n const fallback = ownsListing\n ? candidates.find(\n (entry) => entry.version === String(listing.latestVersion ?? ''),\n )\n : undefined\n const chosen = newest ?? fallback\n if (!chosen) {\n return res.status(409).json({\n error: 'This plugin has no reviewed version available to install',\n })\n }\n version = chosen.version\n versionData = (chosen as { data?: unknown }).data\n }\n if (!versionData?.sha256 || !versionData?.manifest) {\n return res.status(404).json({ error: 'Unknown plugin version' })\n }\n\n // Kill switch (AGL-43 §3.5): a revoked version can't be installed.\n // Shares the predicate with the loaders (AGL-1085) so \"installable\" and\n // \"runnable\" cannot drift apart.\n //\n // Still checked after resolution, and still load-bearing: the unpinned\n // branch can no longer choose a revoked version, but an EXPLICIT pin and\n // the publisher's `latestVersion` fallback both can, and a listing-wide\n // takedown revokes whatever either of them names.\n if (isPluginRevoked(revocation, version)) {\n return res\n .status(409)\n .json({ error: 'This plugin version has been revoked' })\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Install tier (AGL-237): org-extending plugins install once for every\n // host in the org; host-only plugins stay pinned to the host.\n const scope = req.body?.scope === 'org' ? 'org' : 'host'\n let installRef = hostRef.collection('installs').doc(listingId)\n let pinOrgId: string | null = null\n if (scope === 'org') {\n const orgId = await resolveOrgIdForHost(hostId)\n pinOrgId = orgId ?? null\n if (!orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n installRef = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('installs')\n .doc(listingId)\n }\n const existing = await installRef.get()\n const pin = {\n listingId,\n profileId: listing.profileId,\n displayName: listing.displayName,\n pluginId: versionData.manifest.id,\n version,\n sha256: versionData.sha256,\n objectPath: versionData.objectPath,\n manifest: versionData.manifest,\n // The same provenance stamp every other artifact type now carries\n // (AGL-1015), so one reader answers \"where did this come from\" for\n // pinned and copied artifacts alike. The flat pin fields stay: they\n // are what the loader reads.\n installedFrom: pinnedProvenance({\n listingId,\n listing,\n version,\n sha256: versionData.sha256,\n artifactType: 'plugin',\n }),\n ...(existing.exists ? {} : { installedAt: now }),\n updatedAt: now,\n }\n // Each field replaced WHOLE (AGL-4194). `merge: true` merged the new\n // version's manifest INTO the old one's, so a key the new version dropped\n // — its `connections`, say — survived on the pin and went on being read\n // as declared. The fields not written here are still kept.\n await installRef.set(pin, { mergeFields: Object.keys(pin) })\n if (existing.exists) {\n await forgetUndeclaredConnections(firestore, {\n scope,\n hostId,\n orgId: pinOrgId,\n listingId,\n previousPin: existing.data(),\n })\n }\n await announcePinChange(firestore, {\n scope,\n hostId,\n orgId: pinOrgId,\n reason: `marketplace listing ${listingId} installed at ${version}`,\n })\n if (!existing.exists) {\n await listingRef\n .update({\n // installCount is the cumulative all-time total; activeInstalls\n // mirrors live pins and is decremented on uninstall (AGL-880).\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n activeInstalls: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n // Per-version tally (AGL-1036). This runs on EVERY pin write, not just the\n // first: a re-pin is the common case and the one the listing-level counters\n // above are blind to — the pin already exists, so nothing there moves while\n // the install changes version underneath.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: existing.exists ? existing.get('version') : null,\n to: version,\n })\n\n // Enablement sync (AGL-424): the new install loads on next visit for\n // workspaces with a configured switchboard.\n await syncEnabledPlugins(\n firestore,\n await resolveOrgIdForHost(hostId),\n hostId,\n listingId,\n 'add',\n )\n\n // ABI heads-up (AGL-429): the pin succeeds (explicit choice), but the\n // loaders will refuse an incompatible bundle — tell the installer now.\n const manifestHostAbi = Number(versionData.manifest?.hostAbi)\n const hostAbiWarning =\n Number.isInteger(manifestHostAbi) &&\n manifestHostAbi > 0 &&\n !isCompatibleHostAbi(manifestHostAbi)\n ? `Built for host ABI ${manifestHostAbi}; this platform runs ` +\n `${PLUGIN_HOST_ABI_VERSION}. The plugin will not load until the ` +\n 'publisher ships a compatible version.'\n : undefined\n\n return res.status(200).json({\n installed: true,\n upgraded: existing.exists,\n version,\n ...(hostAbiWarning ? { hostAbiWarning } : {}),\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","resolveOrgIdForHost","firebaseAdmin","isCompatibleHostAbi","isFirstPartyPlugin","isPluginRevoked","newestInstallableVersion","PLUGIN_HOST_ABI_VERSION","dropPluginSiteCache","resolveOrgPermissions","ownsListingHere","requirePurchase","pinnedProvenance","recordVersionMove","isPublisherSecurityLocked","isListingBrowsable","isPrivateListing","isVersionApproved","listingArtifactType","compareArtifactVersions","syncEnabledPlugins","firestore","orgId","hostId","listingId","action","orgRef","collection","doc","configured","get","Array","isArray","includes","update","enabledPlugins","FieldValue","arrayUnion","orgPin","hostPin","Promise","all","exists","arrayRemove","forgetUndeclaredConnections","options","pinConnections","reconcileListingConnections","previous","previousPin","length","hostIds","scope","where","docs","map","id","error","console","announcePinChange","reason","installPluginHandler","req","res","method","status","json","String","body","requestedVersion","version","undefined","authorization","headers","idToken","startsWith","slice","listing","membership","versionData","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","hostRef","hostSnapshot","pinRef","pinSnapshot","pinExisted","pinnedVersion","delete","data","listingRef","artifactType","from","to","runTransaction","tx","snapshot","current","Number","activeInstalls","Math","max","catch","ok","listingSnapshot","deletedAt","hiddenAt","profileId","ownsListing","publisherOrgId","targetOrgId","priceUsd","unpaid","buyerUid","buyerOrgId","revocation","reviewState","approvedSnapshot","orderBy","limit","candidates","publishedAt","newestVersion","newest","find","entry","fallback","latestVersion","chosen","sha256","manifest","now","serverTimestamp","installRef","pinOrgId","existing","pin","displayName","pluginId","objectPath","installedFrom","installedAt","updatedAt","set","mergeFields","Object","keys","installCount","increment","manifestHostAbi","hostAbi","hostAbiWarning","isInteger","installed","upgraded"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport {\n resolveOrgIdForHost, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n isCompatibleHostAbi,\n isFirstPartyPlugin,\n isPluginRevoked,\n newestInstallableVersion,\n PLUGIN_HOST_ABI_VERSION,\n type PluginApiHandler,\n type PluginRevocation,\n} from '@aglyn/aglyn/server'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { ownsListingHere } from './own-listing'\nimport { requirePurchase } from './purchase-entitlement'\nimport { pinnedProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\nimport {\n isListingBrowsable,\n isPrivateListing,\n isVersionApproved,\n listingArtifactType,\n} from '../model/marketplace'\nimport { compareArtifactVersions } from '../model/update-state'\n\n/**\n * Installs (or upgrades) a marketplace plugin into a host (AGL-45), pinning a\n * version per AGL-43 §3.4. Writes `hosts/{hostId}/installs/{listingId}`\n * with the exact `{version, sha256}` — upgrades are explicit (a new pin),\n * so a publisher can never swap the code a consumer runs. Access mirrors\n * component installs: free, purchased (webhook-written), or own listing.\n * The pin references the immutable artifact; the sandboxed loader fetches\n * and integrity-checks it at render time.\n *\n * Enablement sync (AGL-424): marketplace plugins ride the same\n * `org.enabledPlugins` switchboard first-party plugins use — but ONLY for\n * workspaces that have explicitly configured the field. Install appends\n * the listing id there; uninstall removes it once no pin (org or acting\n * host) remains. Workspaces that never touched the switchboard stay\n * default-open (absent field ⇒ installs load), so pre-sync installs keep\n * working.\n */\n\n/** Keeps `org.enabledPlugins` in step with install pins (AGL-424). */\nasync function syncEnabledPlugins(\n firestore: FirebaseFirestore.Firestore,\n orgId: string | null,\n hostId: string,\n listingId: string,\n action: 'add' | 'remove-if-unpinned',\n): Promise<void> {\n if (!orgId) return\n // `enabledPlugins` is a flat mix of first-party bundle ids and marketplace\n // listing ids (AGL-777). A listing id is a Firestore doc id, so it can't\n // normally collide with a bundle name — but guard anyway: an install sync\n // must NEVER arrayUnion/arrayRemove a bundle id, which would silently\n // enable or disable a platform plugin as a side effect of a purchase.\n if (isFirstPartyPlugin(listingId)) return\n const orgRef = firestore.collection('orgs').doc(orgId)\n const configured = (await orgRef.get()).get('enabledPlugins')\n // Absent field = default-open workspace; nothing to sync.\n if (!Array.isArray(configured)) return\n if (action === 'add') {\n if (configured.includes(listingId)) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayUnion(listingId),\n })\n return\n }\n const [orgPin, hostPin] = await Promise.all([\n orgRef.collection('installs').doc(listingId).get(),\n firestore\n .collection('hosts')\n .doc(hostId)\n .collection('installs')\n .doc(listingId)\n .get(),\n ])\n if (orgPin.exists || hostPin.exists) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayRemove(listingId),\n })\n}\n/**\n * A pin that moved takes with it the declared connections it no longer\n * declares (AGL-4194): an uninstall, or a re-pin to a version without one.\n * Each such connection's grant is revoked at the provider (best effort, with\n * the endpoint the PREVIOUS pin declared, the new one having none) and the\n * connection and its record are deleted. A site that still runs the listing\n * through its other pin (its own, or the workspace's) keeps its connection.\n * Never throws: the pin itself is already written.\n */\nasync function forgetUndeclaredConnections(\n firestore: FirebaseFirestore.Firestore,\n options: {\n scope: 'org' | 'host'\n hostId: string\n orgId: string | null\n listingId: string\n previousPin: Record<string, unknown> | undefined\n },\n): Promise<void> {\n try {\n const { pinConnections, reconcileListingConnections } = await import('./connections/cleanup')\n const previous = pinConnections(options.previousPin)\n if (!previous.length) return\n const orgId = options.orgId ?? (await resolveOrgIdForHost(options.hostId)) ?? null\n const hostIds =\n options.scope === 'org' && orgId\n ? (await firestore.collection('hosts').where('orgId', '==', orgId).get()).docs.map((doc) => doc.id)\n : [options.hostId]\n await reconcileListingConnections(firestore, { hostIds, orgId, listingId: options.listingId, previous })\n } catch (error) {\n console.error('[marketplace] the connections of a moved pin were not cleaned up', error)\n }\n}\n\n/**\n * Tells the live sites a pin changed (AGL-3741, AGL-4121).\n *\n * The tenant caches each site's resolved plugins, and every page that read\n * them, under the site's plugin tag, and an install or uninstall reaches live\n * pages only through this drop — the cache behind it is held for the\n * platform's week-long backstop. An org-tier pin applies to every site in the\n * org, so it is those sites that are told.\n *\n * `plugins: 'host'` makes the tenant expire the plugin set immediately rather\n * than stale-while-revalidate, so an uninstalled bundle does not run for one\n * more visitor. The app stages each drop in its durable outbox first, so a\n * refused call is retried; the answer is `false` only when a drop neither\n * landed nor was queued, which the caller reports as a failure. Never throws:\n * the pin itself is already written.\n */\nasync function announcePinChange(\n firestore: FirebaseFirestore.Firestore,\n options: { scope: 'org' | 'host'; hostId: string; orgId: string | null; reason: string },\n): Promise<boolean> {\n try {\n const hostIds =\n options.scope === 'org' && options.orgId\n ? (\n await firestore.collection('hosts').where('orgId', '==', options.orgId).get()\n ).docs.map((doc) => doc.id)\n : [options.hostId]\n const result = await dropPluginSiteCache({\n hostIds,\n reason: options.reason,\n plugins: 'host',\n })\n return result.complete\n } catch (error) {\n console.error('[marketplace] the sites running a changed pin were not told', error)\n return false\n }\n}\n\n/** What an uninstall answers when its live-site drop was lost (AGL-4121). */\nconst PIN_DROP_LOST_ERROR =\n 'The plugin was removed, but the live site could not be refreshed. Try again to finish removing it from published pages.'\n\nexport const installPluginHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n // Optional explicit version for upgrade/downgrade; defaults to latest.\n const requestedVersion = req.body?.version\n ? String(req.body.version)\n : undefined\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n // Uninstalls (AGL-237/424). Org pins are API-managed (rules deny\n // client writes); host uninstalls also come through here so the\n // enablement sync runs. Plugin-owned data is never deleted — only the\n // pin (and the switchboard entry when no pin remains anywhere) and, the\n // one exception, a declared connection's grant and record (AGL-4194).\n if (req.body?.action === 'uninstall') {\n const orgId = await resolveOrgIdForHost(hostId)\n if (req.body?.scope === 'org' && !orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n const pinRef =\n req.body?.scope === 'org'\n ? orgId\n ? firestore.collection('orgs').doc(orgId).collection('installs').doc(listingId)\n : null\n : firestore.collection('hosts').doc(hostId).collection('installs').doc(listingId)\n // Active-install accounting (AGL-880): decrement only when a pin actually\n // existed, so a repeat/no-op uninstall never over-counts. `installCount`\n // is a cumulative all-time total and is deliberately left untouched.\n const pinSnapshot = pinRef ? await pinRef.get() : null\n const pinExisted = pinSnapshot?.exists ?? false\n // Read before the delete: the version being left is what the per-version\n // tally has to decrement, and it is gone the moment the pin is.\n const pinnedVersion = pinExisted ? pinSnapshot?.get('version') : null\n if (pinRef) await pinRef.delete()\n if (pinExisted) {\n await forgetUndeclaredConnections(firestore, {\n scope: req.body?.scope === 'org' ? 'org' : 'host',\n hostId,\n orgId,\n listingId,\n previousPin: pinSnapshot?.data(),\n })\n }\n // Announced even when no pin was found (AGL-4121): a retry after a lost\n // drop finds the pin already gone, and it is the retry that has to\n // reach the live pages. A repeat drop is a no-op.\n const told = await announcePinChange(firestore, {\n scope: req.body?.scope === 'org' ? 'org' : 'host',\n hostId,\n orgId,\n reason: `marketplace listing ${listingId} uninstalled`,\n })\n if (pinExisted) {\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: pinnedVersion,\n to: null,\n })\n await firestore\n .runTransaction(async (tx) => {\n const snapshot = await tx.get(listingRef)\n const current = Number(snapshot.get('activeInstalls') ?? 0)\n tx.update(listingRef, { activeInstalls: Math.max(0, current - 1) })\n })\n .catch(() => undefined)\n }\n await syncEnabledPlugins(\n firestore,\n orgId,\n hostId,\n listingId,\n 'remove-if-unpinned',\n )\n if (!told) return res.status(503).json({ error: PIN_DROP_LOST_ERROR })\n return res.status(200).json({ ok: true })\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs too (AGL-948) — the revocation\n // check below stops a hidden listing anyway, but only while the\n // revocation stands, and a takedown is not conditional on it.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'plugin'\n ) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // The publisher installs their own listing for free, and may install\n // an unapproved version of it to test. Org-owned now (AGL-652), so this\n // asks the publishing org's members (comparing a uid to an org id would\n // never match). Membership, not a permission (AGL-4047): it is a fact\n // about who published the listing.\n //\n // TWO facts, correlated (AGL-2484). Membership answers a question about\n // the PUBLISHER org alone; the install TARGET is a different org,\n // resolved from the host by the permission gate above. Nothing tied them\n // together, so one flag computed from org P waived three gates on an\n // install landing in org Q: review, version resolution, and payment. A\n // user who administers publisher org P and holds `installPlugins` on a\n // host in org Q could therefore push P's pending — or rejected — bundle\n // onto Q's live public site, for free.\n //\n // Same-org is a NECESSARY condition, never a sufficient one: the\n // membership check still has to pass, so this narrows the waiver and\n // cannot widen it. A listing with no `profileId` is owned by nobody and\n // waives nothing. Every install door asks this same definition now\n // (`ownsListingHere`, AGL-4047); plugins were the first to need it.\n const ownsListing = await ownsListingHere(firestore, {\n uid: decoded.uid,\n publisherOrgId: listing.profileId,\n targetOrgId: membership.orgId,\n })\n\n // Private plugins are installable ONLY by the owning org (AGL-968).\n // This is what makes private mean private; the UI merely hides them.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // Installs enforce the same review state the marketplace browses by\n // (AGL-965). `isListingBrowsable` used to be applied ONLY by the browse\n // UI, so a listing that was never reviewed, was rejected, or has since\n // been de-listed could still be installed by anyone who knew its id and\n // posted here directly — which also made \"delist\" a suggestion rather\n // than a control. The publisher keeps installing their own listing to\n // test it before review.\n if (!isListingBrowsable(listing) && !isPrivateListing(listing) && !ownsListing) {\n return res\n .status(409)\n .json({ error: 'This plugin is not available for install' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n // Installs resolve the newest APPROVED version, never `latestVersion`\n // (AGL-966). Before this, publishing v1.0.1 to a verified listing put\n // unreviewed code in front of every workspace immediately: the listing\n // kept its status, the queue never surfaced the update, and this line\n // pinned whatever had just been uploaded. A pending version is now\n // simply not offered, and the previously approved one keeps installing.\n //\n // The publisher may still install their own unapproved version — that\n // is how you test a plugin before submitting it. It reaches only their\n // own sites because `ownsListing` now REQUIRES the install to land in\n // the publishing org (AGL-2484); this sentence used to be an assertion\n // about code that did not check.\n // Read the kill switch BEFORE resolving, not after (AGL-2368).\n //\n // The order used to be: pick the newest APPROVED version, then 409 if that\n // one happens to be revoked. Revocation was never an input to the choice,\n // only a veto on it — so stopping one build stopped the whole listing.\n // v2.0.0 revoked with v1.0.0 approved and installable resolved to v2.0.0\n // and answered 409, and `requirePurchase` above has already taken the\n // buyer's money by then. A per-version revocation is deliberately NOT\n // listing-wide (`isListingWideRevocation`); that path made it so anyway.\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n\n let version = requestedVersion ?? ''\n let versionData: any\n if (version) {\n versionData = (\n await listingRef.collection('pluginVersions').doc(version).get()\n ).data()\n if (!isVersionApproved(versionData) && !ownsListing) {\n return res.status(409).json({\n error: 'That version has not passed review yet',\n reviewState: versionData?.reviewState ?? 'unknown',\n })\n }\n } else {\n const approvedSnapshot = await listingRef\n .collection('pluginVersions')\n .orderBy('publishedAt', 'desc')\n .limit(25)\n .get()\n const candidates = approvedSnapshot.docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n reviewState: doc.get('reviewState'),\n publishedAt: doc.get('publishedAt'),\n data: doc.data(),\n }))\n // The SHARED predicate (AGL-2306/2368): approved AND not revoked, which\n // is the same function `repairLatestApprovedVersion` writes the listing\n // mirror from. Two readings of \"what is on offer\" is how the badge and\n // the route came to disagree; one function is how they cannot.\n //\n // Note this also orders by version rather than by `publishedAt`, which\n // is what the mirror has always done — so the version this route hands\n // over is now the version the listing page advertised.\n const newestVersion = newestInstallableVersion(\n candidates,\n revocation,\n compareArtifactVersions,\n )\n const newest = newestVersion\n ? (candidates.find((entry) => entry.version === newestVersion) as\n | { version: string; data: any }\n | undefined)\n : undefined\n // The publisher testing their own listing falls back to latest.\n const fallback = ownsListing\n ? candidates.find(\n (entry) => entry.version === String(listing.latestVersion ?? ''),\n )\n : undefined\n const chosen = newest ?? fallback\n if (!chosen) {\n return res.status(409).json({\n error: 'This plugin has no reviewed version available to install',\n })\n }\n version = chosen.version\n versionData = (chosen as { data?: unknown }).data\n }\n if (!versionData?.sha256 || !versionData?.manifest) {\n return res.status(404).json({ error: 'Unknown plugin version' })\n }\n\n // Kill switch (AGL-43 §3.5): a revoked version can't be installed.\n // Shares the predicate with the loaders (AGL-1085) so \"installable\" and\n // \"runnable\" cannot drift apart.\n //\n // Still checked after resolution, and still load-bearing: the unpinned\n // branch can no longer choose a revoked version, but an EXPLICIT pin and\n // the publisher's `latestVersion` fallback both can, and a listing-wide\n // takedown revokes whatever either of them names.\n if (isPluginRevoked(revocation, version)) {\n return res\n .status(409)\n .json({ error: 'This plugin version has been revoked' })\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Install tier (AGL-237): org-extending plugins install once for every\n // host in the org; host-only plugins stay pinned to the host.\n const scope = req.body?.scope === 'org' ? 'org' : 'host'\n let installRef = hostRef.collection('installs').doc(listingId)\n let pinOrgId: string | null = null\n if (scope === 'org') {\n const orgId = await resolveOrgIdForHost(hostId)\n pinOrgId = orgId ?? null\n if (!orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n installRef = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('installs')\n .doc(listingId)\n }\n const existing = await installRef.get()\n const pin = {\n listingId,\n profileId: listing.profileId,\n displayName: listing.displayName,\n pluginId: versionData.manifest.id,\n version,\n sha256: versionData.sha256,\n objectPath: versionData.objectPath,\n manifest: versionData.manifest,\n // The same provenance stamp every other artifact type now carries\n // (AGL-1015), so one reader answers \"where did this come from\" for\n // pinned and copied artifacts alike. The flat pin fields stay: they\n // are what the loader reads.\n installedFrom: pinnedProvenance({\n listingId,\n listing,\n version,\n sha256: versionData.sha256,\n artifactType: 'plugin',\n }),\n ...(existing.exists ? {} : { installedAt: now }),\n updatedAt: now,\n }\n // Each field replaced WHOLE (AGL-4194). `merge: true` merged the new\n // version's manifest INTO the old one's, so a key the new version dropped\n // — its `connections`, say — survived on the pin and went on being read\n // as declared. The fields not written here are still kept.\n await installRef.set(pin, { mergeFields: Object.keys(pin) })\n if (existing.exists) {\n await forgetUndeclaredConnections(firestore, {\n scope,\n hostId,\n orgId: pinOrgId,\n listingId,\n previousPin: existing.data(),\n })\n }\n await announcePinChange(firestore, {\n scope,\n hostId,\n orgId: pinOrgId,\n reason: `marketplace listing ${listingId} installed at ${version}`,\n })\n if (!existing.exists) {\n await listingRef\n .update({\n // installCount is the cumulative all-time total; activeInstalls\n // mirrors live pins and is decremented on uninstall (AGL-880).\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n activeInstalls: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n // Per-version tally (AGL-1036). This runs on EVERY pin write, not just the\n // first: a re-pin is the common case and the one the listing-level counters\n // above are blind to — the pin already exists, so nothing there moves while\n // the install changes version underneath.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: existing.exists ? existing.get('version') : null,\n to: version,\n })\n\n // Enablement sync (AGL-424): the new install loads on next visit for\n // workspaces with a configured switchboard.\n await syncEnabledPlugins(\n firestore,\n await resolveOrgIdForHost(hostId),\n hostId,\n listingId,\n 'add',\n )\n\n // ABI heads-up (AGL-429): the pin succeeds (explicit choice), but the\n // loaders will refuse an incompatible bundle — tell the installer now.\n const manifestHostAbi = Number(versionData.manifest?.hostAbi)\n const hostAbiWarning =\n Number.isInteger(manifestHostAbi) &&\n manifestHostAbi > 0 &&\n !isCompatibleHostAbi(manifestHostAbi)\n ? `Built for host ABI ${manifestHostAbi}; this platform runs ` +\n `${PLUGIN_HOST_ABI_VERSION}. The plugin will not load until the ` +\n 'publisher ships a compatible version.'\n : undefined\n\n return res.status(200).json({\n installed: true,\n upgraded: existing.exists,\n version,\n ...(hostAbiWarning ? { hostAbiWarning } : {}),\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","resolveOrgIdForHost","firebaseAdmin","isCompatibleHostAbi","isFirstPartyPlugin","isPluginRevoked","newestInstallableVersion","PLUGIN_HOST_ABI_VERSION","dropPluginSiteCache","resolveOrgPermissions","ownsListingHere","requirePurchase","pinnedProvenance","recordVersionMove","isPublisherSecurityLocked","isListingBrowsable","isPrivateListing","isVersionApproved","listingArtifactType","compareArtifactVersions","syncEnabledPlugins","firestore","orgId","hostId","listingId","action","orgRef","collection","doc","configured","get","Array","isArray","includes","update","enabledPlugins","FieldValue","arrayUnion","orgPin","hostPin","Promise","all","exists","arrayRemove","forgetUndeclaredConnections","options","pinConnections","reconcileListingConnections","previous","previousPin","length","hostIds","scope","where","docs","map","id","error","console","announcePinChange","result","reason","plugins","complete","PIN_DROP_LOST_ERROR","installPluginHandler","req","res","method","status","json","String","body","requestedVersion","version","undefined","authorization","headers","idToken","startsWith","slice","listing","membership","versionData","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","hostRef","hostSnapshot","pinRef","pinSnapshot","pinExisted","pinnedVersion","delete","data","told","listingRef","artifactType","from","to","runTransaction","tx","snapshot","current","Number","activeInstalls","Math","max","catch","ok","listingSnapshot","deletedAt","hiddenAt","profileId","ownsListing","publisherOrgId","targetOrgId","priceUsd","unpaid","buyerUid","buyerOrgId","revocation","reviewState","approvedSnapshot","orderBy","limit","candidates","publishedAt","newestVersion","newest","find","entry","fallback","latestVersion","chosen","sha256","manifest","now","serverTimestamp","installRef","pinOrgId","existing","pin","displayName","pluginId","objectPath","installedFrom","installedAt","updatedAt","set","mergeFields","Object","keys","installCount","increment","manifestHostAbi","hostAbi","hostAbiWarning","isInteger","installed","upgraded"],"mappings":"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"}
@@ -301,7 +301,7 @@ import { compareArtifactVersions } from "../model/update-state.js";
301
301
  const tallies = reconcileFromSnapshot(snapshot.docs, listingSnapshot.data(), pins);
302
302
  const versions = visibleDocs.map((doc)=>{
303
303
  var _doc_get, _ref, _doc_get1, _ref1, _doc_get2, _ref2;
304
- var _tallies_byVersion_get, _tallies_byVersion_get1, _doc_get3, _doc_get_capabilities, _doc_get4, _doc_get_toMillis, _doc_get5;
304
+ var _tallies_byVersion_get, _tallies_byVersion_get1, _doc_get3, _doc_get_capabilities, _doc_get4, _doc_get5, _doc_get_toMillis, _doc_get6;
305
305
  return _extends({
306
306
  version: String((_doc_get = doc.get('version')) != null ? _doc_get : doc.id),
307
307
  // Per-version install counts (AGL-1036). `installCount` is every install
@@ -317,8 +317,13 @@ import { compareArtifactVersions } from "../model/update-state.js";
317
317
  hostAbi: Number(doc.get('manifest').hostAbi)
318
318
  } : {}, Array.isArray((_doc_get4 = doc.get('manifest')) == null ? void 0 : (_doc_get_capabilities = _doc_get4.capabilities) == null ? void 0 : _doc_get_capabilities.network) ? {
319
319
  network: doc.get('manifest').capabilities.network.map((entry)=>String(entry)).filter((entry)=>/^https:\/\/[^\s/]+$/.test(entry)).slice(0, 20)
320
+ } : {}, Array.isArray((_doc_get5 = doc.get('manifest')) == null ? void 0 : _doc_get5.connections) ? {
321
+ connections: doc.get('manifest').connections.map((entry)=>{
322
+ var _ref;
323
+ return String((_ref = entry == null ? void 0 : entry.label) != null ? _ref : '').slice(0, 80);
324
+ }).filter(Boolean).slice(0, 10)
320
325
  } : {}, {
321
- publishedAtMs: (_ref2 = (_doc_get5 = doc.get('publishedAt')) == null ? void 0 : (_doc_get_toMillis = _doc_get5.toMillis) == null ? void 0 : _doc_get_toMillis.call(_doc_get5)) != null ? _ref2 : null
326
+ publishedAtMs: (_ref2 = (_doc_get6 = doc.get('publishedAt')) == null ? void 0 : (_doc_get_toMillis = _doc_get6.toMillis) == null ? void 0 : _doc_get_toMillis.call(_doc_get6)) != null ? _ref2 : null
322
327
  });
323
328
  });
324
329
  // Repair `latestApprovedVersion` when it disagrees with the versions