@aglyn/plugins-marketplace 1.14.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +1 -0
  2. package/package.json +15 -14
  3. package/src/lib/components/editor-connections-provider.component.d.ts +37 -0
  4. package/src/lib/components/editor-connections-provider.component.js +161 -0
  5. package/src/lib/components/editor-connections-provider.component.js.map +1 -0
  6. package/src/lib/components/listing-connection-credentials-card.component.d.ts +33 -0
  7. package/src/lib/components/listing-connection-credentials-card.component.js +318 -0
  8. package/src/lib/components/listing-connection-credentials-card.component.js.map +1 -0
  9. package/src/lib/components/listing-detail-page.component.js +10 -2
  10. package/src/lib/components/listing-detail-page.component.js.map +1 -1
  11. package/src/lib/components/site-plugin-connections-card.component.d.ts +38 -0
  12. package/src/lib/components/site-plugin-connections-card.component.js +576 -0
  13. package/src/lib/components/site-plugin-connections-card.component.js.map +1 -0
  14. package/src/lib/declarations.console-server.d.ts +17 -0
  15. package/src/lib/declarations.console-server.js +41 -0
  16. package/src/lib/declarations.console-server.js.map +1 -0
  17. package/src/lib/declarations.js +11 -2
  18. package/src/lib/declarations.js.map +1 -1
  19. package/src/lib/declarations.server.js +68 -1
  20. package/src/lib/declarations.server.js.map +1 -1
  21. package/src/lib/hooks/use-plugin-connections-api.d.ts +89 -0
  22. package/src/lib/hooks/use-plugin-connections-api.js +132 -0
  23. package/src/lib/hooks/use-plugin-connections-api.js.map +1 -0
  24. package/src/lib/model/editor-connections.d.ts +46 -0
  25. package/src/lib/model/editor-connections.js +70 -0
  26. package/src/lib/model/editor-connections.js.map +1 -0
  27. package/src/lib/model/listing-price-floor.js +7 -4
  28. package/src/lib/model/listing-price-floor.js.map +1 -1
  29. package/src/lib/model/marketplace-publisher-permissions.d.ts +3 -1
  30. package/src/lib/model/marketplace-publisher-permissions.js +14 -0
  31. package/src/lib/model/marketplace-publisher-permissions.js.map +1 -1
  32. package/src/lib/model/marketplace-site-permissions.d.ts +9 -0
  33. package/src/lib/model/marketplace-site-permissions.js +29 -0
  34. package/src/lib/model/marketplace-site-permissions.js.map +1 -1
  35. package/src/lib/model/plugin-connection-card.d.ts +88 -0
  36. package/src/lib/model/plugin-connection-card.js +187 -0
  37. package/src/lib/model/plugin-connection-card.js.map +1 -0
  38. package/src/lib/model/plugin-connections.d.ts +172 -0
  39. package/src/lib/model/plugin-connections.js +119 -0
  40. package/src/lib/model/plugin-connections.js.map +1 -0
  41. package/src/lib/plugin.js +21 -1
  42. package/src/lib/plugin.js.map +1 -1
  43. package/src/lib/server/admin-reviews.js +18 -12
  44. package/src/lib/server/admin-reviews.js.map +1 -1
  45. package/src/lib/server/artifact-inventory.js +3 -1
  46. package/src/lib/server/artifact-inventory.js.map +1 -1
  47. package/src/lib/server/checkout.js +4 -0
  48. package/src/lib/server/checkout.js.map +1 -1
  49. package/src/lib/server/connections/cleanup.d.ts +84 -0
  50. package/src/lib/server/connections/cleanup.js +116 -0
  51. package/src/lib/server/connections/cleanup.js.map +1 -0
  52. package/src/lib/server/connections/config.d.ts +42 -0
  53. package/src/lib/server/connections/config.js +48 -0
  54. package/src/lib/server/connections/config.js.map +1 -0
  55. package/src/lib/server/connections/connection-store.d.ts +173 -0
  56. package/src/lib/server/connections/connection-store.js +331 -0
  57. package/src/lib/server/connections/connection-store.js.map +1 -0
  58. package/src/lib/server/connections/credentials-store.d.ts +71 -0
  59. package/src/lib/server/connections/credentials-store.js +78 -0
  60. package/src/lib/server/connections/credentials-store.js.map +1 -0
  61. package/src/lib/server/connections/erasure.d.ts +25 -0
  62. package/src/lib/server/connections/erasure.js +72 -0
  63. package/src/lib/server/connections/erasure.js.map +1 -0
  64. package/src/lib/server/connections/installed-connection.d.ts +64 -0
  65. package/src/lib/server/connections/installed-connection.js +92 -0
  66. package/src/lib/server/connections/installed-connection.js.map +1 -0
  67. package/src/lib/server/connections/oauth-state.d.ts +129 -0
  68. package/src/lib/server/connections/oauth-state.js +252 -0
  69. package/src/lib/server/connections/oauth-state.js.map +1 -0
  70. package/src/lib/server/connections/provider-http.d.ts +99 -0
  71. package/src/lib/server/connections/provider-http.js +253 -0
  72. package/src/lib/server/connections/provider-http.js.map +1 -0
  73. package/src/lib/server/connections/record-announce.d.ts +20 -0
  74. package/src/lib/server/connections/record-announce.js +53 -0
  75. package/src/lib/server/connections/record-announce.js.map +1 -0
  76. package/src/lib/server/connections/record-mapping.d.ts +63 -0
  77. package/src/lib/server/connections/record-mapping.js +151 -0
  78. package/src/lib/server/connections/record-mapping.js.map +1 -0
  79. package/src/lib/server/connections/record-routes.d.ts +17 -0
  80. package/src/lib/server/connections/record-routes.js +96 -0
  81. package/src/lib/server/connections/record-routes.js.map +1 -0
  82. package/src/lib/server/connections/revoke.d.ts +29 -0
  83. package/src/lib/server/connections/revoke.js +52 -0
  84. package/src/lib/server/connections/revoke.js.map +1 -0
  85. package/src/lib/server/connections/routes.d.ts +77 -0
  86. package/src/lib/server/connections/routes.js +706 -0
  87. package/src/lib/server/connections/routes.js.map +1 -0
  88. package/src/lib/server/connections/site-records-platform.d.ts +24 -0
  89. package/src/lib/server/connections/site-records-platform.js +94 -0
  90. package/src/lib/server/connections/site-records-platform.js.map +1 -0
  91. package/src/lib/server/connections/site-records.d.ts +95 -0
  92. package/src/lib/server/connections/site-records.js +221 -0
  93. package/src/lib/server/connections/site-records.js.map +1 -0
  94. package/src/lib/server/connections/sync.d.ts +110 -0
  95. package/src/lib/server/connections/sync.js +414 -0
  96. package/src/lib/server/connections/sync.js.map +1 -0
  97. package/src/lib/server/install-plugin.js +56 -6
  98. package/src/lib/server/install-plugin.js.map +1 -1
  99. package/src/lib/server/publish-plugin.js +5 -2
  100. package/src/lib/server/publish-plugin.js.map +1 -1
  101. package/src/lib/server/revenue-source.js +6 -5
  102. package/src/lib/server/revenue-source.js.map +1 -1
  103. package/src/lib/server.js +60 -0
  104. package/src/lib/server.js.map +1 -1
  105. package/src/lib/testing/memory-firestore.d.ts +34 -0
  106. package/src/lib/testing/memory-firestore.js +250 -0
  107. package/src/lib/testing/memory-firestore.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/sync.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n readPluginConnectionPath,\n type PluginConnectionDeclaration,\n type PluginConnectionSync,\n type PluginConnectionSyncRequest,\n} from '@aglyn/aglyn/app-utils/plugin-connections'\nimport type { PluginConsoleCronContext, PluginConsoleCronReport } from '@aglyn/aglyn/plugin-manager/plugin-console-crons'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport type { SecretBoxKeyring } from '@aglyn/shared-util-tools/secret-box'\nimport { PLUGIN_CONNECTIONS_FEATURE, PLUGIN_CONNECTIONS_HOST_COLLECTION } from '../../model/plugin-connections'\nimport { readPluginConnectionsKeyring } from './config'\nimport { forgetSiteConnection, pinnedConnectionState } from './cleanup'\nimport {\n connectionAccessToken,\n connectionReady,\n connectionRecordRef,\n connectionRef,\n markConnectionError,\n markConnectionSynced,\n PluginConnectionReconnectRequiredError,\n readConnection,\n type PluginConnectionRecord,\n type PluginConnectionTokenDeps,\n} from './connection-store'\nimport { announceConnectionRecordChange, connectionRecordChanged } from './record-announce'\nimport { openCredentials } from './credentials-store'\nimport { latestListingConnections } from './installed-connection'\nimport {\n pluginConnectionApiRequest,\n pluginConnectionRuntime,\n PluginConnectionProviderError,\n type PluginConnectionClient,\n} from './provider-http'\nimport {\n mapPluginConnectionRecord,\n pluginConnectionSyncRequestsRead,\n type PluginConnectionSyncAnswer,\n} from './record-mapping'\n\n/**\n * THE DECLARED SYNC, RUN BY THE PLATFORM (AGL-4194).\n *\n * A connection's manifest declares `sync`: a few templated requests, an\n * interval, and the typed record its answers map into. This runs it, in the\n * CONSOLE only — it opens sealed grants with `PLUGIN_CONNECTIONS_TOKEN_KEY`,\n * which the tenant never holds:\n *\n * - on the console's fifteen-minute `plugin-console-crons` tick, as the\n * `marketplace-connection-sync` job ({@link runPluginConnectionSyncJob}),\n * for every connection whose `nextSyncAtMs` has come, oldest first,\n * {@link PLUGIN_CONNECTION_SYNC_MAX_PER_TICK} at most per tick;\n * - on \"Sync now\" (`POST plugin-connections/sync`), for one connection.\n *\n * One sync ({@link runConnectionSync}):\n *\n * 1. under the declaration of the version the site has PINNED, never the\n * listing's newest;\n * 2. with the site's access token, refreshed under the store's lease when\n * it is near expiry, and once more on a 401 or 403 from the API;\n * 3. each request the record reads, filled from the site's choices by\n * `preparePluginConnectionRequest` (which refuses anything off the\n * declared `apiHosts`), page by page up to the declared `maxPages` and\n * the record's `maxItems`, each answer under the 2 MB ceiling and the\n * per-attempt timeout of `provider-http.ts`, and no page started after\n * the sync's own time budget;\n * 4. mapped into the record (`record-mapping.ts`: undeclared keys dropped,\n * strings cut, the whole record bounded) and written WHOLE to\n * `hosts/{hostId}/pluginConnections/record-{listingId}__{connectionId}`,\n * which no client rule opens;\n * 5. then the connection is marked synced, next due `intervalHours` on.\n *\n * Failures: a grant the provider refuses (`invalid_grant`, a 401 after a\n * fresh token) is `reconnect_required`, and the schedule stops until the\n * owner connects again. Anything else is `error` with the provider's\n * sentence, retried after a backoff that doubles from fifteen minutes up to\n * the declared interval. The last good record stays in place either way.\n *\n * Skipped quietly, with nothing written: the whole tick when the key is unset.\n * Deferred, with only the next due time moved: a site whose plan has no\n * connections, and a pinned version the kill switch revoked. Forgotten\n * (revoked and deleted, `cleanup.ts`): a connection whose site no longer runs\n * a version that declares it.\n */\n\n/** The job id: its `platformCronBeats` mark and its row on `/api/health/crons`. */\nexport const PLUGIN_CONNECTION_SYNC_JOB_ID = 'marketplace-connection-sync'\n\n/** Connections synced at most per tick; the rest wait for the next, oldest first. */\nexport const PLUGIN_CONNECTION_SYNC_MAX_PER_TICK = 25\n\n/** Connections synced at once within a tick. */\nconst CONCURRENCY = 4\n\n/** How long one sync may keep starting pages. */\nexport const PLUGIN_CONNECTION_SYNC_BUDGET_MS = 60_000\n\n/** The first retry after a failed sync; it doubles up to the declared interval. */\nexport const PLUGIN_CONNECTION_SYNC_MIN_BACKOFF_MS = 15 * 60 * 1000\n\n/** How far a deferred connection (plan, kill switch, no workspace) is moved on. */\nexport const PLUGIN_CONNECTION_SYNC_DEFER_MS = 6 * 60 * 60 * 1000\n\nconst HOUR_MS = 60 * 60 * 1000\n\nconst firestore = (): FirebaseFirestore.Firestore => firebaseAdmin.app().firestore()\n\n/** The retry delay after `failures` failed syncs in a row, capped at the interval. */\nexport function pluginConnectionSyncBackoffMs(failures: number, intervalHours: number): number {\n const cap = Math.max(PLUGIN_CONNECTION_SYNC_MIN_BACKOFF_MS, intervalHours * HOUR_MS)\n const exponent = Math.min(Math.max(0, failures - 1), 20)\n return Math.min(cap, PLUGIN_CONNECTION_SYNC_MIN_BACKOFF_MS * 2 ** exponent)\n}\n\n/** What one sync came to. */\nexport type PluginConnectionSyncOutcome =\n | 'synced'\n | 'error'\n | 'reconnect'\n | 'not-ready'\n | 'no-sync'\n | 'deferred'\n | 'forgotten'\n | 'gone'\n\nconst isNotFound = (error: unknown) =>\n Boolean(error) && ((error as { code?: unknown }).code === 5 || /NOT_FOUND/.test(String((error as Error)?.message)))\n\n/** An update to a connection that may have been disconnected meanwhile: a missing document is not an error. */\nasync function updateIfPresent(write: () => Promise<unknown>): Promise<boolean> {\n try {\n await write()\n return true\n } catch (error) {\n if (isNotFound(error)) return false\n throw error\n }\n}\n\n/**\n * One declared request, page by page: every page's body (the summary fields\n * read the first) and the items across pages, up to `maxItems`. A 401 or 403\n * refreshes the token once for the whole sync; a second one is final.\n */\nasync function fetchSyncRequest(\n connection: PluginConnectionDeclaration,\n request: PluginConnectionSyncRequest,\n input: {\n token: { current: string; refreshed: boolean; refresh: () => Promise<string> }\n choices: Readonly<Record<string, string>>\n maxItems: number\n deadlineMs: number\n },\n): Promise<PluginConnectionSyncAnswer> {\n const pages: unknown[] = []\n const items: unknown[] = []\n let cursor: string | null = null\n const maxPages = input.maxItems > 0 ? (request.pagination?.maxPages ?? 1) : 1\n for (let page = 0; page < maxPages; page += 1) {\n if (pluginConnectionRuntime.now() >= input.deadlineMs) {\n if (page === 0) throw new PluginConnectionProviderError('transient', 'The sync ran out of time. It will try again.')\n break\n }\n let body: unknown\n try {\n body = await pluginConnectionApiRequest(connection, request, { accessToken: input.token.current, choices: input.choices, cursor })\n } catch (error) {\n if (!(error instanceof PluginConnectionProviderError) || error.code !== 'auth' || input.token.refreshed) throw error\n input.token.current = await input.token.refresh()\n input.token.refreshed = true\n body = await pluginConnectionApiRequest(connection, request, { accessToken: input.token.current, choices: input.choices, cursor })\n }\n pages.push(body)\n if (input.maxItems <= 0) break\n const list = request.itemsPath ? readPluginConnectionPath(body, request.itemsPath) : undefined\n if (Array.isArray(list)) items.push(...list.slice(0, Math.max(0, input.maxItems - items.length)))\n if (items.length >= input.maxItems || !request.pagination) break\n const next = readPluginConnectionPath(body, request.pagination.cursorPath)\n if (typeof next !== 'string' || !next || next.length > 2048 || next === cursor) break\n cursor = next\n }\n return { pages, items }\n}\n\n/** The items a request feeds into the record, or 0 for one only summary fields read. */\nfunction itemsWanted(sync: PluginConnectionSync, requestId: string): number {\n return sync.record.items?.request === requestId ? sync.record.items.maxItems : 0\n}\n\nexport interface RunConnectionSyncInput {\n keyring: SecretBoxKeyring\n record: PluginConnectionRecord\n connection: PluginConnectionDeclaration\n /** The pinned version the declaration came from. */\n version: string\n client: PluginConnectionClient\n /** Start no page after this, epoch ms. */\n deadlineMs: number\n}\n\n/**\n * Runs one connection's declared sync and records the outcome on the\n * connection. Never throws for a provider's failure; a Firestore failure\n * propagates.\n */\nexport async function runConnectionSync(input: RunConnectionSyncInput): Promise<PluginConnectionSyncOutcome> {\n const { record, connection } = input\n const db = firestore()\n const ref = connectionRef(db, record.hostId, record.listingId, record.connectionId)\n const sync = connection.sync\n if (!sync) {\n // Nothing declared to run: off the schedule until a version declares one.\n await updateIfPresent(() => ref.update({ nextSyncAtMs: null, updatedAtMs: pluginConnectionRuntime.now() }))\n return 'no-sync'\n }\n if (!connectionReady(record, connection)) {\n // The choose route puts it back on the schedule once every resource is chosen.\n await updateIfPresent(() => ref.update({ nextSyncAtMs: null, updatedAtMs: pluginConnectionRuntime.now() }))\n return 'not-ready'\n }\n const choices = Object.fromEntries(Object.entries(record.choices).map(([key, choice]) => [key, choice.id]))\n const tokenDeps: PluginConnectionTokenDeps = {\n firestore: db,\n keyring: input.keyring,\n connection,\n client: input.client,\n now: pluginConnectionRuntime.now,\n sleep: pluginConnectionRuntime.sleep,\n }\n try {\n const token = {\n current: await connectionAccessToken(tokenDeps, record),\n refreshed: false,\n refresh: () => connectionAccessToken(tokenDeps, record, { force: true }),\n }\n const answers: Record<string, PluginConnectionSyncAnswer> = {}\n for (const request of pluginConnectionSyncRequestsRead(sync)) {\n answers[request.id] = await fetchSyncRequest(connection, request, {\n token,\n choices,\n maxItems: itemsWanted(sync, request.id),\n deadlineMs: input.deadlineMs,\n })\n }\n const nowMs = pluginConnectionRuntime.now()\n const doc = mapPluginConnectionRecord(sync, answers, {\n listingId: record.listingId,\n connectionId: record.connectionId,\n version: input.version,\n syncedAtMs: nowMs,\n })\n const recordRef = connectionRecordRef(db, record.hostId, record.listingId, record.connectionId)\n const before = (await recordRef.get()).data() as Record<string, unknown> | undefined\n // Whole, never merged: a key the new schema dropped must not survive.\n await recordRef.set(doc)\n const marked = await updateIfPresent(() =>\n markConnectionSynced(db, record, { nowMs, nextSyncAtMs: nowMs + sync.intervalHours * HOUR_MS }),\n )\n if (!marked) {\n // Disconnected while it ran: the record must not outlive the grant.\n await recordRef.delete()\n await announceConnectionRecordChange(record.hostId, `plugin connection ${record.listingId}/${record.connectionId} gone`)\n return 'gone'\n }\n // The site's pages show the new values (AGL-4195); an unchanged record says nothing.\n if (connectionRecordChanged(before, doc as unknown as Record<string, unknown>)) {\n await announceConnectionRecordChange(record.hostId, `plugin connection ${record.listingId}/${record.connectionId} synced`)\n }\n return 'synced'\n } catch (error) {\n const nowMs = pluginConnectionRuntime.now()\n const grantGone =\n error instanceof PluginConnectionReconnectRequiredError ||\n (error instanceof PluginConnectionProviderError && error.code === 'auth' && error.status === 401)\n if (grantGone) {\n const message =\n error instanceof PluginConnectionReconnectRequiredError\n ? error.message\n : `${connection.label} refused the connection. Connect again.`\n await updateIfPresent(() => markConnectionError(db, record, { nowMs, message, reconnect: true }))\n return 'reconnect'\n }\n if (!(error instanceof PluginConnectionProviderError)) {\n console.error('plugin-connections: a sync failed', error instanceof Error ? error.message : error)\n }\n const message =\n error instanceof PluginConnectionProviderError\n ? `${connection.label} did not answer: ${error.message}`\n : 'The sync failed. It will try again.'\n const failures = (record.syncFailures ?? 0) + 1\n await updateIfPresent(() =>\n markConnectionError(db, record, {\n nowMs,\n message,\n reconnect: false,\n nextSyncAtMs: nowMs + pluginConnectionSyncBackoffMs(failures, sync.intervalHours),\n syncFailures: failures,\n }),\n )\n return 'error'\n }\n}\n\n/** Moves a connection's next run on without touching its status. */\nasync function defer(record: PluginConnectionRecord, nowMs: number): Promise<void> {\n await updateIfPresent(() =>\n connectionRef(firestore(), record.hostId, record.listingId, record.connectionId).update({\n nextSyncAtMs: nowMs + PLUGIN_CONNECTION_SYNC_DEFER_MS,\n updatedAtMs: nowMs,\n }),\n )\n}\n\n/** The declaration to revoke an orphan with: the listing's newest version's, the pin being gone. */\nasync function orphanDeclaration(listingId: string, connectionId: string): Promise<PluginConnectionDeclaration | null> {\n try {\n const listing = await firestore().collection('marketplaceListings').doc(listingId).get()\n const declared = await latestListingConnections(firestore(), listingId, String(listing.get('latestVersion') ?? ''))\n return declared.find((one) => one.id === connectionId) ?? null\n } catch {\n return null\n }\n}\n\n/** One due connection: gates, then the sync. */\nexport async function syncDueConnection(\n keyring: SecretBoxKeyring,\n record: PluginConnectionRecord,\n deadlineMs: number,\n): Promise<PluginConnectionSyncOutcome> {\n const nowMs = pluginConnectionRuntime.now()\n const resolved = await getOrgForHost(record.hostId)\n if (!resolved) {\n const host = await firestore().collection('hosts').doc(record.hostId).get()\n if (!host.exists) {\n // The site is gone and its eraser missed this: revoke and forget.\n await forgetSiteConnection(firestore(), record, {\n orgId: record.orgId || null,\n declaration: await orphanDeclaration(record.listingId, record.connectionId),\n })\n return 'forgotten'\n }\n await defer(record, nowMs)\n return 'deferred'\n }\n if (!checkEntitlement(resolved.org as never, PLUGIN_CONNECTIONS_FEATURE)) {\n // A plan without connections keeps its grant and its last record; it is\n // not run. Sync now (plan-gated) is how an upgrade gets a fresh one at once.\n await defer(record, nowMs)\n return 'deferred'\n }\n const pinned = await pinnedConnectionState(firestore(), {\n hostId: record.hostId,\n orgId: resolved.orgId,\n listingId: record.listingId,\n connectionId: record.connectionId,\n })\n if (pinned.state === 'gone') {\n await forgetSiteConnection(firestore(), record, {\n orgId: resolved.orgId,\n declaration: await orphanDeclaration(record.listingId, record.connectionId),\n })\n return 'forgotten'\n }\n if (pinned.state === 'paused') {\n await defer(record, nowMs)\n return 'deferred'\n }\n let client: PluginConnectionClient | null = null\n try {\n client = await openCredentials(firestore(), keyring, record.listingId, record.connectionId)\n } catch (error) {\n console.error('plugin-connections: a listing’s credentials did not open', error instanceof Error ? error.message : error)\n }\n if (!client) {\n const failures = (record.syncFailures ?? 0) + 1\n await updateIfPresent(() =>\n markConnectionError(firestore(), record, {\n nowMs,\n message: 'The plugin’s publisher has not finished setting up this connection yet.',\n reconnect: false,\n nextSyncAtMs: nowMs + pluginConnectionSyncBackoffMs(failures, pinned.connection.sync?.intervalHours ?? 24),\n syncFailures: failures,\n }),\n )\n return 'error'\n }\n return runConnectionSync({\n keyring,\n record,\n connection: pinned.connection,\n version: pinned.installed.version,\n client,\n deadlineMs: Math.min(deadlineMs, nowMs + PLUGIN_CONNECTION_SYNC_BUDGET_MS),\n })\n}\n\n/**\n * The due connections, oldest first: a collection-group query over every\n * site's `pluginConnections`, which needs its `COLLECTION_GROUP` index\n * (`kind`, `status`, `nextSyncAtMs`) in `cloud/firebase-firestore.indexes.json`.\n * `error` is included: its `nextSyncAtMs` is the backoff. A\n * `reconnect_required` connection has none, so it is never due.\n */\nexport async function dueConnections(nowMs: number, limit = PLUGIN_CONNECTION_SYNC_MAX_PER_TICK): Promise<PluginConnectionRecord[]> {\n const snapshot = await firestore()\n .collectionGroup(PLUGIN_CONNECTIONS_HOST_COLLECTION)\n .where('kind', '==', 'connection')\n .where('status', 'in', ['connected', 'error'])\n .where('nextSyncAtMs', '<=', nowMs)\n .orderBy('nextSyncAtMs', 'asc')\n .limit(limit)\n .get()\n return snapshot.docs\n .map((doc) => {\n const record = readConnection(doc.data())\n // The site is the one the document sits under, whatever it says.\n const hostId = doc.ref.parent.parent?.id\n return record && hostId ? { ...record, hostId } : null\n })\n .filter((record): record is PluginConnectionRecord => Boolean(record))\n}\n\n/**\n * The `marketplace-connection-sync` console job: every due connection, a few\n * at a time, none started after the tick's deadline. Reports counts only.\n */\nexport async function runPluginConnectionSyncJob(context: PluginConsoleCronContext): Promise<PluginConsoleCronReport> {\n const keyring = readPluginConnectionsKeyring()\n if (!keyring) return { configured: false }\n const due = await dueConnections(context.nowMs)\n const counts: Record<PluginConnectionSyncOutcome | 'failed' | 'notStarted', number> = {\n synced: 0,\n error: 0,\n reconnect: 0,\n 'not-ready': 0,\n 'no-sync': 0,\n deferred: 0,\n forgotten: 0,\n gone: 0,\n failed: 0,\n notStarted: 0,\n }\n let next = 0\n const worker = async () => {\n while (next < due.length) {\n const record = due[next++]\n if (pluginConnectionRuntime.now() >= context.deadlineMs) {\n counts.notStarted += 1\n continue\n }\n try {\n counts[await syncDueConnection(keyring, record, context.deadlineMs)] += 1\n } catch (error) {\n counts.failed += 1\n console.error('plugin-connections: a due connection failed', error instanceof Error ? error.message : error)\n }\n }\n }\n await Promise.all(Array.from({ length: Math.min(CONCURRENCY, due.length) }, worker))\n return {\n configured: true,\n due: due.length,\n synced: counts.synced,\n errors: counts.error + counts.failed,\n reconnectRequired: counts.reconnect,\n deferred: counts.deferred,\n forgotten: counts.forgotten,\n notReady: counts['not-ready'] + counts['no-sync'],\n notStarted: counts.notStarted,\n }\n}\n"],"names":["checkEntitlement","readPluginConnectionPath","firebaseAdmin","getOrgForHost","PLUGIN_CONNECTIONS_FEATURE","PLUGIN_CONNECTIONS_HOST_COLLECTION","readPluginConnectionsKeyring","forgetSiteConnection","pinnedConnectionState","connectionAccessToken","connectionReady","connectionRecordRef","connectionRef","markConnectionError","markConnectionSynced","PluginConnectionReconnectRequiredError","readConnection","announceConnectionRecordChange","connectionRecordChanged","openCredentials","latestListingConnections","pluginConnectionApiRequest","pluginConnectionRuntime","PluginConnectionProviderError","mapPluginConnectionRecord","pluginConnectionSyncRequestsRead","PLUGIN_CONNECTION_SYNC_JOB_ID","PLUGIN_CONNECTION_SYNC_MAX_PER_TICK","CONCURRENCY","PLUGIN_CONNECTION_SYNC_BUDGET_MS","PLUGIN_CONNECTION_SYNC_MIN_BACKOFF_MS","PLUGIN_CONNECTION_SYNC_DEFER_MS","HOUR_MS","firestore","app","pluginConnectionSyncBackoffMs","failures","intervalHours","cap","Math","max","exponent","min","isNotFound","error","Boolean","code","test","String","message","updateIfPresent","write","fetchSyncRequest","connection","request","input","pages","items","cursor","maxPages","maxItems","pagination","page","now","deadlineMs","body","accessToken","token","current","choices","refreshed","refresh","push","list","itemsPath","undefined","Array","isArray","slice","length","next","cursorPath","itemsWanted","sync","requestId","record","runConnectionSync","db","ref","hostId","listingId","connectionId","update","nextSyncAtMs","updatedAtMs","Object","fromEntries","entries","map","key","choice","id","tokenDeps","keyring","client","sleep","force","answers","nowMs","doc","version","syncedAtMs","recordRef","before","get","data","set","marked","delete","grantGone","status","label","reconnect","console","Error","syncFailures","defer","orphanDeclaration","listing","declared","collection","find","one","syncDueConnection","resolved","host","exists","orgId","declaration","org","pinned","state","installed","dueConnections","limit","snapshot","collectionGroup","where","orderBy","docs","parent","filter","runPluginConnectionSyncJob","context","configured","due","counts","synced","deferred","forgotten","gone","failed","notStarted","worker","Promise","all","from","errors","reconnectRequired","notReady"],"mappings":"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"}
@@ -71,6 +71,34 @@ import { compareArtifactVersions } from "../model/update-state.js";
71
71
  enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayRemove(listingId)
72
72
  });
73
73
  }
74
+ /**
75
+ * A pin that moved takes with it the declared connections it no longer
76
+ * declares (AGL-4194): an uninstall, or a re-pin to a version without one.
77
+ * Each such connection's grant is revoked at the provider (best effort, with
78
+ * the endpoint the PREVIOUS pin declared, the new one having none) and the
79
+ * connection and its record are deleted. A site that still runs the listing
80
+ * through its other pin (its own, or the workspace's) keeps its connection.
81
+ * Never throws: the pin itself is already written.
82
+ */ async function forgetUndeclaredConnections(firestore, options) {
83
+ try {
84
+ var _ref, _options_orgId;
85
+ const { pinConnections, reconcileListingConnections } = await import("./connections/cleanup.js");
86
+ const previous = pinConnections(options.previousPin);
87
+ if (!previous.length) return;
88
+ const orgId = (_ref = (_options_orgId = options.orgId) != null ? _options_orgId : await resolveOrgIdForHost(options.hostId)) != null ? _ref : null;
89
+ const hostIds = options.scope === 'org' && orgId ? (await firestore.collection('hosts').where('orgId', '==', orgId).get()).docs.map((doc)=>doc.id) : [
90
+ options.hostId
91
+ ];
92
+ await reconcileListingConnections(firestore, {
93
+ hostIds,
94
+ orgId,
95
+ listingId: options.listingId,
96
+ previous
97
+ });
98
+ } catch (error) {
99
+ console.error('[marketplace] the connections of a moved pin were not cleaned up', error);
100
+ }
101
+ }
74
102
  /**
75
103
  * Tells the live sites a pin changed (AGL-3741).
76
104
  *
@@ -146,7 +174,8 @@ export const installPluginHandler = async (req, res)=>{
146
174
  // Uninstalls (AGL-237/424). Org pins are API-managed (rules deny
147
175
  // client writes); host uninstalls also come through here so the
148
176
  // enablement sync runs. Plugin-owned data is never deleted — only the
149
- // pin (and the switchboard entry when no pin remains anywhere).
177
+ // pin (and the switchboard entry when no pin remains anywhere) and, the
178
+ // one exception, a declared connection's grant and record (AGL-4194).
150
179
  if (((_req_body3 = req.body) == null ? void 0 : _req_body3.action) === 'uninstall') {
151
180
  var _ref2;
152
181
  var _req_body5, _req_body6;
@@ -167,11 +196,18 @@ export const installPluginHandler = async (req, res)=>{
167
196
  const pinnedVersion = pinExisted ? pinSnapshot == null ? void 0 : pinSnapshot.get('version') : null;
168
197
  if (pinRef) await pinRef.delete();
169
198
  if (pinExisted) {
170
- var _req_body7;
171
- await announcePinChange(firestore, {
199
+ var _req_body7, _req_body8;
200
+ await forgetUndeclaredConnections(firestore, {
172
201
  scope: ((_req_body7 = req.body) == null ? void 0 : _req_body7.scope) === 'org' ? 'org' : 'host',
173
202
  hostId,
174
203
  orgId,
204
+ listingId,
205
+ previousPin: pinSnapshot == null ? void 0 : pinSnapshot.data()
206
+ });
207
+ await announcePinChange(firestore, {
208
+ scope: ((_req_body8 = req.body) == null ? void 0 : _req_body8.scope) === 'org' ? 'org' : 'host',
209
+ hostId,
210
+ orgId,
175
211
  reason: `marketplace listing ${listingId} uninstalled`
176
212
  });
177
213
  }
@@ -376,7 +412,7 @@ export const installPluginHandler = async (req, res)=>{
376
412
  installRef = firestore.collection('orgs').doc(orgId).collection('installs').doc(listingId);
377
413
  }
378
414
  const existing = await installRef.get();
379
- await installRef.set(_extends({
415
+ const pin = _extends({
380
416
  listingId,
381
417
  profileId: listing.profileId,
382
418
  displayName: listing.displayName,
@@ -400,9 +436,23 @@ export const installPluginHandler = async (req, res)=>{
400
436
  installedAt: now
401
437
  }, {
402
438
  updatedAt: now
403
- }), {
404
- merge: true
405
439
  });
440
+ // Each field replaced WHOLE (AGL-4194). `merge: true` merged the new
441
+ // version's manifest INTO the old one's, so a key the new version dropped
442
+ // — its `connections`, say — survived on the pin and went on being read
443
+ // as declared. The fields not written here are still kept.
444
+ await installRef.set(pin, {
445
+ mergeFields: Object.keys(pin)
446
+ });
447
+ if (existing.exists) {
448
+ await forgetUndeclaredConnections(firestore, {
449
+ scope,
450
+ hostId,
451
+ orgId: pinOrgId,
452
+ listingId,
453
+ previousPin: existing.data()
454
+ });
455
+ }
406
456
  await announcePinChange(firestore, {
407
457
  scope,
408
458
  hostId,
@@ -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 * Tells the live sites a pin changed (AGL-3741).\n *\n * The tenant caches each site's resolved plugins under its\n * `tenant-data:{hostId}` tag, and an install or uninstall used to reach live\n * pages only when that entry expired — which is why it expired every minute,\n * re-reading the install pins, listings, versions and revocations of every\n * site sixty times an hour. An org-tier pin applies to every site in the org,\n * so it is those sites that are told. Best effort and never throws, like the\n * revocation fan-out it shares: the pin itself is already written.\n */\nasync function announcePinChange(\n firestore: FirebaseFirestore.Firestore,\n options: { scope: 'org' | 'host'; hostId: string; orgId: string | null; reason: string },\n): Promise<void> {\n try {\n const hostIds =\n options.scope === 'org' && options.orgId\n ? (\n await firestore.collection('hosts').where('orgId', '==', options.orgId).get()\n ).docs.map((doc) => doc.id)\n : [options.hostId]\n await dropPluginSiteCache({ hostIds, reason: options.reason })\n } catch (error) {\n console.error('[marketplace] the sites running a changed pin were not told', error)\n }\n}\n\nexport const installPluginHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n // Optional explicit version for upgrade/downgrade; defaults to latest.\n const requestedVersion = req.body?.version\n ? String(req.body.version)\n : undefined\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n // Uninstalls (AGL-237/424). Org pins are API-managed (rules deny\n // client writes); host uninstalls also come through here so the\n // enablement sync runs. Plugin-owned data is never deleted — only the\n // pin (and the switchboard entry when no pin remains anywhere).\n if (req.body?.action === 'uninstall') {\n const orgId = await resolveOrgIdForHost(hostId)\n if (req.body?.scope === 'org' && !orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n const pinRef =\n req.body?.scope === 'org'\n ? orgId\n ? firestore.collection('orgs').doc(orgId).collection('installs').doc(listingId)\n : null\n : firestore.collection('hosts').doc(hostId).collection('installs').doc(listingId)\n // Active-install accounting (AGL-880): decrement only when a pin actually\n // existed, so a repeat/no-op uninstall never over-counts. `installCount`\n // is a cumulative all-time total and is deliberately left untouched.\n const pinSnapshot = pinRef ? await pinRef.get() : null\n const pinExisted = pinSnapshot?.exists ?? false\n // Read before the delete: the version being left is what the per-version\n // tally has to decrement, and it is gone the moment the pin is.\n const pinnedVersion = pinExisted ? pinSnapshot?.get('version') : null\n if (pinRef) await pinRef.delete()\n if (pinExisted) {\n await announcePinChange(firestore, {\n scope: req.body?.scope === 'org' ? 'org' : 'host',\n hostId,\n orgId,\n reason: `marketplace listing ${listingId} uninstalled`,\n })\n }\n if (pinExisted) {\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: pinnedVersion,\n to: null,\n })\n await firestore\n .runTransaction(async (tx) => {\n const snapshot = await tx.get(listingRef)\n const current = Number(snapshot.get('activeInstalls') ?? 0)\n tx.update(listingRef, { activeInstalls: Math.max(0, current - 1) })\n })\n .catch(() => undefined)\n }\n await syncEnabledPlugins(\n firestore,\n orgId,\n hostId,\n listingId,\n 'remove-if-unpinned',\n )\n return res.status(200).json({ ok: true })\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs too (AGL-948) — the revocation\n // check below stops a hidden listing anyway, but only while the\n // revocation stands, and a takedown is not conditional on it.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'plugin'\n ) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // The publisher installs their own listing for free, and may install\n // an unapproved version of it to test. Org-owned now (AGL-652), so this\n // asks the publishing org's members (comparing a uid to an org id would\n // never match). Membership, not a permission (AGL-4047): it is a fact\n // about who published the listing.\n //\n // TWO facts, correlated (AGL-2484). Membership answers a question about\n // the PUBLISHER org alone; the install TARGET is a different org,\n // resolved from the host by the permission gate above. Nothing tied them\n // together, so one flag computed from org P waived three gates on an\n // install landing in org Q: review, version resolution, and payment. A\n // user who administers publisher org P and holds `installPlugins` on a\n // host in org Q could therefore push P's pending — or rejected — bundle\n // onto Q's live public site, for free.\n //\n // Same-org is a NECESSARY condition, never a sufficient one: the\n // membership check still has to pass, so this narrows the waiver and\n // cannot widen it. A listing with no `profileId` is owned by nobody and\n // waives nothing. Every install door asks this same definition now\n // (`ownsListingHere`, AGL-4047); plugins were the first to need it.\n const ownsListing = await ownsListingHere(firestore, {\n uid: decoded.uid,\n publisherOrgId: listing.profileId,\n targetOrgId: membership.orgId,\n })\n\n // Private plugins are installable ONLY by the owning org (AGL-968).\n // This is what makes private mean private; the UI merely hides them.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // Installs enforce the same review state the marketplace browses by\n // (AGL-965). `isListingBrowsable` used to be applied ONLY by the browse\n // UI, so a listing that was never reviewed, was rejected, or has since\n // been de-listed could still be installed by anyone who knew its id and\n // posted here directly — which also made \"delist\" a suggestion rather\n // than a control. The publisher keeps installing their own listing to\n // test it before review.\n if (!isListingBrowsable(listing) && !isPrivateListing(listing) && !ownsListing) {\n return res\n .status(409)\n .json({ error: 'This plugin is not available for install' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n // Installs resolve the newest APPROVED version, never `latestVersion`\n // (AGL-966). Before this, publishing v1.0.1 to a verified listing put\n // unreviewed code in front of every workspace immediately: the listing\n // kept its status, the queue never surfaced the update, and this line\n // pinned whatever had just been uploaded. A pending version is now\n // simply not offered, and the previously approved one keeps installing.\n //\n // The publisher may still install their own unapproved version — that\n // is how you test a plugin before submitting it. It reaches only their\n // own sites because `ownsListing` now REQUIRES the install to land in\n // the publishing org (AGL-2484); this sentence used to be an assertion\n // about code that did not check.\n // Read the kill switch BEFORE resolving, not after (AGL-2368).\n //\n // The order used to be: pick the newest APPROVED version, then 409 if that\n // one happens to be revoked. Revocation was never an input to the choice,\n // only a veto on it — so stopping one build stopped the whole listing.\n // v2.0.0 revoked with v1.0.0 approved and installable resolved to v2.0.0\n // and answered 409, and `requirePurchase` above has already taken the\n // buyer's money by then. A per-version revocation is deliberately NOT\n // listing-wide (`isListingWideRevocation`); that path made it so anyway.\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n\n let version = requestedVersion ?? ''\n let versionData: any\n if (version) {\n versionData = (\n await listingRef.collection('pluginVersions').doc(version).get()\n ).data()\n if (!isVersionApproved(versionData) && !ownsListing) {\n return res.status(409).json({\n error: 'That version has not passed review yet',\n reviewState: versionData?.reviewState ?? 'unknown',\n })\n }\n } else {\n const approvedSnapshot = await listingRef\n .collection('pluginVersions')\n .orderBy('publishedAt', 'desc')\n .limit(25)\n .get()\n const candidates = approvedSnapshot.docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n reviewState: doc.get('reviewState'),\n publishedAt: doc.get('publishedAt'),\n data: doc.data(),\n }))\n // The SHARED predicate (AGL-2306/2368): approved AND not revoked, which\n // is the same function `repairLatestApprovedVersion` writes the listing\n // mirror from. Two readings of \"what is on offer\" is how the badge and\n // the route came to disagree; one function is how they cannot.\n //\n // Note this also orders by version rather than by `publishedAt`, which\n // is what the mirror has always done — so the version this route hands\n // over is now the version the listing page advertised.\n const newestVersion = newestInstallableVersion(\n candidates,\n revocation,\n compareArtifactVersions,\n )\n const newest = newestVersion\n ? (candidates.find((entry) => entry.version === newestVersion) as\n | { version: string; data: any }\n | undefined)\n : undefined\n // The publisher testing their own listing falls back to latest.\n const fallback = ownsListing\n ? candidates.find(\n (entry) => entry.version === String(listing.latestVersion ?? ''),\n )\n : undefined\n const chosen = newest ?? fallback\n if (!chosen) {\n return res.status(409).json({\n error: 'This plugin has no reviewed version available to install',\n })\n }\n version = chosen.version\n versionData = (chosen as { data?: unknown }).data\n }\n if (!versionData?.sha256 || !versionData?.manifest) {\n return res.status(404).json({ error: 'Unknown plugin version' })\n }\n\n // Kill switch (AGL-43 §3.5): a revoked version can't be installed.\n // Shares the predicate with the loaders (AGL-1085) so \"installable\" and\n // \"runnable\" cannot drift apart.\n //\n // Still checked after resolution, and still load-bearing: the unpinned\n // branch can no longer choose a revoked version, but an EXPLICIT pin and\n // the publisher's `latestVersion` fallback both can, and a listing-wide\n // takedown revokes whatever either of them names.\n if (isPluginRevoked(revocation, version)) {\n return res\n .status(409)\n .json({ error: 'This plugin version has been revoked' })\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Install tier (AGL-237): org-extending plugins install once for every\n // host in the org; host-only plugins stay pinned to the host.\n const scope = req.body?.scope === 'org' ? 'org' : 'host'\n let installRef = hostRef.collection('installs').doc(listingId)\n let pinOrgId: string | null = null\n if (scope === 'org') {\n const orgId = await resolveOrgIdForHost(hostId)\n pinOrgId = orgId ?? null\n if (!orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n installRef = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('installs')\n .doc(listingId)\n }\n const existing = await installRef.get()\n await installRef.set(\n {\n listingId,\n profileId: listing.profileId,\n displayName: listing.displayName,\n pluginId: versionData.manifest.id,\n version,\n sha256: versionData.sha256,\n objectPath: versionData.objectPath,\n manifest: versionData.manifest,\n // The same provenance stamp every other artifact type now carries\n // (AGL-1015), so one reader answers \"where did this come from\" for\n // pinned and copied artifacts alike. The flat pin fields stay: they\n // are what the loader reads.\n installedFrom: pinnedProvenance({\n listingId,\n listing,\n version,\n sha256: versionData.sha256,\n artifactType: 'plugin',\n }),\n ...(existing.exists ? {} : { installedAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n await announcePinChange(firestore, {\n scope,\n hostId,\n orgId: pinOrgId,\n reason: `marketplace listing ${listingId} installed at ${version}`,\n })\n if (!existing.exists) {\n await listingRef\n .update({\n // installCount is the cumulative all-time total; activeInstalls\n // mirrors live pins and is decremented on uninstall (AGL-880).\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n activeInstalls: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n // Per-version tally (AGL-1036). This runs on EVERY pin write, not just the\n // first: a re-pin is the common case and the one the listing-level counters\n // above are blind to — the pin already exists, so nothing there moves while\n // the install changes version underneath.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: existing.exists ? existing.get('version') : null,\n to: version,\n })\n\n // Enablement sync (AGL-424): the new install loads on next visit for\n // workspaces with a configured switchboard.\n await syncEnabledPlugins(\n firestore,\n await resolveOrgIdForHost(hostId),\n hostId,\n listingId,\n 'add',\n )\n\n // ABI heads-up (AGL-429): the pin succeeds (explicit choice), but the\n // loaders will refuse an incompatible bundle — tell the installer now.\n const manifestHostAbi = Number(versionData.manifest?.hostAbi)\n const hostAbiWarning =\n Number.isInteger(manifestHostAbi) &&\n manifestHostAbi > 0 &&\n !isCompatibleHostAbi(manifestHostAbi)\n ? `Built for host ABI ${manifestHostAbi}; this platform runs ` +\n `${PLUGIN_HOST_ABI_VERSION}. The plugin will not load until the ` +\n 'publisher ships a compatible version.'\n : undefined\n\n return res.status(200).json({\n installed: true,\n upgraded: existing.exists,\n version,\n ...(hostAbiWarning ? { hostAbiWarning } : {}),\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","resolveOrgIdForHost","firebaseAdmin","isCompatibleHostAbi","isFirstPartyPlugin","isPluginRevoked","newestInstallableVersion","PLUGIN_HOST_ABI_VERSION","dropPluginSiteCache","resolveOrgPermissions","ownsListingHere","requirePurchase","pinnedProvenance","recordVersionMove","isPublisherSecurityLocked","isListingBrowsable","isPrivateListing","isVersionApproved","listingArtifactType","compareArtifactVersions","syncEnabledPlugins","firestore","orgId","hostId","listingId","action","orgRef","collection","doc","configured","get","Array","isArray","includes","update","enabledPlugins","FieldValue","arrayUnion","orgPin","hostPin","Promise","all","exists","arrayRemove","announcePinChange","options","hostIds","scope","where","docs","map","id","reason","error","console","installPluginHandler","req","res","method","status","json","String","body","requestedVersion","version","undefined","authorization","headers","idToken","startsWith","slice","length","listing","membership","versionData","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","hostRef","hostSnapshot","pinRef","pinSnapshot","pinExisted","pinnedVersion","delete","listingRef","artifactType","from","to","runTransaction","tx","snapshot","current","Number","activeInstalls","Math","max","catch","ok","listingSnapshot","data","deletedAt","hiddenAt","profileId","ownsListing","publisherOrgId","targetOrgId","priceUsd","unpaid","buyerUid","buyerOrgId","revocation","reviewState","approvedSnapshot","orderBy","limit","candidates","publishedAt","newestVersion","newest","find","entry","fallback","latestVersion","chosen","sha256","manifest","now","serverTimestamp","installRef","pinOrgId","existing","set","displayName","pluginId","objectPath","installedFrom","installedAt","updatedAt","merge","installCount","increment","manifestHostAbi","hostAbi","hostAbiWarning","isInteger","installed","upgraded"],"mappings":"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"}
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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"}
@@ -449,7 +449,7 @@ export const publishPluginHandler = async (req, res)=>{
449
449
  });
450
450
  }
451
451
  try {
452
- var _ref6, _membership_orgId, _ref7, _ref8, _manifest_contributes, _ref9;
452
+ var _ref6, _membership_orgId, _ref7, _ref8, _manifest_contributes, _manifest_connections, _ref9;
453
453
  var _manifest_capabilities, _contentVerdict_content, _contentVerdict_content1, _contentVerdict_content2, _req_body4, _contentVerdict_content3;
454
454
  // Act on the org the client is in (AGL-868), not a guessed first-org — a
455
455
  // member can belong to several, and the plugin publishes under one of them.
@@ -531,7 +531,10 @@ export const publishPluginHandler = async (req, res)=>{
531
531
  maxBytes: MAX_PLUGIN_BUNDLE_BYTES,
532
532
  declaredNetwork: (_ref8 = (_manifest_capabilities = manifest.capabilities) == null ? void 0 : _manifest_capabilities.network) != null ? _ref8 : [],
533
533
  declaredContributions: (_manifest_contributes = manifest.contributes) != null ? _manifest_contributes : null,
534
- requireContributions: true
534
+ requireContributions: true,
535
+ // Validated already by `validatePluginManifest`; the checker compares
536
+ // the hosts with the bundle's calls (AGL-4193).
537
+ declaredConnections: (_manifest_connections = manifest.connections) != null ? _manifest_connections : null
535
538
  });
536
539
  if (!verification.ok) {
537
540
  return res.status(422).json({