@aglyn/plugins-marketplace 1.15.2 → 1.16.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.
- package/package.json +15 -15
- package/src/lib/components/listing-content.component.js +14 -12
- package/src/lib/components/listing-content.component.js.map +1 -1
- package/src/lib/components/site-plugin-connections-card.component.js +4 -3
- package/src/lib/components/site-plugin-connections-card.component.js.map +1 -1
- package/src/lib/model/editor-connections.d.ts +1 -1
- package/src/lib/model/editor-connections.js +13 -2
- package/src/lib/model/editor-connections.js.map +1 -1
- package/src/lib/model/marketplace.d.ts +11 -0
- package/src/lib/model/marketplace.js +12 -2
- package/src/lib/model/marketplace.js.map +1 -1
- package/src/lib/model/plugin-connection-card.d.ts +6 -0
- package/src/lib/model/plugin-connection-card.js +8 -0
- package/src/lib/model/plugin-connection-card.js.map +1 -1
- package/src/lib/model/plugin-connections.d.ts +14 -0
- package/src/lib/model/plugin-connections.js +18 -1
- package/src/lib/model/plugin-connections.js.map +1 -1
- package/src/lib/server/admin-reviews.js +9 -0
- package/src/lib/server/admin-reviews.js.map +1 -1
- package/src/lib/server/admin-trust.js +11 -0
- package/src/lib/server/admin-trust.js.map +1 -1
- package/src/lib/server/connections/site-records-platform.js +8 -5
- package/src/lib/server/connections/site-records-platform.js.map +1 -1
- package/src/lib/server/install-plugin.js +34 -19
- package/src/lib/server/install-plugin.js.map +1 -1
- package/src/lib/server/listing-versions.js +7 -2
- package/src/lib/server/listing-versions.js.map +1 -1
- package/src/lib/server/revoke-cache-drop.d.ts +29 -34
- package/src/lib/server/revoke-cache-drop.js +43 -49
- 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":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,sBAAqB;AAC1D,SAASC,eAAe,QAAQ,mDAAkD;AAClF,SACEC,uBAAuB,EACvBC,aAAa,EACbC,sBAAsB,QACjB,2BAA0B;AACjC,SAASC,sBAAsB,QAAQ,mDAAkD;AACzF,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,aAAa,QAAQ,oDAAmD;AACjF,SAASC,oBAAoB,QAAQ,yBAAqB;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,eAAeC,QAAQC,OAAgB;QAMfC;IALtB,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAEF,SAASG,UAAU,EAAE,GAAG,MAAMd,qBAAqBU;IACzE,MAAMC,UAAUG;IAChB,IAAIF,WAAW,QAAQ;QACrB,OAAOG,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAqB,GAAG;YAAEC,QAAQ;QAAI;IACtE;IACA,MAAMC,iBAAgBR,yBAAAA,QAAQQ,aAAa,YAArBR,yBAAyB;IAC/C,MAAMS,UAAUD,cAAcE,UAAU,CAAC,aACrCF,cAAcG,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS,OAAOL,SAASC,IAAI,CAAC;QAAEC,OAAO;IAAkB,GAAG;QAAEC,QAAQ;IAAI;IAE/E,IAAI;YAQSO;QAPX,MAAMA,UAAU,MAAMtB,cAAcuB,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACR;QAC/D,IAAI,CAACK,QAAQI,cAAc,IAAI,CAACzB,uBAAuBqB,UAAU;YAC/D,OAAOvB;QACT;QACA,IAAI,CAACuB,OAAO,CAAC,QAAQ,EAAE;YACrB,OAAOV,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAa,GAAG;gBAAEC,QAAQ;YAAI;QAC9D;QACA,IAAIY,QAAOL,qBAAAA,OAAO,CAAC,YAAY,YAApBA,qBAAwB,eAAe,SAAS;YACzD,OAAOV,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAgC,GAAG;gBAAEC,QAAQ;YAAI;QACjF;QAEA,MAAMa,YAAYD,eAAOjB,wBAAAA,KAAMkB,SAAS,mBAAI;QAC5C,MAAMC,UAAUF,gBAAOjB,wBAAAA,KAAMmB,OAAO,oBAAI;QACxC,MAAMC,SAASpB,CAAAA,wBAAAA,KAAMoB,MAAM,MAAK,WAAW,WAAW;QACtD,IAAI,CAACF,aAAa,CAACC,SAAS;YAC1B,OAAOjB,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAA+B,GAAG;gBAAEC,QAAQ;YAAI;QAChF;QAEA,MAAMgB,aAAa/B,cAChBuB,GAAG,GACHS,SAAS,GACTC,UAAU,CAAC,uBACXC,GAAG,CAACN,WACJK,UAAU,CAAC,kBACXC,GAAG,CAACL;QACP,MAAMM,WAAW,MAAMJ,WAAWK,GAAG;QACrC,MAAMC,SAASF,SAASC,GAAG,CAAC;QAC5B,oDAAoD;QACpD,MAAME,gBAAgBC,QAAQJ,SAASC,GAAG,CAAC;QAC3C,IAAI,OAAOC,WAAW,YAAY,CAACA,QAAQ;YACzC,OAAOzB,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAyB,GAAG;gBAAEC,QAAQ;YAAI;QAC1E;QAEA,kEAAkE;QAClE,uEAAuE;QACvE,wEAAwE;QACxE,+CAA+C;QAC/C,IAAIe,WAAW,WAAWK,SAASC,GAAG,CAAC,mBAAmB,YAAY;gBAKnDD;YAJjB,OAAOvB,SAASC,IAAI,CAClB;gBACEC,OACE;gBACF0B,WAAW,GAAEL,gBAAAA,SAASC,GAAG,CAAC,0BAAbD,gBAA+B;YAC9C,GACA;gBAAEpB,QAAQ;YAAI;QAElB;QAEA,IAAIe,WAAW,SAAS;YACtB;;;;;OAKC,GACD,MAAM,EAAEW,MAAM,EAAEC,SAAS,EAAEC,MAAM,EAAE,GAAG,MAAM7C,gBAAgBuC;YAC5D,IAAI,CAACI,UAAU,CAACC,WAAW;gBACzB,OAAO9B,SAASC,IAAI,CAClB;oBACEC,KAAK,EACH6B,iBAAAA,SACA;gBACJ,GACA;oBAAE5B,QAAQ;gBAAI;YAElB;YACA,MAAMgB,WAAWa,GAAG,CAClB;gBACEC,OAAO;gBACPH;gBACAI,gBAAgBxB,QAAQyB,GAAG;gBAC3BC,gBAAgB7C,WAAW8C,eAAe;YAC5C,GACA;gBAAEC,OAAO;YAAK;QAElB,OAAO;YACL,MAAMnB,WAAWa,GAAG,CAClB;gBACEC,OAAO1C,WAAWgD,MAAM;gBACxBT,WAAWvC,WAAWgD,MAAM;gBAC5BL,gBAAgB3C,WAAWgD,MAAM;gBACjCH,gBAAgB7C,WAAWgD,MAAM;YACnC,GACA;gBAAED,OAAO;YAAK;QAElB;QAEA,MAAM9C,cAAcJ,cAAcuB,GAAG,GAAGS,SAAS,IAAI;YACnDoB,UAAU9B,QAAQyB,GAAG;YACrBjB,QAAQ,CAAC,cAAc,EAAEA,QAAQ;YACjCuB,QAAQ,CAAC,oBAAoB,EAAEzB,UAAU,gBAAgB,EAAEC,SAAS;YACpEyB,OAAOxB,WAAW,UAAU;gBAAEe,OAAO;YAAQ,IAAI;gBAAEA,OAAO;YAAK;YAC/DU,IAAIpD,WAAW8C,eAAe;QAChC;QAEA,wEAAwE;QACxE,yEAAyE;QACzE,2EAA2E;QAC3E,yEAAyE;QACzE,oCAAoC;QACpC,IAAInB,WAAW,YAAYQ,eAAe;YACxC,MAAMjC,qBAAqBL,cAAcuB,GAAG,GAAGS,SAAS,IAAIJ;QAC9D;QAEA,OAAOhB,SAASC,IAAI,CAAC;YAAE2C,IAAI;YAAM1B;YAAQF;YAAWC;QAAQ,GAAG;YAAEd,QAAQ;QAAI;IAC/E,EAAE,OAAOD,OAAO;QACd,2DAA2D;QAC3D,uEAAuE;QACvE,MAAM2C,kBAAkBvD,uBAAuBY;QAC/C,IAAI2C,iBAAiB,OAAOA;QAC5BC,QAAQ5C,KAAK,CAACA;QACd,OAAOF,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAuB,GAAG;YAAEC,QAAQ;QAAI;IACxE;AACF;AAEA,SAAST,WAAWqD,qBAAqB,GAAE"}
|
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
106
|
-
*
|
|
107
|
-
* pages only
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
|
208
|
+
var _req_body8;
|
|
200
209
|
await forgetUndeclaredConnections(firestore, {
|
|
201
|
-
scope: ((
|
|
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,
|
|
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 = (
|
|
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
|