@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.
- package/README.md +1 -0
- package/package.json +15 -14
- package/src/lib/components/editor-connections-provider.component.d.ts +37 -0
- package/src/lib/components/editor-connections-provider.component.js +161 -0
- package/src/lib/components/editor-connections-provider.component.js.map +1 -0
- package/src/lib/components/listing-connection-credentials-card.component.d.ts +33 -0
- package/src/lib/components/listing-connection-credentials-card.component.js +318 -0
- package/src/lib/components/listing-connection-credentials-card.component.js.map +1 -0
- package/src/lib/components/listing-detail-page.component.js +10 -2
- package/src/lib/components/listing-detail-page.component.js.map +1 -1
- package/src/lib/components/site-plugin-connections-card.component.d.ts +38 -0
- package/src/lib/components/site-plugin-connections-card.component.js +576 -0
- package/src/lib/components/site-plugin-connections-card.component.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +17 -0
- package/src/lib/declarations.console-server.js +41 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.js +11 -2
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +68 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-plugin-connections-api.d.ts +89 -0
- package/src/lib/hooks/use-plugin-connections-api.js +132 -0
- package/src/lib/hooks/use-plugin-connections-api.js.map +1 -0
- package/src/lib/model/editor-connections.d.ts +46 -0
- package/src/lib/model/editor-connections.js +70 -0
- package/src/lib/model/editor-connections.js.map +1 -0
- package/src/lib/model/listing-price-floor.js +7 -4
- package/src/lib/model/listing-price-floor.js.map +1 -1
- package/src/lib/model/marketplace-publisher-permissions.d.ts +3 -1
- package/src/lib/model/marketplace-publisher-permissions.js +14 -0
- package/src/lib/model/marketplace-publisher-permissions.js.map +1 -1
- package/src/lib/model/marketplace-site-permissions.d.ts +9 -0
- package/src/lib/model/marketplace-site-permissions.js +29 -0
- package/src/lib/model/marketplace-site-permissions.js.map +1 -1
- package/src/lib/model/plugin-connection-card.d.ts +88 -0
- package/src/lib/model/plugin-connection-card.js +187 -0
- package/src/lib/model/plugin-connection-card.js.map +1 -0
- package/src/lib/model/plugin-connections.d.ts +172 -0
- package/src/lib/model/plugin-connections.js +119 -0
- package/src/lib/model/plugin-connections.js.map +1 -0
- package/src/lib/plugin.js +21 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/admin-reviews.js +18 -12
- package/src/lib/server/admin-reviews.js.map +1 -1
- package/src/lib/server/artifact-inventory.js +3 -1
- package/src/lib/server/artifact-inventory.js.map +1 -1
- package/src/lib/server/checkout.js +4 -0
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/connections/cleanup.d.ts +84 -0
- package/src/lib/server/connections/cleanup.js +116 -0
- package/src/lib/server/connections/cleanup.js.map +1 -0
- package/src/lib/server/connections/config.d.ts +42 -0
- package/src/lib/server/connections/config.js +48 -0
- package/src/lib/server/connections/config.js.map +1 -0
- package/src/lib/server/connections/connection-store.d.ts +173 -0
- package/src/lib/server/connections/connection-store.js +331 -0
- package/src/lib/server/connections/connection-store.js.map +1 -0
- package/src/lib/server/connections/credentials-store.d.ts +71 -0
- package/src/lib/server/connections/credentials-store.js +78 -0
- package/src/lib/server/connections/credentials-store.js.map +1 -0
- package/src/lib/server/connections/erasure.d.ts +25 -0
- package/src/lib/server/connections/erasure.js +72 -0
- package/src/lib/server/connections/erasure.js.map +1 -0
- package/src/lib/server/connections/installed-connection.d.ts +64 -0
- package/src/lib/server/connections/installed-connection.js +92 -0
- package/src/lib/server/connections/installed-connection.js.map +1 -0
- package/src/lib/server/connections/oauth-state.d.ts +129 -0
- package/src/lib/server/connections/oauth-state.js +252 -0
- package/src/lib/server/connections/oauth-state.js.map +1 -0
- package/src/lib/server/connections/provider-http.d.ts +99 -0
- package/src/lib/server/connections/provider-http.js +253 -0
- package/src/lib/server/connections/provider-http.js.map +1 -0
- package/src/lib/server/connections/record-announce.d.ts +20 -0
- package/src/lib/server/connections/record-announce.js +53 -0
- package/src/lib/server/connections/record-announce.js.map +1 -0
- package/src/lib/server/connections/record-mapping.d.ts +63 -0
- package/src/lib/server/connections/record-mapping.js +151 -0
- package/src/lib/server/connections/record-mapping.js.map +1 -0
- package/src/lib/server/connections/record-routes.d.ts +17 -0
- package/src/lib/server/connections/record-routes.js +96 -0
- package/src/lib/server/connections/record-routes.js.map +1 -0
- package/src/lib/server/connections/revoke.d.ts +29 -0
- package/src/lib/server/connections/revoke.js +52 -0
- package/src/lib/server/connections/revoke.js.map +1 -0
- package/src/lib/server/connections/routes.d.ts +77 -0
- package/src/lib/server/connections/routes.js +706 -0
- package/src/lib/server/connections/routes.js.map +1 -0
- package/src/lib/server/connections/site-records-platform.d.ts +24 -0
- package/src/lib/server/connections/site-records-platform.js +94 -0
- package/src/lib/server/connections/site-records-platform.js.map +1 -0
- package/src/lib/server/connections/site-records.d.ts +95 -0
- package/src/lib/server/connections/site-records.js +221 -0
- package/src/lib/server/connections/site-records.js.map +1 -0
- package/src/lib/server/connections/sync.d.ts +110 -0
- package/src/lib/server/connections/sync.js +414 -0
- package/src/lib/server/connections/sync.js.map +1 -0
- package/src/lib/server/install-plugin.js +56 -6
- package/src/lib/server/install-plugin.js.map +1 -1
- package/src/lib/server/publish-plugin.js +5 -2
- package/src/lib/server/publish-plugin.js.map +1 -1
- package/src/lib/server/revenue-source.js +6 -5
- package/src/lib/server/revenue-source.js.map +1 -1
- package/src/lib/server.js +60 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +34 -0
- package/src/lib/testing/memory-firestore.js +250 -0
- package/src/lib/testing/memory-firestore.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/routes.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n isPluginConnectionChoiceId,\n pluginConnectionAuthorizeUrl,\n type PluginConnectionDeclaration,\n} from '@aglyn/aglyn/app-utils/plugin-connections'\nimport type { PluginApiRequestSubject } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost, logHostActivity } from '@aglyn/tenant-data-admin'\nimport { isEmailVerified, isImpersonationSession } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { SecretBoxKeyring } from '@aglyn/shared-util-tools/secret-box'\nimport {\n PLUGIN_CONNECTIONS_CREDENTIALS_PERMISSION,\n PLUGIN_CONNECTIONS_FEATURE,\n PLUGIN_CONNECTIONS_PLAN_REFUSAL,\n PLUGIN_CONNECTIONS_SITE_PERMISSION,\n PLUGIN_CONNECTIONS_SITE_ROLE_PRESET,\n pluginConnectionHosts,\n pluginConnectionsPermissionRefusal,\n type PluginConnectionCredentialsStatus,\n type PluginConnectionStatusEntry,\n} from '../../model/plugin-connections'\nimport { publisherPermissionVerdict } from '../publisher-permissions'\nimport { isHostId, isListingId, readPluginConnectionsKeyring } from './config'\nimport { announceConnectionRecordChange } from './record-announce'\nimport {\n connectionAccessToken,\n connectionReady,\n connectionRecordRef,\n connectionRef,\n getConnection,\n listHostConnections,\n newConnectionRecord,\n PluginConnectionReconnectRequiredError,\n toConnectionView,\n type PluginConnectionRecord,\n} from './connection-store'\nimport { clearCredentials, credentialsStatus, openCredentials, saveCredentials, sealCredentials } from './credentials-store'\nimport {\n installedConnection,\n installedConnectionsForHost,\n latestListingConnections,\n} from './installed-connection'\nimport {\n connectRedirectUri,\n consoleOriginFor,\n consolePath,\n consumeConnectState,\n mintConnectState,\n readConnectState,\n recordConnectState,\n type ConnectStateClaims,\n} from './oauth-state'\nimport {\n exchangePluginConnectionCode,\n listPluginConnectionResource,\n pluginConnectionRuntime,\n PluginConnectionProviderError,\n type PluginConnectionClient,\n} from './provider-http'\nimport { revokeRecord } from './revoke'\nimport { runConnectionSync } from './sync'\n\n/**\n * THE DECLARED CONNECTIONS' ROUTES (AGL-4193), served by the console for every\n * marketplace plugin that declares a connection:\n *\n * GET|POST plugin-connections/connect/start the provider's consent address (site key, plan)\n * GET plugin-connections/connect/callback the provider's redirect back (signed state, no bearer)\n * GET plugin-connections/resources what the grant reaches, to pick (site key, plan)\n * POST plugin-connections/choose the resource the site reads (site key, plan)\n * POST plugin-connections/disconnect revoke and forget the grant (site key, any plan)\n * GET plugin-connections/status each declared connection, no token (site key)\n * POST plugin-connections/sync run the declared sync once (\"Sync now\") (site key, plan)\n * GET|POST plugin-connections/credentials the listing's OAuth app, write-only (publisher key)\n *\n * The site key is \"Manage a site's plugin connections\"\n * (`marketplace.siteConnections`) and the publisher key \"Set a listing's\n * connection credentials\" (`marketplace.connectionCredentials`); a refusal\n * names the permission, never a role. The callback path is registered as a\n * redirect URI at providers and must never move.\n *\n * Without `PLUGIN_CONNECTIONS_TOKEN_KEY` every route that would seal or open a\n * secret answers 503, and status says `configured: false`.\n */\n\nconst NO_STORE = { 'Cache-Control': 'no-store' }\nconst MAX_CODE_CHARS = 4096\nconst CLIENT_ID_PATTERN = /^[\\x21-\\x7e]{1,512}$/\nconst CLIENT_SECRET_PATTERN = /^[\\x21-\\x7e]{1,1024}$/\nconst NOT_CONFIGURED = 'Plugin connections are not set up on this deployment yet.'\n\nconst firestore = (): FirebaseFirestore.Firestore => firebaseAdmin.app().firestore()\n\nexport function routeJson(body: unknown, status = 200): Response {\n return Response.json(body, { status, headers: NO_STORE })\n}\n\nexport function routeError(status: number, error: string, extra: Record<string, unknown> = {}): Response {\n return Response.json({ error, ...extra }, { status, headers: NO_STORE })\n}\n\nconst methodNotAllowed = (allow: string) =>\n new Response(JSON.stringify({ error: 'Method not allowed' }), {\n status: 405,\n headers: { 'Content-Type': 'application/json', Allow: allow, ...NO_STORE },\n })\n\n/** The request's parameters: the query for a GET, the JSON body (over the query) for a POST. */\nasync function readParams(request: Request): Promise<Record<string, unknown>> {\n const query = Object.fromEntries(new URL(request.url).searchParams.entries())\n if (request.method === 'GET' || request.method === 'HEAD') return query\n const body = await request.json().catch(() => null)\n return body && typeof body === 'object' && !Array.isArray(body) ? { ...query, ...(body as Record<string, unknown>) } : query\n}\n\n/** The signed-in member, or the refusal. */\nasync function signedIn(request: Request): Promise<{ uid: string; staff: boolean } | Response> {\n const authorization = request.headers.get('authorization') ?? ''\n if (!authorization.startsWith('Bearer ')) return routeError(401, 'Unauthenticated')\n let decoded\n try {\n decoded = await firebaseAdmin.app().auth().verifyIdToken(authorization.slice('Bearer '.length))\n } catch (error) {\n // Only a token the verifier refused is the caller's fault; a certificate\n // fetch that failed is ours, and must not read as a bad sign-in.\n if (isRefusedIdToken(error)) return routeError(401, 'Unauthenticated')\n console.error('plugin-connections: the sign-in could not be checked', error)\n return routeError(503, 'The sign-in could not be checked. Try again.')\n }\n if (!isEmailVerified(decoded) && !isImpersonationSession(decoded)) {\n return routeError(403, 'Verify your email address first')\n }\n return { uid: decoded.uid, staff: decoded['staff'] === true }\n}\n\n/** Whether `uid` holds \"Manage a site's plugin connections\" on the site. */\nexport function holdsSiteConnectionsKey(host: Record<string, unknown>, uid: string): boolean {\n return holdsStampedSiteKey(host, uid, PLUGIN_CONNECTIONS_SITE_PERMISSION, PLUGIN_CONNECTIONS_SITE_ROLE_PRESET)\n}\n\nexport interface SiteActor {\n uid: string\n hostId: string\n orgId: string\n org: Record<string, unknown>\n host: Record<string, unknown>\n planAllows: boolean\n}\n\n/**\n * THE GATE EVERY SITE ROUTE CLIMBS. The console's dispatcher has already\n * refused a site with the marketplace switched off or unreleased, a lockdown\n * and a caller over the write limit. What it cannot know is the member, the\n * key and the plan:\n *\n * 401 no bearer token, or one the verifier refused\n * 403 an unverified address (an impersonation session is exempt)\n * 400 no site named\n * 404 no such site\n * 403 without \"Manage a site's plugin connections\" on the site — held by\n * the member themselves: the grant is the business's own account at\n * the provider, so staff support does not connect one\n * 403 a plan without connections, when the route asks for it\n */\nexport async function siteConnectionsGate(\n request: Request,\n params: Record<string, unknown>,\n options: { requirePlan: boolean },\n): Promise<SiteActor | Response> {\n const caller = await signedIn(request)\n if (caller instanceof Response) return caller\n const hostId = params['hostId']\n if (!isHostId(hostId)) return routeError(400, 'Missing hostId')\n const [hostSnapshot, resolved] = await Promise.all([firestore().collection('hosts').doc(hostId).get(), getOrgForHost(hostId)])\n if (!hostSnapshot.exists || !resolved) return routeError(404, 'Site not found')\n const host = hostSnapshot.data() ?? {}\n if (!holdsSiteConnectionsKey(host, caller.uid)) {\n return routeError(403, pluginConnectionsPermissionRefusal(PLUGIN_CONNECTIONS_SITE_PERMISSION), {\n reason: 'permission',\n permission: PLUGIN_CONNECTIONS_SITE_PERMISSION,\n })\n }\n const planAllows = checkEntitlement(resolved.org as never, PLUGIN_CONNECTIONS_FEATURE)\n if (options.requirePlan && !planAllows) {\n return routeError(403, PLUGIN_CONNECTIONS_PLAN_REFUSAL, { reason: 'plan', feature: PLUGIN_CONNECTIONS_FEATURE })\n }\n return { uid: caller.uid, hostId, orgId: resolved.orgId, org: resolved.org as Record<string, unknown>, host, planAllows }\n}\n\n/** The keyring, or the 503 that says connections are not set up. */\nfunction keyringOr503(): SecretBoxKeyring | Response {\n return readPluginConnectionsKeyring() ?? routeError(503, NOT_CONFIGURED)\n}\n\n/** The declared connection a request names on the actor's site, with the publisher's app, or the refusal. */\nasync function resolveConnection(\n actor: SiteActor,\n params: Record<string, unknown>,\n keyring: SecretBoxKeyring,\n): Promise<\n | Response\n | {\n listingId: string\n displayName: string\n /** The pinned version the declaration is read from. */\n version: string\n connection: PluginConnectionDeclaration\n client: PluginConnectionClient\n }\n> {\n const listingId = params['listingId']\n const connectionId = params['connectionId']\n if (!isListingId(listingId) || typeof connectionId !== 'string' || !connectionId) {\n return routeError(400, 'Missing listingId or connectionId')\n }\n const found = await installedConnection(firestore(), { hostId: actor.hostId, orgId: actor.orgId, listingId, connectionId })\n if (!found) return routeError(404, 'This site runs no plugin that declares that connection.')\n let client: PluginConnectionClient | null\n try {\n client = await openCredentials(firestore(), keyring, listingId, connectionId)\n } catch (error) {\n console.error('plugin-connections: a listing’s credentials did not open', error)\n client = null\n }\n if (!client) {\n return routeError(409, 'The plugin’s publisher has not finished setting up this connection yet.', { reason: 'credentials' })\n }\n return {\n listingId,\n displayName: found.installed.displayName,\n version: found.installed.version,\n connection: found.connection,\n client,\n }\n}\n\nconst activityTarget = (connection: PluginConnectionDeclaration, listingId: string) =>\n ({ type: 'marketplace:plugin-connection', id: `${listingId}/${connection.id}`, name: connection.label }) as const\n\n/* ------------------------------------------------------------------ */\n/* Connect */\n/* ------------------------------------------------------------------ */\n\nexport async function connectStartRoute(request: Request): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'POST') return methodNotAllowed('GET, POST')\n const params = await readParams(request)\n const actor = await siteConnectionsGate(request, params, { requirePlan: true })\n if (actor instanceof Response) return actor\n const keyring = keyringOr503()\n if (keyring instanceof Response) return keyring\n const resolved = await resolveConnection(actor, params, keyring)\n if (resolved instanceof Response) return resolved\n const origin = consoleOriginFor(request.url)\n const redirectUri = connectRedirectUri(request.url)\n if (!origin || !redirectUri) return routeError(503, 'This console has no address to come back to.')\n const returnTo = consolePath(params['returnTo'], origin)\n if (!returnTo) return routeError(400, 'Connect from the console.')\n const nowMs = pluginConnectionRuntime.now()\n let minted\n try {\n minted = mintConnectState({\n hostId: actor.hostId,\n orgId: actor.orgId,\n uid: actor.uid,\n listingId: resolved.listingId,\n connectionId: resolved.connection.id,\n returnTo,\n nowMs,\n })\n } catch {\n return routeError(503, NOT_CONFIGURED)\n }\n await recordConnectState(firestore(), { claims: minted.claims, redirectUri, nowMs })\n return routeJson({\n authorizeUrl: pluginConnectionAuthorizeUrl(resolved.connection, {\n clientId: resolved.client.clientId,\n redirectUri,\n state: minted.state,\n }),\n })\n}\n\n/** The workspace and member a provider's redirect is for, read off the signed state. */\nexport function connectCallbackSubject(request: Request): PluginApiRequestSubject | null {\n const read = readConnectState(new URL(request.url).searchParams.get('state'), pluginConnectionRuntime.now())\n if (read.ok) return { orgId: read.claims.orgId, uid: read.claims.uid }\n if (read.ok === false && read.refusal === 'state-expired') {\n const { claims } = read as { claims: ConnectStateClaims }\n return { orgId: claims.orgId, uid: claims.uid }\n }\n return null\n}\n\n/**\n * Whether the member who started the connect may still finish it: the site\n * still in the same workspace, on a plan with connections, and the member\n * still holding the key.\n */\nasync function mayStillConnect(claims: ConnectStateClaims): Promise<SiteActor | null> {\n const [hostSnapshot, resolved] = await Promise.all([\n firestore().collection('hosts').doc(claims.hostId).get(),\n getOrgForHost(claims.hostId),\n ])\n if (!hostSnapshot.exists || !resolved || resolved.orgId !== claims.orgId) return null\n const host = hostSnapshot.data() ?? {}\n if (!holdsSiteConnectionsKey(host, claims.uid)) return null\n if (!checkEntitlement(resolved.org as never, PLUGIN_CONNECTIONS_FEATURE)) return null\n return {\n uid: claims.uid,\n hostId: claims.hostId,\n orgId: resolved.orgId,\n org: resolved.org as Record<string, unknown>,\n host,\n planAllows: true,\n }\n}\n\nconst plain = (status: number, text: string) =>\n new Response(text, {\n status,\n headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Referrer-Policy': 'no-referrer', ...NO_STORE },\n })\n\n/** Back to the console page the connect started on, with what happened. */\nfunction backTo(returnTo: string, claims: Pick<ConnectStateClaims, 'listingId' | 'connectionId'>, outcome: string): Response {\n const url = new URL(returnTo, 'https://console.invalid')\n url.searchParams.set('pluginConnectionListing', claims.listingId)\n url.searchParams.set('pluginConnection', claims.connectionId)\n url.searchParams.set('pluginConnectionResult', outcome)\n return new Response(null, {\n status: 303,\n headers: {\n // A RELATIVE location: the callback's own origin, which is the console's.\n Location: `${url.pathname}${url.search}`,\n 'Referrer-Policy': 'no-referrer',\n ...NO_STORE,\n },\n })\n}\n\nexport async function connectCallbackRoute(request: Request): Promise<Response> {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const params = new URL(request.url).searchParams\n const nowMs = pluginConnectionRuntime.now()\n const read = readConnectState(params.get('state'), nowMs)\n if (read.ok === false && read.refusal === 'state-invalid') {\n return plain(400, 'This connection link is not valid. Return to the console and connect again.')\n }\n const claims = (read as { claims: ConnectStateClaims }).claims\n const origin = consoleOriginFor(request.url)\n const returnTo = origin ? consolePath(claims.returnTo, origin) : null\n if (!returnTo) return plain(400, 'This connection link is not valid. Return to the console and connect again.')\n if (read.ok === false) return backTo(returnTo, claims, 'expired')\n const consumed = await consumeConnectState(firestore(), { claims, nowMs })\n if (consumed.ok === false) return backTo(returnTo, claims, 'expired')\n if (params.get('error')) return backTo(returnTo, claims, 'denied')\n const code = params.get('code') ?? ''\n if (!code || code.length > MAX_CODE_CHARS) return backTo(returnTo, claims, 'failed')\n const actor = await mayStillConnect(claims)\n if (!actor) return backTo(returnTo, claims, 'not-permitted')\n const keyring = readPluginConnectionsKeyring()\n if (!keyring) return backTo(returnTo, claims, 'failed')\n const resolved = await resolveConnection(actor, claims as unknown as Record<string, unknown>, keyring)\n if (resolved instanceof Response) return backTo(returnTo, claims, resolved.status === 409 ? 'not-set-up' : 'failed')\n\n try {\n const tokens = await exchangePluginConnectionCode(resolved.connection, resolved.client, {\n code,\n redirectUri: (consumed as { redirectUri: string }).redirectUri,\n })\n const previous = await getConnection(firestore(), actor.hostId, resolved.listingId, resolved.connection.id)\n const record = newConnectionRecord({\n keyring,\n hostId: actor.hostId,\n orgId: actor.orgId,\n listingId: resolved.listingId,\n connection: resolved.connection,\n tokens,\n uid: actor.uid,\n nowMs,\n previous,\n })\n await connectionRef(firestore(), actor.hostId, resolved.listingId, resolved.connection.id).set(record)\n await logHostActivity(\n actor.hostId,\n { uid: actor.uid },\n `Connected ${resolved.connection.label} for ${resolved.displayName}`,\n activityTarget(resolved.connection, resolved.listingId),\n )\n return backTo(returnTo, claims, 'connected')\n } catch (error) {\n console.error('plugin-connections: connect callback failed', error instanceof Error ? error.message : error)\n return backTo(returnTo, claims, 'failed')\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* Resources */\n/* ------------------------------------------------------------------ */\n\n/** The site's connection behind the gate, the plan, the key and the declaration. */\nasync function connectedRoute(\n request: Request,\n method: 'GET' | 'POST',\n): Promise<\n | Response\n | {\n params: Record<string, unknown>\n actor: SiteActor\n keyring: SecretBoxKeyring\n listingId: string\n version: string\n connection: PluginConnectionDeclaration\n client: PluginConnectionClient\n record: PluginConnectionRecord\n }\n> {\n if (request.method !== method) return methodNotAllowed(method)\n const params = await readParams(request)\n const actor = await siteConnectionsGate(request, params, { requirePlan: true })\n if (actor instanceof Response) return actor\n const keyring = keyringOr503()\n if (keyring instanceof Response) return keyring\n const resolved = await resolveConnection(actor, params, keyring)\n if (resolved instanceof Response) return resolved\n const record = await getConnection(firestore(), actor.hostId, resolved.listingId, resolved.connection.id)\n if (!record) return routeError(409, `${resolved.connection.label} is not connected.`, { reason: 'not-connected' })\n if (record.status === 'reconnect_required') {\n return routeError(409, `${resolved.connection.label} needs to be connected again.`, { reason: 'reconnect' })\n }\n return {\n params,\n actor,\n keyring,\n listingId: resolved.listingId,\n version: resolved.version,\n connection: resolved.connection,\n client: resolved.client,\n record,\n }\n}\n\n/** A resource's items, refreshing the token once on a refusal. */\nasync function listResource(\n context: Exclude<Awaited<ReturnType<typeof connectedRoute>>, Response>,\n resourceId: string,\n): Promise<{ items: Array<{ id: string; label: string }> } | Response> {\n const { connection, record } = context\n const index = (connection.resources ?? []).findIndex((resource) => resource.id === resourceId)\n if (index < 0) return routeError(404, 'That connection declares no such resource.')\n const resource = (connection.resources ?? [])[index]\n const earlier = (connection.resources ?? []).slice(0, index)\n const missing = earlier.find((one) => !record.choices[one.id])\n if (missing) return routeError(409, `Choose the ${missing.label.toLowerCase()} first.`, { reason: 'choose-first', resource: missing.id })\n const choices = Object.fromEntries(earlier.map((one) => [one.id, record.choices[one.id].id]))\n const deps = {\n firestore: firestore(),\n keyring: context.keyring,\n connection,\n client: context.client,\n now: pluginConnectionRuntime.now,\n sleep: pluginConnectionRuntime.sleep,\n }\n try {\n let accessToken = await connectionAccessToken(deps, record)\n try {\n return { items: await listPluginConnectionResource(connection, resource, { accessToken, choices }) }\n } catch (error) {\n if (!(error instanceof PluginConnectionProviderError) || error.code !== 'auth') throw error\n accessToken = await connectionAccessToken(deps, record, { force: true })\n return { items: await listPluginConnectionResource(connection, resource, { accessToken, choices }) }\n }\n } catch (error) {\n if (error instanceof PluginConnectionReconnectRequiredError) {\n return routeError(409, `${connection.label} needs to be connected again.`, { reason: 'reconnect' })\n }\n if (error instanceof PluginConnectionProviderError) {\n return routeError(502, `${connection.label} did not answer: ${error.message}`, { reason: 'provider' })\n }\n throw error\n }\n}\n\nexport async function resourcesRoute(request: Request): Promise<Response> {\n const context = await connectedRoute(request, 'GET')\n if (context instanceof Response) return context\n const listed = await listResource(context, String(context.params['resource'] ?? ''))\n return listed instanceof Response ? listed : routeJson(listed)\n}\n\nexport async function chooseRoute(request: Request): Promise<Response> {\n const context = await connectedRoute(request, 'POST')\n if (context instanceof Response) return context\n const { params, actor, connection, record, listingId } = context\n const resourceId = String(params['resource'] ?? '')\n const id = params['id']\n if (!isPluginConnectionChoiceId(id)) return routeError(400, 'Choose one of the listed items.')\n // Re-read what the grant reaches: a choice is only ever one the provider listed.\n const listed = await listResource(context, resourceId)\n if (listed instanceof Response) return listed\n const choice = listed.items.find((item) => item.id === id)\n if (!choice) return routeError(400, 'That is not one this connection reaches.')\n const resources = connection.resources ?? []\n const index = resources.findIndex((resource) => resource.id === resourceId)\n // A new choice clears every later one: a location belongs to its account.\n const choices = { ...record.choices, [resourceId]: choice }\n if (record.choices[resourceId]?.id !== choice.id) {\n for (const later of resources.slice(index + 1)) delete choices[later.id]\n }\n const nowMs = pluginConnectionRuntime.now()\n const ready = connectionReady({ choices }, connection)\n await connectionRef(firestore(), actor.hostId, listingId, connection.id).update({\n choices,\n nextSyncAtMs: ready ? nowMs : null,\n updatedAtMs: nowMs,\n })\n const next = await getConnection(firestore(), actor.hostId, listingId, connection.id)\n return routeJson({ connection: next ? toConnectionView(next, connection) : null })\n}\n\n/* ------------------------------------------------------------------ */\n/* Disconnect and status */\n/* ------------------------------------------------------------------ */\n\nexport async function disconnectRoute(request: Request): Promise<Response> {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const params = await readParams(request)\n // Any plan: a workspace that moved to Free must still be able to let go.\n const actor = await siteConnectionsGate(request, params, { requirePlan: false })\n if (actor instanceof Response) return actor\n const listingId = params['listingId']\n const connectionId = params['connectionId']\n if (!isListingId(listingId) || typeof connectionId !== 'string' || !connectionId) {\n return routeError(400, 'Missing listingId or connectionId')\n }\n const record = await getConnection(firestore(), actor.hostId, listingId, connectionId)\n if (!record) return routeJson({ connection: null })\n const revoked = await revokeRecord(record, actor.orgId)\n // The record goes with the grant (AGL-4194): what was read with a grant the\n // owner withdrew is not shown on their site afterwards.\n await Promise.all([\n connectionRef(firestore(), actor.hostId, listingId, connectionId).delete(),\n connectionRecordRef(firestore(), actor.hostId, listingId, connectionId).delete(),\n ])\n // The site's pages stop showing it now, not when their cache expires (AGL-4195).\n await announceConnectionRecordChange(actor.hostId, `plugin connection ${listingId}/${connectionId} disconnected`)\n await logHostActivity(actor.hostId, { uid: actor.uid }, `Disconnected a plugin connection (${record.provider})`, {\n type: 'marketplace:plugin-connection',\n id: `${listingId}/${connectionId}`,\n })\n return routeJson({ connection: null, revoked })\n}\n\nexport async function statusRoute(request: Request): Promise<Response> {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const params = await readParams(request)\n const actor = await siteConnectionsGate(request, params, { requirePlan: false })\n if (actor instanceof Response) return actor\n const onlyListing = isListingId(params['listingId']) ? params['listingId'] : null\n const [installed, records] = await Promise.all([\n installedConnectionsForHost(firestore(), { hostId: actor.hostId, orgId: actor.orgId }),\n listHostConnections(firestore(), actor.hostId),\n ])\n const entries: PluginConnectionStatusEntry[] = []\n for (const plugin of installed) {\n if (onlyListing && plugin.listingId !== onlyListing) continue\n const set = await credentialsStatus(\n firestore(),\n plugin.listingId,\n plugin.connections.map((connection) => connection.id),\n )\n for (const connection of plugin.connections) {\n const record = records.find((one) => one.listingId === plugin.listingId && one.connectionId === connection.id)\n entries.push({\n listingId: plugin.listingId,\n connectionId: connection.id,\n provider: connection.provider,\n label: connection.label,\n ...(connection.description ? { description: connection.description } : {}),\n resources: (connection.resources ?? []).map((resource) => ({ id: resource.id, label: resource.label })),\n connection: record ? toConnectionView(record, connection) : null,\n credentialsSet: set[connection.id]?.set ?? false,\n planAllows: actor.planAllows,\n hosts: pluginConnectionHosts(connection),\n })\n }\n }\n return routeJson({ configured: Boolean(readPluginConnectionsKeyring()), planAllows: actor.planAllows, connections: entries })\n}\n\n/* ------------------------------------------------------------------ */\n/* Sync now */\n/* ------------------------------------------------------------------ */\n\n/** A second \"Sync now\" within this of the last sync that ran is answered with the connection as it is. */\nexport const PLUGIN_CONNECTION_SYNC_NOW_COOLDOWN_MS = 60_000\n\n/** How long a \"Sync now\" may keep starting pages: the console's request has to answer. */\nconst SYNC_NOW_BUDGET_MS = 45_000\n\n/**\n * POST `{ hostId, listingId, connectionId }` (AGL-4194): runs the\n * connection's declared sync once, now, as the scheduled sync would, and\n * answers `{ connection, synced }` — the view after the run, which carries a\n * failure as `status` and `lastError`, so a provider error is a 200 with\n * `synced: false`, not a 502. Site key and plan.\n *\n * 409 `not-connected`, `reconnect`, `choose-first` (a resource not chosen)\n * or `no-sync` (the pinned version declares no sync)\n * 429 `too-soon` within a minute of the last sync, with the view\n */\nexport async function syncNowRoute(request: Request): Promise<Response> {\n const context = await connectedRoute(request, 'POST')\n if (context instanceof Response) return context\n const { actor, connection, record, listingId, keyring, client, version } = context\n if (!connection.sync) {\n return routeError(409, `${connection.label} declares nothing to sync.`, { reason: 'no-sync' })\n }\n const missing = (connection.resources ?? []).find((resource) => !record.choices[resource.id])\n if (missing) {\n return routeError(409, `Choose the ${missing.label.toLowerCase()} first.`, { reason: 'choose-first', resource: missing.id })\n }\n const nowMs = pluginConnectionRuntime.now()\n if (record.lastSyncAtMs && nowMs - record.lastSyncAtMs < PLUGIN_CONNECTION_SYNC_NOW_COOLDOWN_MS) {\n return routeError(429, `${connection.label} synced less than a minute ago.`, {\n reason: 'too-soon',\n retryAfterMs: PLUGIN_CONNECTION_SYNC_NOW_COOLDOWN_MS - (nowMs - record.lastSyncAtMs),\n connection: toConnectionView(record, connection),\n })\n }\n const outcome = await runConnectionSync({\n keyring,\n record,\n connection,\n version,\n client,\n deadlineMs: nowMs + SYNC_NOW_BUDGET_MS,\n })\n const next = await getConnection(firestore(), actor.hostId, listingId, connection.id)\n return routeJson({ connection: next ? toConnectionView(next, connection) : null, synced: outcome === 'synced' })\n}\n\n/* ------------------------------------------------------------------ */\n/* The publisher's OAuth app */\n/* ------------------------------------------------------------------ */\n\n/**\n * GET: whether each connection the listing's latest version declares has its\n * OAuth app set. POST `{ listingId, connectionId, clientId, clientSecret }`\n * sets or replaces the pair; `{ listingId, connectionId, clear: true }`\n * removes it. Never answers either value. Asks \"Set a listing's connection\n * credentials\" of a member of the workspace that published the listing.\n */\nexport async function credentialsRoute(request: Request): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'POST') return methodNotAllowed('GET, POST')\n const params = await readParams(request)\n const caller = await signedIn(request)\n if (caller instanceof Response) return caller\n const listingId = params['listingId']\n if (!isListingId(listingId)) return routeError(400, 'Missing listingId')\n const listing = await firestore().collection('marketplaceListings').doc(listingId).get()\n if (!listing.exists || listing.get('deletedAt')) return routeError(404, 'Unknown listing')\n const publisherOrgId = listing.get('profileId')\n // The key spelled out (`PLUGIN_CONNECTIONS_CREDENTIALS_PERMISSION`), so the\n // repo's enforcement guard finds the door that asks it.\n const verdict = await publisherPermissionVerdict(\n caller.uid,\n typeof publisherOrgId === 'string' ? publisherOrgId : null,\n 'marketplace.connectionCredentials',\n )\n if (verdict === 'not-a-member') return routeError(403, 'Not your listing')\n if (verdict === 'refused') {\n return routeError(403, pluginConnectionsPermissionRefusal(PLUGIN_CONNECTIONS_CREDENTIALS_PERMISSION), {\n reason: 'permission',\n permission: PLUGIN_CONNECTIONS_CREDENTIALS_PERMISSION,\n })\n }\n const declared = await latestListingConnections(firestore(), listingId, String(listing.get('latestVersion') ?? ''))\n const answer = async (): Promise<Response> => {\n const set = await credentialsStatus(firestore(), listingId, declared.map((connection) => connection.id))\n const credentials: PluginConnectionCredentialsStatus[] = declared.map((connection) => ({\n connectionId: connection.id,\n label: connection.label,\n set: set[connection.id]?.set ?? false,\n updatedAtMs: set[connection.id]?.updatedAtMs ?? null,\n }))\n return routeJson({ configured: Boolean(readPluginConnectionsKeyring()), credentials })\n }\n if (request.method === 'GET') return answer()\n\n const connection = declared.find((one) => one.id === params['connectionId'])\n if (!connection) return routeError(404, 'The listing’s latest version declares no such connection.')\n if (params['clear'] === true) {\n await clearCredentials(firestore(), listingId, connection.id)\n return answer()\n }\n const clientId = typeof params['clientId'] === 'string' ? params['clientId'].trim() : ''\n const clientSecret = typeof params['clientSecret'] === 'string' ? params['clientSecret'].trim() : ''\n if (!CLIENT_ID_PATTERN.test(clientId) || !CLIENT_SECRET_PATTERN.test(clientSecret)) {\n return routeError(400, 'Enter the OAuth app’s client id and client secret.')\n }\n const keyring = keyringOr503()\n if (keyring instanceof Response) return keyring\n await saveCredentials(\n firestore(),\n sealCredentials(keyring, {\n orgId: String(publisherOrgId),\n listingId,\n connectionId: connection.id,\n clientId,\n clientSecret,\n uid: caller.uid,\n nowMs: pluginConnectionRuntime.now(),\n }),\n )\n return answer()\n}\n"],"names":["holdsStampedSiteKey","checkEntitlement","isPluginConnectionChoiceId","pluginConnectionAuthorizeUrl","firebaseAdmin","getOrgForHost","logHostActivity","isEmailVerified","isImpersonationSession","isRefusedIdToken","PLUGIN_CONNECTIONS_CREDENTIALS_PERMISSION","PLUGIN_CONNECTIONS_FEATURE","PLUGIN_CONNECTIONS_PLAN_REFUSAL","PLUGIN_CONNECTIONS_SITE_PERMISSION","PLUGIN_CONNECTIONS_SITE_ROLE_PRESET","pluginConnectionHosts","pluginConnectionsPermissionRefusal","publisherPermissionVerdict","isHostId","isListingId","readPluginConnectionsKeyring","announceConnectionRecordChange","connectionAccessToken","connectionReady","connectionRecordRef","connectionRef","getConnection","listHostConnections","newConnectionRecord","PluginConnectionReconnectRequiredError","toConnectionView","clearCredentials","credentialsStatus","openCredentials","saveCredentials","sealCredentials","installedConnection","installedConnectionsForHost","latestListingConnections","connectRedirectUri","consoleOriginFor","consolePath","consumeConnectState","mintConnectState","readConnectState","recordConnectState","exchangePluginConnectionCode","listPluginConnectionResource","pluginConnectionRuntime","PluginConnectionProviderError","revokeRecord","runConnectionSync","NO_STORE","MAX_CODE_CHARS","CLIENT_ID_PATTERN","CLIENT_SECRET_PATTERN","NOT_CONFIGURED","firestore","app","routeJson","body","status","Response","json","headers","routeError","error","extra","methodNotAllowed","allow","JSON","stringify","Allow","readParams","request","query","Object","fromEntries","URL","url","searchParams","entries","method","catch","Array","isArray","signedIn","authorization","get","startsWith","decoded","auth","verifyIdToken","slice","length","console","uid","staff","holdsSiteConnectionsKey","host","siteConnectionsGate","params","options","hostSnapshot","caller","hostId","resolved","Promise","all","collection","doc","exists","data","reason","permission","planAllows","org","requirePlan","feature","orgId","keyringOr503","resolveConnection","actor","keyring","listingId","connectionId","found","client","displayName","installed","version","connection","activityTarget","type","id","name","label","connectStartRoute","origin","redirectUri","returnTo","nowMs","now","minted","claims","authorizeUrl","clientId","state","connectCallbackSubject","read","ok","refusal","mayStillConnect","plain","text","backTo","outcome","set","Location","pathname","search","connectCallbackRoute","consumed","code","tokens","previous","record","Error","message","connectedRoute","listResource","context","resourceId","index","resources","findIndex","resource","earlier","missing","find","one","choices","toLowerCase","map","deps","sleep","accessToken","items","force","resourcesRoute","listed","String","chooseRoute","choice","item","later","ready","update","nextSyncAtMs","updatedAtMs","next","disconnectRoute","revoked","delete","provider","statusRoute","onlyListing","records","plugin","connections","push","description","credentialsSet","hosts","configured","Boolean","PLUGIN_CONNECTION_SYNC_NOW_COOLDOWN_MS","SYNC_NOW_BUDGET_MS","syncNowRoute","sync","lastSyncAtMs","retryAfterMs","deadlineMs","synced","credentialsRoute","listing","publisherOrgId","verdict","declared","answer","credentials","trim","clientSecret","test"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,0CAAyC;AAC7E,SAASC,gBAAgB,QAAQ,2CAA0C;AAC3E,SACEC,0BAA0B,EAC1BC,4BAA4B,QAEvB,4CAA2C;AAElD,SAASC,aAAa,EAAEC,aAAa,EAAEC,eAAe,QAAQ,2BAA0B;AACxF,SAASC,eAAe,EAAEC,sBAAsB,QAAQ,iDAAgD;AACxG,SAASC,gBAAgB,QAAQ,mDAAkD;AAEnF,SACEC,yCAAyC,EACzCC,0BAA0B,EAC1BC,+BAA+B,EAC/BC,kCAAkC,EAClCC,mCAAmC,EACnCC,qBAAqB,EACrBC,kCAAkC,QAG7B,oCAAgC;AACvC,SAASC,0BAA0B,QAAQ,8BAA0B;AACrE,SAASC,QAAQ,EAAEC,WAAW,EAAEC,4BAA4B,QAAQ,cAAU;AAC9E,SAASC,8BAA8B,QAAQ,uBAAmB;AAClE,SACEC,qBAAqB,EACrBC,eAAe,EACfC,mBAAmB,EACnBC,aAAa,EACbC,aAAa,EACbC,mBAAmB,EACnBC,mBAAmB,EACnBC,sCAAsC,EACtCC,gBAAgB,QAEX,wBAAoB;AAC3B,SAASC,gBAAgB,EAAEC,iBAAiB,EAAEC,eAAe,EAAEC,eAAe,EAAEC,eAAe,QAAQ,yBAAqB;AAC5H,SACEC,mBAAmB,EACnBC,2BAA2B,EAC3BC,wBAAwB,QACnB,4BAAwB;AAC/B,SACEC,kBAAkB,EAClBC,gBAAgB,EAChBC,WAAW,EACXC,mBAAmB,EACnBC,gBAAgB,EAChBC,gBAAgB,EAChBC,kBAAkB,QAEb,mBAAe;AACtB,SACEC,4BAA4B,EAC5BC,4BAA4B,EAC5BC,uBAAuB,EACvBC,6BAA6B,QAExB,qBAAiB;AACxB,SAASC,YAAY,QAAQ,cAAU;AACvC,SAASC,iBAAiB,QAAQ,YAAQ;AAE1C;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,MAAMC,WAAW;IAAE,iBAAiB;AAAW;AAC/C,MAAMC,iBAAiB;AACvB,MAAMC,oBAAoB;AAC1B,MAAMC,wBAAwB;AAC9B,MAAMC,iBAAiB;AAEvB,MAAMC,YAAY,IAAmCrD,cAAcsD,GAAG,GAAGD,SAAS;AAElF,OAAO,SAASE,UAAUC,IAAa,EAAEC,SAAS,GAAG;IACnD,OAAOC,SAASC,IAAI,CAACH,MAAM;QAAEC;QAAQG,SAASZ;IAAS;AACzD;AAEA,OAAO,SAASa,WAAWJ,MAAc,EAAEK,KAAa,EAAEC,QAAiC,CAAC,CAAC;IAC3F,OAAOL,SAASC,IAAI,CAAC;QAAEG;OAAUC,QAAS;QAAEN;QAAQG,SAASZ;IAAS;AACxE;AAEA,MAAMgB,mBAAmB,CAACC,QACxB,IAAIP,SAASQ,KAAKC,SAAS,CAAC;QAAEL,OAAO;IAAqB,IAAI;QAC5DL,QAAQ;QACRG,SAAS;YAAE,gBAAgB;YAAoBQ,OAAOH;WAAUjB;IAClE;AAEF,8FAA8F,GAC9F,eAAeqB,WAAWC,OAAgB;IACxC,MAAMC,QAAQC,OAAOC,WAAW,CAAC,IAAIC,IAAIJ,QAAQK,GAAG,EAAEC,YAAY,CAACC,OAAO;IAC1E,IAAIP,QAAQQ,MAAM,KAAK,SAASR,QAAQQ,MAAM,KAAK,QAAQ,OAAOP;IAClE,MAAMf,OAAO,MAAMc,QAAQX,IAAI,GAAGoB,KAAK,CAAC,IAAM;IAC9C,OAAOvB,QAAQ,OAAOA,SAAS,YAAY,CAACwB,MAAMC,OAAO,CAACzB,QAAQ,aAAKe,OAAWf,QAAqCe;AACzH;AAEA,0CAA0C,GAC1C,eAAeW,SAASZ,OAAgB;QAChBA;IAAtB,MAAMa,iBAAgBb,uBAAAA,QAAQV,OAAO,CAACwB,GAAG,CAAC,4BAApBd,uBAAwC;IAC9D,IAAI,CAACa,cAAcE,UAAU,CAAC,YAAY,OAAOxB,WAAW,KAAK;IACjE,IAAIyB;IACJ,IAAI;QACFA,UAAU,MAAMtF,cAAcsD,GAAG,GAAGiC,IAAI,GAAGC,aAAa,CAACL,cAAcM,KAAK,CAAC,UAAUC,MAAM;IAC/F,EAAE,OAAO5B,OAAO;QACd,yEAAyE;QACzE,iEAAiE;QACjE,IAAIzD,iBAAiByD,QAAQ,OAAOD,WAAW,KAAK;QACpD8B,QAAQ7B,KAAK,CAAC,wDAAwDA;QACtE,OAAOD,WAAW,KAAK;IACzB;IACA,IAAI,CAAC1D,gBAAgBmF,YAAY,CAAClF,uBAAuBkF,UAAU;QACjE,OAAOzB,WAAW,KAAK;IACzB;IACA,OAAO;QAAE+B,KAAKN,QAAQM,GAAG;QAAEC,OAAOP,OAAO,CAAC,QAAQ,KAAK;IAAK;AAC9D;AAEA,0EAA0E,GAC1E,OAAO,SAASQ,wBAAwBC,IAA6B,EAAEH,GAAW;IAChF,OAAOhG,oBAAoBmG,MAAMH,KAAKnF,oCAAoCC;AAC5E;AAWA;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAesF,oBACpB1B,OAAgB,EAChB2B,MAA+B,EAC/BC,OAAiC;QAQpBC;IANb,MAAMC,SAAS,MAAMlB,SAASZ;IAC9B,IAAI8B,kBAAkB1C,UAAU,OAAO0C;IACvC,MAAMC,SAASJ,MAAM,CAAC,SAAS;IAC/B,IAAI,CAACnF,SAASuF,SAAS,OAAOxC,WAAW,KAAK;IAC9C,MAAM,CAACsC,cAAcG,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAACnD,YAAYoD,UAAU,CAAC,SAASC,GAAG,CAACL,QAAQjB,GAAG;QAAInF,cAAcoG;KAAQ;IAC7H,IAAI,CAACF,aAAaQ,MAAM,IAAI,CAACL,UAAU,OAAOzC,WAAW,KAAK;IAC9D,MAAMkC,QAAOI,qBAAAA,aAAaS,IAAI,cAAjBT,qBAAuB,CAAC;IACrC,IAAI,CAACL,wBAAwBC,MAAMK,OAAOR,GAAG,GAAG;QAC9C,OAAO/B,WAAW,KAAKjD,mCAAmCH,qCAAqC;YAC7FoG,QAAQ;YACRC,YAAYrG;QACd;IACF;IACA,MAAMsG,aAAalH,iBAAiByG,SAASU,GAAG,EAAWzG;IAC3D,IAAI2F,QAAQe,WAAW,IAAI,CAACF,YAAY;QACtC,OAAOlD,WAAW,KAAKrD,iCAAiC;YAAEqG,QAAQ;YAAQK,SAAS3G;QAA2B;IAChH;IACA,OAAO;QAAEqF,KAAKQ,OAAOR,GAAG;QAAES;QAAQc,OAAOb,SAASa,KAAK;QAAEH,KAAKV,SAASU,GAAG;QAA6BjB;QAAMgB;IAAW;AAC1H;AAEA,kEAAkE,GAClE,SAASK;QACApG;IAAP,QAAOA,gCAAAA,0CAAAA,gCAAkC6C,WAAW,KAAKT;AAC3D;AAEA,2GAA2G,GAC3G,eAAeiE,kBACbC,KAAgB,EAChBrB,MAA+B,EAC/BsB,OAAyB;IAYzB,MAAMC,YAAYvB,MAAM,CAAC,YAAY;IACrC,MAAMwB,eAAexB,MAAM,CAAC,eAAe;IAC3C,IAAI,CAAClF,YAAYyG,cAAc,OAAOC,iBAAiB,YAAY,CAACA,cAAc;QAChF,OAAO5D,WAAW,KAAK;IACzB;IACA,MAAM6D,QAAQ,MAAM1F,oBAAoBqB,aAAa;QAAEgD,QAAQiB,MAAMjB,MAAM;QAAEc,OAAOG,MAAMH,KAAK;QAAEK;QAAWC;IAAa;IACzH,IAAI,CAACC,OAAO,OAAO7D,WAAW,KAAK;IACnC,IAAI8D;IACJ,IAAI;QACFA,SAAS,MAAM9F,gBAAgBwB,aAAakE,SAASC,WAAWC;IAClE,EAAE,OAAO3D,OAAO;QACd6B,QAAQ7B,KAAK,CAAC,4DAA4DA;QAC1E6D,SAAS;IACX;IACA,IAAI,CAACA,QAAQ;QACX,OAAO9D,WAAW,KAAK,2EAA2E;YAAEgD,QAAQ;QAAc;IAC5H;IACA,OAAO;QACLW;QACAI,aAAaF,MAAMG,SAAS,CAACD,WAAW;QACxCE,SAASJ,MAAMG,SAAS,CAACC,OAAO;QAChCC,YAAYL,MAAMK,UAAU;QAC5BJ;IACF;AACF;AAEA,MAAMK,iBAAiB,CAACD,YAAyCP,YAC9D,CAAA;QAAES,MAAM;QAAiCC,IAAI,GAAGV,UAAU,CAAC,EAAEO,WAAWG,EAAE,EAAE;QAAEC,MAAMJ,WAAWK,KAAK;IAAC,CAAA;AAExG,sEAAsE,GACtE,wEAAwE,GACxE,sEAAsE,GAEtE,OAAO,eAAeC,kBAAkB/D,OAAgB;IACtD,IAAIA,QAAQQ,MAAM,KAAK,SAASR,QAAQQ,MAAM,KAAK,QAAQ,OAAOd,iBAAiB;IACnF,MAAMiC,SAAS,MAAM5B,WAAWC;IAChC,MAAMgD,QAAQ,MAAMtB,oBAAoB1B,SAAS2B,QAAQ;QAAEgB,aAAa;IAAK;IAC7E,IAAIK,iBAAiB5D,UAAU,OAAO4D;IACtC,MAAMC,UAAUH;IAChB,IAAIG,mBAAmB7D,UAAU,OAAO6D;IACxC,MAAMjB,WAAW,MAAMe,kBAAkBC,OAAOrB,QAAQsB;IACxD,IAAIjB,oBAAoB5C,UAAU,OAAO4C;IACzC,MAAMgC,SAASlG,iBAAiBkC,QAAQK,GAAG;IAC3C,MAAM4D,cAAcpG,mBAAmBmC,QAAQK,GAAG;IAClD,IAAI,CAAC2D,UAAU,CAACC,aAAa,OAAO1E,WAAW,KAAK;IACpD,MAAM2E,WAAWnG,YAAY4D,MAAM,CAAC,WAAW,EAAEqC;IACjD,IAAI,CAACE,UAAU,OAAO3E,WAAW,KAAK;IACtC,MAAM4E,QAAQ7F,wBAAwB8F,GAAG;IACzC,IAAIC;IACJ,IAAI;QACFA,SAASpG,iBAAiB;YACxB8D,QAAQiB,MAAMjB,MAAM;YACpBc,OAAOG,MAAMH,KAAK;YAClBvB,KAAK0B,MAAM1B,GAAG;YACd4B,WAAWlB,SAASkB,SAAS;YAC7BC,cAAcnB,SAASyB,UAAU,CAACG,EAAE;YACpCM;YACAC;QACF;IACF,EAAE,eAAM;QACN,OAAO5E,WAAW,KAAKT;IACzB;IACA,MAAMX,mBAAmBY,aAAa;QAAEuF,QAAQD,OAAOC,MAAM;QAAEL;QAAaE;IAAM;IAClF,OAAOlF,UAAU;QACfsF,cAAc9I,6BAA6BuG,SAASyB,UAAU,EAAE;YAC9De,UAAUxC,SAASqB,MAAM,CAACmB,QAAQ;YAClCP;YACAQ,OAAOJ,OAAOI,KAAK;QACrB;IACF;AACF;AAEA,sFAAsF,GACtF,OAAO,SAASC,uBAAuB1E,OAAgB;IACrD,MAAM2E,OAAOzG,iBAAiB,IAAIkC,IAAIJ,QAAQK,GAAG,EAAEC,YAAY,CAACQ,GAAG,CAAC,UAAUxC,wBAAwB8F,GAAG;IACzG,IAAIO,KAAKC,EAAE,EAAE,OAAO;QAAE/B,OAAO8B,KAAKL,MAAM,CAACzB,KAAK;QAAEvB,KAAKqD,KAAKL,MAAM,CAAChD,GAAG;IAAC;IACrE,IAAIqD,KAAKC,EAAE,KAAK,SAASD,KAAKE,OAAO,KAAK,iBAAiB;QACzD,MAAM,EAAEP,MAAM,EAAE,GAAGK;QACnB,OAAO;YAAE9B,OAAOyB,OAAOzB,KAAK;YAAEvB,KAAKgD,OAAOhD,GAAG;QAAC;IAChD;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,eAAewD,gBAAgBR,MAA0B;QAM1CzC;IALb,MAAM,CAACA,cAAcG,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;QACjDnD,YAAYoD,UAAU,CAAC,SAASC,GAAG,CAACkC,OAAOvC,MAAM,EAAEjB,GAAG;QACtDnF,cAAc2I,OAAOvC,MAAM;KAC5B;IACD,IAAI,CAACF,aAAaQ,MAAM,IAAI,CAACL,YAAYA,SAASa,KAAK,KAAKyB,OAAOzB,KAAK,EAAE,OAAO;IACjF,MAAMpB,QAAOI,qBAAAA,aAAaS,IAAI,cAAjBT,qBAAuB,CAAC;IACrC,IAAI,CAACL,wBAAwBC,MAAM6C,OAAOhD,GAAG,GAAG,OAAO;IACvD,IAAI,CAAC/F,iBAAiByG,SAASU,GAAG,EAAWzG,6BAA6B,OAAO;IACjF,OAAO;QACLqF,KAAKgD,OAAOhD,GAAG;QACfS,QAAQuC,OAAOvC,MAAM;QACrBc,OAAOb,SAASa,KAAK;QACrBH,KAAKV,SAASU,GAAG;QACjBjB;QACAgB,YAAY;IACd;AACF;AAEA,MAAMsC,QAAQ,CAAC5F,QAAgB6F,OAC7B,IAAI5F,SAAS4F,MAAM;QACjB7F;QACAG,SAAS;YAAE,gBAAgB;YAA6B,mBAAmB;WAAkBZ;IAC/F;AAEF,yEAAyE,GACzE,SAASuG,OAAOf,QAAgB,EAAEI,MAA8D,EAAEY,OAAe;IAC/G,MAAM7E,MAAM,IAAID,IAAI8D,UAAU;IAC9B7D,IAAIC,YAAY,CAAC6E,GAAG,CAAC,2BAA2Bb,OAAOpB,SAAS;IAChE7C,IAAIC,YAAY,CAAC6E,GAAG,CAAC,oBAAoBb,OAAOnB,YAAY;IAC5D9C,IAAIC,YAAY,CAAC6E,GAAG,CAAC,0BAA0BD;IAC/C,OAAO,IAAI9F,SAAS,MAAM;QACxBD,QAAQ;QACRG,SAAS;YACP,0EAA0E;YAC1E8F,UAAU,GAAG/E,IAAIgF,QAAQ,GAAGhF,IAAIiF,MAAM,EAAE;YACxC,mBAAmB;WAChB5G;IAEP;AACF;AAEA,OAAO,eAAe6G,qBAAqBvF,OAAgB;QAgB5C2B;IAfb,IAAI3B,QAAQQ,MAAM,KAAK,OAAO,OAAOd,iBAAiB;IACtD,MAAMiC,SAAS,IAAIvB,IAAIJ,QAAQK,GAAG,EAAEC,YAAY;IAChD,MAAM6D,QAAQ7F,wBAAwB8F,GAAG;IACzC,MAAMO,OAAOzG,iBAAiByD,OAAOb,GAAG,CAAC,UAAUqD;IACnD,IAAIQ,KAAKC,EAAE,KAAK,SAASD,KAAKE,OAAO,KAAK,iBAAiB;QACzD,OAAOE,MAAM,KAAK;IACpB;IACA,MAAMT,SAAS,AAACK,KAAwCL,MAAM;IAC9D,MAAMN,SAASlG,iBAAiBkC,QAAQK,GAAG;IAC3C,MAAM6D,WAAWF,SAASjG,YAAYuG,OAAOJ,QAAQ,EAAEF,UAAU;IACjE,IAAI,CAACE,UAAU,OAAOa,MAAM,KAAK;IACjC,IAAIJ,KAAKC,EAAE,KAAK,OAAO,OAAOK,OAAOf,UAAUI,QAAQ;IACvD,MAAMkB,WAAW,MAAMxH,oBAAoBe,aAAa;QAAEuF;QAAQH;IAAM;IACxE,IAAIqB,SAASZ,EAAE,KAAK,OAAO,OAAOK,OAAOf,UAAUI,QAAQ;IAC3D,IAAI3C,OAAOb,GAAG,CAAC,UAAU,OAAOmE,OAAOf,UAAUI,QAAQ;IACzD,MAAMmB,QAAO9D,cAAAA,OAAOb,GAAG,CAAC,mBAAXa,cAAsB;IACnC,IAAI,CAAC8D,QAAQA,KAAKrE,MAAM,GAAGzC,gBAAgB,OAAOsG,OAAOf,UAAUI,QAAQ;IAC3E,MAAMtB,QAAQ,MAAM8B,gBAAgBR;IACpC,IAAI,CAACtB,OAAO,OAAOiC,OAAOf,UAAUI,QAAQ;IAC5C,MAAMrB,UAAUvG;IAChB,IAAI,CAACuG,SAAS,OAAOgC,OAAOf,UAAUI,QAAQ;IAC9C,MAAMtC,WAAW,MAAMe,kBAAkBC,OAAOsB,QAA8CrB;IAC9F,IAAIjB,oBAAoB5C,UAAU,OAAO6F,OAAOf,UAAUI,QAAQtC,SAAS7C,MAAM,KAAK,MAAM,eAAe;IAE3G,IAAI;QACF,MAAMuG,SAAS,MAAMtH,6BAA6B4D,SAASyB,UAAU,EAAEzB,SAASqB,MAAM,EAAE;YACtFoC;YACAxB,aAAa,AAACuB,SAAqCvB,WAAW;QAChE;QACA,MAAM0B,WAAW,MAAM3I,cAAc+B,aAAaiE,MAAMjB,MAAM,EAAEC,SAASkB,SAAS,EAAElB,SAASyB,UAAU,CAACG,EAAE;QAC1G,MAAMgC,SAAS1I,oBAAoB;YACjC+F;YACAlB,QAAQiB,MAAMjB,MAAM;YACpBc,OAAOG,MAAMH,KAAK;YAClBK,WAAWlB,SAASkB,SAAS;YAC7BO,YAAYzB,SAASyB,UAAU;YAC/BiC;YACApE,KAAK0B,MAAM1B,GAAG;YACd6C;YACAwB;QACF;QACA,MAAM5I,cAAcgC,aAAaiE,MAAMjB,MAAM,EAAEC,SAASkB,SAAS,EAAElB,SAASyB,UAAU,CAACG,EAAE,EAAEuB,GAAG,CAACS;QAC/F,MAAMhK,gBACJoH,MAAMjB,MAAM,EACZ;YAAET,KAAK0B,MAAM1B,GAAG;QAAC,GACjB,CAAC,UAAU,EAAEU,SAASyB,UAAU,CAACK,KAAK,CAAC,KAAK,EAAE9B,SAASsB,WAAW,EAAE,EACpEI,eAAe1B,SAASyB,UAAU,EAAEzB,SAASkB,SAAS;QAExD,OAAO+B,OAAOf,UAAUI,QAAQ;IAClC,EAAE,OAAO9E,OAAO;QACd6B,QAAQ7B,KAAK,CAAC,+CAA+CA,iBAAiBqG,QAAQrG,MAAMsG,OAAO,GAAGtG;QACtG,OAAOyF,OAAOf,UAAUI,QAAQ;IAClC;AACF;AAEA,sEAAsE,GACtE,wEAAwE,GACxE,sEAAsE,GAEtE,kFAAkF,GAClF,eAAeyB,eACb/F,OAAgB,EAChBQ,MAAsB;IActB,IAAIR,QAAQQ,MAAM,KAAKA,QAAQ,OAAOd,iBAAiBc;IACvD,MAAMmB,SAAS,MAAM5B,WAAWC;IAChC,MAAMgD,QAAQ,MAAMtB,oBAAoB1B,SAAS2B,QAAQ;QAAEgB,aAAa;IAAK;IAC7E,IAAIK,iBAAiB5D,UAAU,OAAO4D;IACtC,MAAMC,UAAUH;IAChB,IAAIG,mBAAmB7D,UAAU,OAAO6D;IACxC,MAAMjB,WAAW,MAAMe,kBAAkBC,OAAOrB,QAAQsB;IACxD,IAAIjB,oBAAoB5C,UAAU,OAAO4C;IACzC,MAAM4D,SAAS,MAAM5I,cAAc+B,aAAaiE,MAAMjB,MAAM,EAAEC,SAASkB,SAAS,EAAElB,SAASyB,UAAU,CAACG,EAAE;IACxG,IAAI,CAACgC,QAAQ,OAAOrG,WAAW,KAAK,GAAGyC,SAASyB,UAAU,CAACK,KAAK,CAAC,kBAAkB,CAAC,EAAE;QAAEvB,QAAQ;IAAgB;IAChH,IAAIqD,OAAOzG,MAAM,KAAK,sBAAsB;QAC1C,OAAOI,WAAW,KAAK,GAAGyC,SAASyB,UAAU,CAACK,KAAK,CAAC,6BAA6B,CAAC,EAAE;YAAEvB,QAAQ;QAAY;IAC5G;IACA,OAAO;QACLZ;QACAqB;QACAC;QACAC,WAAWlB,SAASkB,SAAS;QAC7BM,SAASxB,SAASwB,OAAO;QACzBC,YAAYzB,SAASyB,UAAU;QAC/BJ,QAAQrB,SAASqB,MAAM;QACvBuC;IACF;AACF;AAEA,gEAAgE,GAChE,eAAeI,aACbC,OAAsE,EACtEC,UAAkB;QAGHzC,uBAEGA,wBACDA;IAJjB,MAAM,EAAEA,UAAU,EAAEmC,MAAM,EAAE,GAAGK;IAC/B,MAAME,QAAQ,EAAC1C,wBAAAA,WAAW2C,SAAS,YAApB3C,wBAAwB,EAAE,EAAE4C,SAAS,CAAC,CAACC,WAAaA,SAAS1C,EAAE,KAAKsC;IACnF,IAAIC,QAAQ,GAAG,OAAO5G,WAAW,KAAK;IACtC,MAAM+G,WAAW,EAAC7C,yBAAAA,WAAW2C,SAAS,YAApB3C,yBAAwB,EAAE,CAAC,CAAC0C,MAAM;IACpD,MAAMI,UAAU,EAAC9C,yBAAAA,WAAW2C,SAAS,YAApB3C,yBAAwB,EAAE,EAAEtC,KAAK,CAAC,GAAGgF;IACtD,MAAMK,UAAUD,QAAQE,IAAI,CAAC,CAACC,MAAQ,CAACd,OAAOe,OAAO,CAACD,IAAI9C,EAAE,CAAC;IAC7D,IAAI4C,SAAS,OAAOjH,WAAW,KAAK,CAAC,WAAW,EAAEiH,QAAQ1C,KAAK,CAAC8C,WAAW,GAAG,OAAO,CAAC,EAAE;QAAErE,QAAQ;QAAgB+D,UAAUE,QAAQ5C,EAAE;IAAC;IACvI,MAAM+C,UAAUzG,OAAOC,WAAW,CAACoG,QAAQM,GAAG,CAAC,CAACH,MAAQ;YAACA,IAAI9C,EAAE;YAAEgC,OAAOe,OAAO,CAACD,IAAI9C,EAAE,CAAC,CAACA,EAAE;SAAC;IAC3F,MAAMkD,OAAO;QACX/H,WAAWA;QACXkE,SAASgD,QAAQhD,OAAO;QACxBQ;QACAJ,QAAQ4C,QAAQ5C,MAAM;QACtBe,KAAK9F,wBAAwB8F,GAAG;QAChC2C,OAAOzI,wBAAwByI,KAAK;IACtC;IACA,IAAI;QACF,IAAIC,cAAc,MAAMpK,sBAAsBkK,MAAMlB;QACpD,IAAI;YACF,OAAO;gBAAEqB,OAAO,MAAM5I,6BAA6BoF,YAAY6C,UAAU;oBAAEU;oBAAaL;gBAAQ;YAAG;QACrG,EAAE,OAAOnH,OAAO;YACd,IAAI,CAAEA,CAAAA,iBAAiBjB,6BAA4B,KAAMiB,MAAMiG,IAAI,KAAK,QAAQ,MAAMjG;YACtFwH,cAAc,MAAMpK,sBAAsBkK,MAAMlB,QAAQ;gBAAEsB,OAAO;YAAK;YACtE,OAAO;gBAAED,OAAO,MAAM5I,6BAA6BoF,YAAY6C,UAAU;oBAAEU;oBAAaL;gBAAQ;YAAG;QACrG;IACF,EAAE,OAAOnH,OAAO;QACd,IAAIA,iBAAiBrC,wCAAwC;YAC3D,OAAOoC,WAAW,KAAK,GAAGkE,WAAWK,KAAK,CAAC,6BAA6B,CAAC,EAAE;gBAAEvB,QAAQ;YAAY;QACnG;QACA,IAAI/C,iBAAiBjB,+BAA+B;YAClD,OAAOgB,WAAW,KAAK,GAAGkE,WAAWK,KAAK,CAAC,iBAAiB,EAAEtE,MAAMsG,OAAO,EAAE,EAAE;gBAAEvD,QAAQ;YAAW;QACtG;QACA,MAAM/C;IACR;AACF;AAEA,OAAO,eAAe2H,eAAenH,OAAgB;QAGDiG;IAFlD,MAAMA,UAAU,MAAMF,eAAe/F,SAAS;IAC9C,IAAIiG,mBAAmB7G,UAAU,OAAO6G;IACxC,MAAMmB,SAAS,MAAMpB,aAAaC,SAASoB,QAAOpB,2BAAAA,QAAQtE,MAAM,CAAC,WAAW,YAA1BsE,2BAA8B;IAChF,OAAOmB,kBAAkBhI,WAAWgI,SAASnI,UAAUmI;AACzD;AAEA,OAAO,eAAeE,YAAYtH,OAAgB;QAItB2B,kBAQR8B;QAIdmC;IAfJ,MAAMK,UAAU,MAAMF,eAAe/F,SAAS;IAC9C,IAAIiG,mBAAmB7G,UAAU,OAAO6G;IACxC,MAAM,EAAEtE,MAAM,EAAEqB,KAAK,EAAES,UAAU,EAAEmC,MAAM,EAAE1C,SAAS,EAAE,GAAG+C;IACzD,MAAMC,aAAamB,QAAO1F,mBAAAA,MAAM,CAAC,WAAW,YAAlBA,mBAAsB;IAChD,MAAMiC,KAAKjC,MAAM,CAAC,KAAK;IACvB,IAAI,CAACnG,2BAA2BoI,KAAK,OAAOrE,WAAW,KAAK;IAC5D,iFAAiF;IACjF,MAAM6H,SAAS,MAAMpB,aAAaC,SAASC;IAC3C,IAAIkB,kBAAkBhI,UAAU,OAAOgI;IACvC,MAAMG,SAASH,OAAOH,KAAK,CAACR,IAAI,CAAC,CAACe,OAASA,KAAK5D,EAAE,KAAKA;IACvD,IAAI,CAAC2D,QAAQ,OAAOhI,WAAW,KAAK;IACpC,MAAM6G,aAAY3C,wBAAAA,WAAW2C,SAAS,YAApB3C,wBAAwB,EAAE;IAC5C,MAAM0C,QAAQC,UAAUC,SAAS,CAAC,CAACC,WAAaA,SAAS1C,EAAE,KAAKsC;IAChE,0EAA0E;IAC1E,MAAMS,UAAU,aAAKf,OAAOe,OAAO;QAAE,CAACT,WAAW,EAAEqB;;IACnD,IAAI3B,EAAAA,6BAAAA,OAAOe,OAAO,CAACT,WAAW,qBAA1BN,2BAA4BhC,EAAE,MAAK2D,OAAO3D,EAAE,EAAE;QAChD,KAAK,MAAM6D,SAASrB,UAAUjF,KAAK,CAACgF,QAAQ,GAAI,OAAOQ,OAAO,CAACc,MAAM7D,EAAE,CAAC;IAC1E;IACA,MAAMO,QAAQ7F,wBAAwB8F,GAAG;IACzC,MAAMsD,QAAQ7K,gBAAgB;QAAE8J;IAAQ,GAAGlD;IAC3C,MAAM1G,cAAcgC,aAAaiE,MAAMjB,MAAM,EAAEmB,WAAWO,WAAWG,EAAE,EAAE+D,MAAM,CAAC;QAC9EhB;QACAiB,cAAcF,QAAQvD,QAAQ;QAC9B0D,aAAa1D;IACf;IACA,MAAM2D,OAAO,MAAM9K,cAAc+B,aAAaiE,MAAMjB,MAAM,EAAEmB,WAAWO,WAAWG,EAAE;IACpF,OAAO3E,UAAU;QAAEwE,YAAYqE,OAAO1K,iBAAiB0K,MAAMrE,cAAc;IAAK;AAClF;AAEA,sEAAsE,GACtE,wEAAwE,GACxE,sEAAsE,GAEtE,OAAO,eAAesE,gBAAgB/H,OAAgB;IACpD,IAAIA,QAAQQ,MAAM,KAAK,QAAQ,OAAOd,iBAAiB;IACvD,MAAMiC,SAAS,MAAM5B,WAAWC;IAChC,yEAAyE;IACzE,MAAMgD,QAAQ,MAAMtB,oBAAoB1B,SAAS2B,QAAQ;QAAEgB,aAAa;IAAM;IAC9E,IAAIK,iBAAiB5D,UAAU,OAAO4D;IACtC,MAAME,YAAYvB,MAAM,CAAC,YAAY;IACrC,MAAMwB,eAAexB,MAAM,CAAC,eAAe;IAC3C,IAAI,CAAClF,YAAYyG,cAAc,OAAOC,iBAAiB,YAAY,CAACA,cAAc;QAChF,OAAO5D,WAAW,KAAK;IACzB;IACA,MAAMqG,SAAS,MAAM5I,cAAc+B,aAAaiE,MAAMjB,MAAM,EAAEmB,WAAWC;IACzE,IAAI,CAACyC,QAAQ,OAAO3G,UAAU;QAAEwE,YAAY;IAAK;IACjD,MAAMuE,UAAU,MAAMxJ,aAAaoH,QAAQ5C,MAAMH,KAAK;IACtD,4EAA4E;IAC5E,wDAAwD;IACxD,MAAMZ,QAAQC,GAAG,CAAC;QAChBnF,cAAcgC,aAAaiE,MAAMjB,MAAM,EAAEmB,WAAWC,cAAc8E,MAAM;QACxEnL,oBAAoBiC,aAAaiE,MAAMjB,MAAM,EAAEmB,WAAWC,cAAc8E,MAAM;KAC/E;IACD,iFAAiF;IACjF,MAAMtL,+BAA+BqG,MAAMjB,MAAM,EAAE,CAAC,kBAAkB,EAAEmB,UAAU,CAAC,EAAEC,aAAa,aAAa,CAAC;IAChH,MAAMvH,gBAAgBoH,MAAMjB,MAAM,EAAE;QAAET,KAAK0B,MAAM1B,GAAG;IAAC,GAAG,CAAC,kCAAkC,EAAEsE,OAAOsC,QAAQ,CAAC,CAAC,CAAC,EAAE;QAC/GvE,MAAM;QACNC,IAAI,GAAGV,UAAU,CAAC,EAAEC,cAAc;IACpC;IACA,OAAOlE,UAAU;QAAEwE,YAAY;QAAMuE;IAAQ;AAC/C;AAEA,OAAO,eAAeG,YAAYnI,OAAgB;IAChD,IAAIA,QAAQQ,MAAM,KAAK,OAAO,OAAOd,iBAAiB;IACtD,MAAMiC,SAAS,MAAM5B,WAAWC;IAChC,MAAMgD,QAAQ,MAAMtB,oBAAoB1B,SAAS2B,QAAQ;QAAEgB,aAAa;IAAM;IAC9E,IAAIK,iBAAiB5D,UAAU,OAAO4D;IACtC,MAAMoF,cAAc3L,YAAYkF,MAAM,CAAC,YAAY,IAAIA,MAAM,CAAC,YAAY,GAAG;IAC7E,MAAM,CAAC4B,WAAW8E,QAAQ,GAAG,MAAMpG,QAAQC,GAAG,CAAC;QAC7CvE,4BAA4BoB,aAAa;YAAEgD,QAAQiB,MAAMjB,MAAM;YAAEc,OAAOG,MAAMH,KAAK;QAAC;QACpF5F,oBAAoB8B,aAAaiE,MAAMjB,MAAM;KAC9C;IACD,MAAMxB,UAAyC,EAAE;IACjD,KAAK,MAAM+H,UAAU/E,UAAW;QAC9B,IAAI6E,eAAeE,OAAOpF,SAAS,KAAKkF,aAAa;QACrD,MAAMjD,MAAM,MAAM7H,kBAChByB,aACAuJ,OAAOpF,SAAS,EAChBoF,OAAOC,WAAW,CAAC1B,GAAG,CAAC,CAACpD,aAAeA,WAAWG,EAAE;QAEtD,KAAK,MAAMH,cAAc6E,OAAOC,WAAW,CAAE;gBAQ7B9E;gBAEI0B;YATlB,MAAMS,SAASyC,QAAQ5B,IAAI,CAAC,CAACC,MAAQA,IAAIxD,SAAS,KAAKoF,OAAOpF,SAAS,IAAIwD,IAAIvD,YAAY,KAAKM,WAAWG,EAAE;YAC7GrD,QAAQiI,IAAI,CAAC;gBACXtF,WAAWoF,OAAOpF,SAAS;gBAC3BC,cAAcM,WAAWG,EAAE;gBAC3BsE,UAAUzE,WAAWyE,QAAQ;gBAC7BpE,OAAOL,WAAWK,KAAK;eACnBL,WAAWgF,WAAW,GAAG;gBAAEA,aAAahF,WAAWgF,WAAW;YAAC,IAAI,CAAC;gBACxErC,WAAW,EAAC3C,wBAAAA,WAAW2C,SAAS,YAApB3C,wBAAwB,EAAE,EAAEoD,GAAG,CAAC,CAACP,WAAc,CAAA;wBAAE1C,IAAI0C,SAAS1C,EAAE;wBAAEE,OAAOwC,SAASxC,KAAK;oBAAC,CAAA;gBACpGL,YAAYmC,SAASxI,iBAAiBwI,QAAQnC,cAAc;gBAC5DiF,cAAc,WAAEvD,qBAAAA,GAAG,CAAC1B,WAAWG,EAAE,CAAC,qBAAlBuB,mBAAoBA,GAAG,mBAAI;gBAC3C1C,YAAYO,MAAMP,UAAU;gBAC5BkG,OAAOtM,sBAAsBoH;;QAEjC;IACF;IACA,OAAOxE,UAAU;QAAE2J,YAAYC,QAAQnM;QAAiC+F,YAAYO,MAAMP,UAAU;QAAE8F,aAAahI;IAAQ;AAC7H;AAEA,sEAAsE,GACtE,wEAAwE,GACxE,sEAAsE,GAEtE,wGAAwG,GACxG,OAAO,MAAMuI,yCAAyC,MAAM;AAE5D,wFAAwF,GACxF,MAAMC,qBAAqB;AAE3B;;;;;;;;;;CAUC,GACD,OAAO,eAAeC,aAAahJ,OAAgB;QAOhCyD;IANjB,MAAMwC,UAAU,MAAMF,eAAe/F,SAAS;IAC9C,IAAIiG,mBAAmB7G,UAAU,OAAO6G;IACxC,MAAM,EAAEjD,KAAK,EAAES,UAAU,EAAEmC,MAAM,EAAE1C,SAAS,EAAED,OAAO,EAAEI,MAAM,EAAEG,OAAO,EAAE,GAAGyC;IAC3E,IAAI,CAACxC,WAAWwF,IAAI,EAAE;QACpB,OAAO1J,WAAW,KAAK,GAAGkE,WAAWK,KAAK,CAAC,0BAA0B,CAAC,EAAE;YAAEvB,QAAQ;QAAU;IAC9F;IACA,MAAMiE,UAAU,EAAC/C,wBAAAA,WAAW2C,SAAS,YAApB3C,wBAAwB,EAAE,EAAEgD,IAAI,CAAC,CAACH,WAAa,CAACV,OAAOe,OAAO,CAACL,SAAS1C,EAAE,CAAC;IAC5F,IAAI4C,SAAS;QACX,OAAOjH,WAAW,KAAK,CAAC,WAAW,EAAEiH,QAAQ1C,KAAK,CAAC8C,WAAW,GAAG,OAAO,CAAC,EAAE;YAAErE,QAAQ;YAAgB+D,UAAUE,QAAQ5C,EAAE;QAAC;IAC5H;IACA,MAAMO,QAAQ7F,wBAAwB8F,GAAG;IACzC,IAAIwB,OAAOsD,YAAY,IAAI/E,QAAQyB,OAAOsD,YAAY,GAAGJ,wCAAwC;QAC/F,OAAOvJ,WAAW,KAAK,GAAGkE,WAAWK,KAAK,CAAC,+BAA+B,CAAC,EAAE;YAC3EvB,QAAQ;YACR4G,cAAcL,yCAA0C3E,CAAAA,QAAQyB,OAAOsD,YAAY,AAAD;YAClFzF,YAAYrG,iBAAiBwI,QAAQnC;QACvC;IACF;IACA,MAAMyB,UAAU,MAAMzG,kBAAkB;QACtCwE;QACA2C;QACAnC;QACAD;QACAH;QACA+F,YAAYjF,QAAQ4E;IACtB;IACA,MAAMjB,OAAO,MAAM9K,cAAc+B,aAAaiE,MAAMjB,MAAM,EAAEmB,WAAWO,WAAWG,EAAE;IACpF,OAAO3E,UAAU;QAAEwE,YAAYqE,OAAO1K,iBAAiB0K,MAAMrE,cAAc;QAAM4F,QAAQnE,YAAY;IAAS;AAChH;AAEA,sEAAsE,GACtE,wEAAwE,GACxE,sEAAsE,GAEtE;;;;;;CAMC,GACD,OAAO,eAAeoE,iBAAiBtJ,OAAgB;QAwB0BuJ;IAvB/E,IAAIvJ,QAAQQ,MAAM,KAAK,SAASR,QAAQQ,MAAM,KAAK,QAAQ,OAAOd,iBAAiB;IACnF,MAAMiC,SAAS,MAAM5B,WAAWC;IAChC,MAAM8B,SAAS,MAAMlB,SAASZ;IAC9B,IAAI8B,kBAAkB1C,UAAU,OAAO0C;IACvC,MAAMoB,YAAYvB,MAAM,CAAC,YAAY;IACrC,IAAI,CAAClF,YAAYyG,YAAY,OAAO3D,WAAW,KAAK;IACpD,MAAMgK,UAAU,MAAMxK,YAAYoD,UAAU,CAAC,uBAAuBC,GAAG,CAACc,WAAWpC,GAAG;IACtF,IAAI,CAACyI,QAAQlH,MAAM,IAAIkH,QAAQzI,GAAG,CAAC,cAAc,OAAOvB,WAAW,KAAK;IACxE,MAAMiK,iBAAiBD,QAAQzI,GAAG,CAAC;IACnC,4EAA4E;IAC5E,wDAAwD;IACxD,MAAM2I,UAAU,MAAMlN,2BACpBuF,OAAOR,GAAG,EACV,OAAOkI,mBAAmB,WAAWA,iBAAiB,MACtD;IAEF,IAAIC,YAAY,gBAAgB,OAAOlK,WAAW,KAAK;IACvD,IAAIkK,YAAY,WAAW;QACzB,OAAOlK,WAAW,KAAKjD,mCAAmCN,4CAA4C;YACpGuG,QAAQ;YACRC,YAAYxG;QACd;IACF;IACA,MAAM0N,WAAW,MAAM9L,yBAAyBmB,aAAamE,WAAWmE,QAAOkC,eAAAA,QAAQzI,GAAG,CAAC,4BAAZyI,eAAgC;IAC/G,MAAMI,SAAS;QACb,MAAMxE,MAAM,MAAM7H,kBAAkByB,aAAamE,WAAWwG,SAAS7C,GAAG,CAAC,CAACpD,aAAeA,WAAWG,EAAE;QACtG,MAAMgG,cAAmDF,SAAS7C,GAAG,CAAC,CAACpD;;gBAGhE0B,oBACQA;mBAJwE;gBACrFhC,cAAcM,WAAWG,EAAE;gBAC3BE,OAAOL,WAAWK,KAAK;gBACvBqB,GAAG,WAAEA,qBAAAA,GAAG,CAAC1B,WAAWG,EAAE,CAAC,qBAAlBuB,mBAAoBA,GAAG,mBAAI;gBAChC0C,WAAW,YAAE1C,sBAAAA,GAAG,CAAC1B,WAAWG,EAAE,CAAC,qBAAlBuB,oBAAoB0C,WAAW,oBAAI;YAClD;;QACA,OAAO5I,UAAU;YAAE2J,YAAYC,QAAQnM;YAAiCkN;QAAY;IACtF;IACA,IAAI5J,QAAQQ,MAAM,KAAK,OAAO,OAAOmJ;IAErC,MAAMlG,aAAaiG,SAASjD,IAAI,CAAC,CAACC,MAAQA,IAAI9C,EAAE,KAAKjC,MAAM,CAAC,eAAe;IAC3E,IAAI,CAAC8B,YAAY,OAAOlE,WAAW,KAAK;IACxC,IAAIoC,MAAM,CAAC,QAAQ,KAAK,MAAM;QAC5B,MAAMtE,iBAAiB0B,aAAamE,WAAWO,WAAWG,EAAE;QAC5D,OAAO+F;IACT;IACA,MAAMnF,WAAW,OAAO7C,MAAM,CAAC,WAAW,KAAK,WAAWA,MAAM,CAAC,WAAW,CAACkI,IAAI,KAAK;IACtF,MAAMC,eAAe,OAAOnI,MAAM,CAAC,eAAe,KAAK,WAAWA,MAAM,CAAC,eAAe,CAACkI,IAAI,KAAK;IAClG,IAAI,CAACjL,kBAAkBmL,IAAI,CAACvF,aAAa,CAAC3F,sBAAsBkL,IAAI,CAACD,eAAe;QAClF,OAAOvK,WAAW,KAAK;IACzB;IACA,MAAM0D,UAAUH;IAChB,IAAIG,mBAAmB7D,UAAU,OAAO6D;IACxC,MAAMzF,gBACJuB,aACAtB,gBAAgBwF,SAAS;QACvBJ,OAAOwE,OAAOmC;QACdtG;QACAC,cAAcM,WAAWG,EAAE;QAC3BY;QACAsF;QACAxI,KAAKQ,OAAOR,GAAG;QACf6C,OAAO7F,wBAAwB8F,GAAG;IACpC;IAEF,OAAOuF;AACT"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type SiteConnectionRecordEntry } from './site-records';
|
|
18
|
+
/** The site's records, cached per site. A failed read shows none. */
|
|
19
|
+
export declare function cachedSiteConnectionRecords(hostId: string): Promise<SiteConnectionRecordEntry[]>;
|
|
20
|
+
/** The image hosts the site's signed plugins declare, cached per site. A failed read admits none. */
|
|
21
|
+
export declare function cachedSiteImageHosts(hostId: string): Promise<string[]>;
|
|
22
|
+
export declare const connectionRecordsEnricher: import("@aglyn/aglyn/src").SitePageEnricher;
|
|
23
|
+
export declare const connectionVariableComputer: import("@aglyn/aglyn/src/lib/plugin-manager/computed-variables").VariableComputer;
|
|
24
|
+
export declare const pluginImageHostsSource: import("@aglyn/aglyn/src/lib/plugin-manager/plugin-site-csp").PluginSiteCspSource;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { firebaseAdmin, getOrgForHost, getRealmPluginInstalls } from "@aglyn/tenant-data-admin";
|
|
17
|
+
import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
|
|
18
|
+
import { createConnectionRecordsEnricher, createConnectionVariableComputer, createPluginImageHostsSource, readSiteConnectionRecords, siteDeclaredImageHosts } from "./site-records.js";
|
|
19
|
+
/*
|
|
20
|
+
* The site-side readers of declared connections (AGL-4195), wired to the
|
|
21
|
+
* platform: the Admin SDK, the tenant's render cache, and the page loader's
|
|
22
|
+
* own read of the site's signed installs. Loaded with the first page that
|
|
23
|
+
* needs one of them, never with the boot.
|
|
24
|
+
*/ const firestore = ()=>firebaseAdmin.app().firestore();
|
|
25
|
+
/**
|
|
26
|
+
* As long as the page's own data, and dropped by the same tag: a publish, a
|
|
27
|
+
* plan change and an install change all announce `tenant-data:{hostId}`, and
|
|
28
|
+
* a sync that writes a record should too. A shorter TTL here would cap every
|
|
29
|
+
* page of the site at it (`render-cache.ts`).
|
|
30
|
+
*/ const SITE_CONNECTION_RECORDS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS;
|
|
31
|
+
/** As the site's signed installs are cached (`realm-plugins.ts`), which this reads. */ const SITE_IMAGE_HOSTS_TTL_SECONDS = 300;
|
|
32
|
+
const realmInstalls = (hostId)=>getRealmPluginInstalls({
|
|
33
|
+
hostId
|
|
34
|
+
});
|
|
35
|
+
/** The site's records, cached per site. A failed read shows none. */ export async function cachedSiteConnectionRecords(hostId) {
|
|
36
|
+
const read = ()=>readSiteConnectionRecords({
|
|
37
|
+
firestore: firestore(),
|
|
38
|
+
orgForHost: async (id)=>{
|
|
39
|
+
const found = await getOrgForHost(id);
|
|
40
|
+
return found ? {
|
|
41
|
+
orgId: found.orgId,
|
|
42
|
+
org: found.org
|
|
43
|
+
} : null;
|
|
44
|
+
}
|
|
45
|
+
}, hostId);
|
|
46
|
+
try {
|
|
47
|
+
return await withRenderCache({
|
|
48
|
+
key: [
|
|
49
|
+
'plugin-connection-records',
|
|
50
|
+
hostId
|
|
51
|
+
],
|
|
52
|
+
revalidate: SITE_CONNECTION_RECORDS_TTL_SECONDS,
|
|
53
|
+
tags: [
|
|
54
|
+
tenantDataTag(hostId)
|
|
55
|
+
],
|
|
56
|
+
read
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('plugin-connections: a site’s records were unreadable', error);
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The image hosts the site's signed plugins declare, cached per site. A failed read admits none. */ export async function cachedSiteImageHosts(hostId) {
|
|
64
|
+
const read = ()=>siteDeclaredImageHosts({
|
|
65
|
+
realmInstalls,
|
|
66
|
+
declaredImageHosts: async (listingId, version)=>(await firestore().collection('marketplaceListings').doc(listingId).collection('pluginVersions').doc(version).get()).get('manifest.capabilities.imageHosts')
|
|
67
|
+
}, hostId);
|
|
68
|
+
try {
|
|
69
|
+
return await withRenderCache({
|
|
70
|
+
key: [
|
|
71
|
+
'plugin-image-hosts',
|
|
72
|
+
hostId
|
|
73
|
+
],
|
|
74
|
+
revalidate: SITE_IMAGE_HOSTS_TTL_SECONDS,
|
|
75
|
+
tags: [
|
|
76
|
+
tenantDataTag(hostId)
|
|
77
|
+
],
|
|
78
|
+
read
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('plugin-connections: a site’s plugin image hosts were unreadable', error);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export const connectionRecordsEnricher = createConnectionRecordsEnricher({
|
|
86
|
+
realmInstalls,
|
|
87
|
+
records: cachedSiteConnectionRecords
|
|
88
|
+
});
|
|
89
|
+
export const connectionVariableComputer = createConnectionVariableComputer({
|
|
90
|
+
records: cachedSiteConnectionRecords
|
|
91
|
+
});
|
|
92
|
+
export const pluginImageHostsSource = createPluginImageHostsSource(cachedSiteImageHosts);
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=site-records-platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/site-records-platform.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { firebaseAdmin, getOrgForHost, getRealmPluginInstalls } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\nimport {\n createConnectionRecordsEnricher,\n createConnectionVariableComputer,\n createPluginImageHostsSource,\n readSiteConnectionRecords,\n siteDeclaredImageHosts,\n type SiteConnectionRecordEntry,\n} from './site-records'\n\n/*\n * The site-side readers of declared connections (AGL-4195), wired to the\n * platform: the Admin SDK, the tenant's render cache, and the page loader's\n * own read of the site's signed installs. Loaded with the first page that\n * needs one of them, never with the boot.\n */\n\nconst firestore = (): FirebaseFirestore.Firestore => firebaseAdmin.app().firestore()\n\n/**\n * As long as the page's own data, and dropped by the same tag: a publish, a\n * plan change and an install change all announce `tenant-data:{hostId}`, and\n * a sync that writes a record should too. A shorter TTL here would cap every\n * page of the site at it (`render-cache.ts`).\n */\nconst SITE_CONNECTION_RECORDS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/** As the site's signed installs are cached (`realm-plugins.ts`), which this reads. */\nconst SITE_IMAGE_HOSTS_TTL_SECONDS = 300\n\nconst realmInstalls = (hostId: string) => getRealmPluginInstalls({ hostId })\n\n/** The site's records, cached per site. A failed read shows none. */\nexport async function cachedSiteConnectionRecords(hostId: string): Promise<SiteConnectionRecordEntry[]> {\n const read = () =>\n readSiteConnectionRecords(\n {\n firestore: firestore(),\n orgForHost: async (id) => {\n const found = await getOrgForHost(id)\n return found ? { orgId: found.orgId, org: found.org as Record<string, unknown> } : null\n },\n },\n hostId,\n )\n try {\n return await withRenderCache({\n key: ['plugin-connection-records', hostId],\n revalidate: SITE_CONNECTION_RECORDS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s records were unreadable', error)\n return []\n }\n}\n\n/** The image hosts the site's signed plugins declare, cached per site. A failed read admits none. */\nexport async function cachedSiteImageHosts(hostId: string): Promise<string[]> {\n const read = () =>\n siteDeclaredImageHosts(\n {\n realmInstalls,\n declaredImageHosts: async (listingId, version) =>\n (\n await firestore()\n .collection('marketplaceListings')\n .doc(listingId)\n .collection('pluginVersions')\n .doc(version)\n .get()\n ).get('manifest.capabilities.imageHosts'),\n },\n hostId,\n )\n try {\n return await withRenderCache({\n key: ['plugin-image-hosts', hostId],\n revalidate: SITE_IMAGE_HOSTS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s plugin image hosts were unreadable', error)\n return []\n }\n}\n\nexport const connectionRecordsEnricher = createConnectionRecordsEnricher({\n realmInstalls,\n records: cachedSiteConnectionRecords,\n})\n\nexport const connectionVariableComputer = createConnectionVariableComputer({ records: cachedSiteConnectionRecords })\n\nexport const pluginImageHostsSource = createPluginImageHostsSource(cachedSiteImageHosts)\n"],"names":["firebaseAdmin","getOrgForHost","getRealmPluginInstalls","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","createConnectionRecordsEnricher","createConnectionVariableComputer","createPluginImageHostsSource","readSiteConnectionRecords","siteDeclaredImageHosts","firestore","app","SITE_CONNECTION_RECORDS_TTL_SECONDS","SITE_IMAGE_HOSTS_TTL_SECONDS","realmInstalls","hostId","cachedSiteConnectionRecords","read","orgForHost","id","found","orgId","org","key","revalidate","tags","error","console","cachedSiteImageHosts","declaredImageHosts","listingId","version","collection","doc","get","connectionRecordsEnricher","records","connectionVariableComputer","pluginImageHostsSource"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,EAAEC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AAC/F,SACEC,+BAA+B,EAC/BC,aAAa,EACbC,eAAe,QACV,wCAAuC;AAC9C,SACEC,+BAA+B,EAC/BC,gCAAgC,EAChCC,4BAA4B,EAC5BC,yBAAyB,EACzBC,sBAAsB,QAEjB,oBAAgB;AAEvB;;;;;CAKC,GAED,MAAMC,YAAY,IAAmCX,cAAcY,GAAG,GAAGD,SAAS;AAElF;;;;;CAKC,GACD,MAAME,sCAAsCV;AAE5C,qFAAqF,GACrF,MAAMW,+BAA+B;AAErC,MAAMC,gBAAgB,CAACC,SAAmBd,uBAAuB;QAAEc;IAAO;AAE1E,mEAAmE,GACnE,OAAO,eAAeC,4BAA4BD,MAAc;IAC9D,MAAME,OAAO,IACXT,0BACE;YACEE,WAAWA;YACXQ,YAAY,OAAOC;gBACjB,MAAMC,QAAQ,MAAMpB,cAAcmB;gBAClC,OAAOC,QAAQ;oBAAEC,OAAOD,MAAMC,KAAK;oBAAEC,KAAKF,MAAME,GAAG;gBAA4B,IAAI;YACrF;QACF,GACAP;IAEJ,IAAI;QACF,OAAO,MAAMX,gBAAgB;YAC3BmB,KAAK;gBAAC;gBAA6BR;aAAO;YAC1CS,YAAYZ;YACZa,MAAM;gBAACtB,cAAcY;aAAQ;YAC7BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,wDAAwDA;QACtE,OAAO,EAAE;IACX;AACF;AAEA,mGAAmG,GACnG,OAAO,eAAeE,qBAAqBb,MAAc;IACvD,MAAME,OAAO,IACXR,uBACE;YACEK;YACAe,oBAAoB,OAAOC,WAAWC,UACpC,AACE,CAAA,MAAMrB,YACHsB,UAAU,CAAC,uBACXC,GAAG,CAACH,WACJE,UAAU,CAAC,kBACXC,GAAG,CAACF,SACJG,GAAG,EAAC,EACPA,GAAG,CAAC;QACV,GACAnB;IAEJ,IAAI;QACF,OAAO,MAAMX,gBAAgB;YAC3BmB,KAAK;gBAAC;gBAAsBR;aAAO;YACnCS,YAAYX;YACZY,MAAM;gBAACtB,cAAcY;aAAQ;YAC7BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,mEAAmEA;QACjF,OAAO,EAAE;IACX;AACF;AAEA,OAAO,MAAMS,4BAA4B9B,gCAAgC;IACvES;IACAsB,SAASpB;AACX,GAAE;AAEF,OAAO,MAAMqB,6BAA6B/B,iCAAiC;IAAE8B,SAASpB;AAA4B,GAAE;AAEpH,OAAO,MAAMsB,yBAAyB/B,6BAA6BqB,sBAAqB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type PluginConnectionDeclaration } from '@aglyn/aglyn/app-utils/plugin-connections';
|
|
18
|
+
import { type PluginConnectionSiteRecord } from '@aglyn/aglyn/app-utils/plugin-connection-records';
|
|
19
|
+
import type { HostVariable } from '@aglyn/aglyn/app-utils/variables';
|
|
20
|
+
import type { VariableComputer } from '@aglyn/aglyn/plugin-manager/computed-variables';
|
|
21
|
+
import type { PluginSiteCspSource } from '@aglyn/aglyn/plugin-manager/plugin-site-csp';
|
|
22
|
+
import type { RealmPluginInstall } from '@aglyn/aglyn/plugin-manager/realm-plugins';
|
|
23
|
+
import type { SitePageEnricher } from '@aglyn/aglyn/plugin-manager/site-page-hooks';
|
|
24
|
+
/** Records read for one site at most: four connections a plugin, a page of plugins. */
|
|
25
|
+
export declare const SITE_CONNECTION_RECORDS_READ = 40;
|
|
26
|
+
/** One connection's record on a site, as the page and the variables read it. */
|
|
27
|
+
export interface SiteConnectionRecordEntry {
|
|
28
|
+
listingId: string;
|
|
29
|
+
connectionId: string;
|
|
30
|
+
record: PluginConnectionSiteRecord;
|
|
31
|
+
variables: Record<string, HostVariable>;
|
|
32
|
+
}
|
|
33
|
+
/** What reading a site's records needs: the store and the site's workspace. */
|
|
34
|
+
export interface SiteConnectionRecordsReadDeps {
|
|
35
|
+
firestore: FirebaseFirestore.Firestore;
|
|
36
|
+
/** The site's workspace and its document, or `null` for an unindexed site. */
|
|
37
|
+
orgForHost(hostId: string): Promise<{
|
|
38
|
+
orgId: string;
|
|
39
|
+
org: Record<string, unknown>;
|
|
40
|
+
} | null>;
|
|
41
|
+
}
|
|
42
|
+
/** A stored record, held to what `connection` declares, or `null` when it is not one. */
|
|
43
|
+
export declare function toSiteRecord(data: Record<string, unknown> | undefined, connection: PluginConnectionDeclaration): PluginConnectionSiteRecord | null;
|
|
44
|
+
/**
|
|
45
|
+
* Every record the site's installed plugins may show, read uncached.
|
|
46
|
+
*
|
|
47
|
+
* One query when the site has none — the case for nearly every site — and
|
|
48
|
+
* the install pins and the workspace only when it has some.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readSiteConnectionRecords(deps: SiteConnectionRecordsReadDeps, hostId: string): Promise<SiteConnectionRecordEntry[]>;
|
|
51
|
+
export interface ConnectionRecordsEnricherDeps {
|
|
52
|
+
/** The site's signed (realm) installs, as the page loader reads them. */
|
|
53
|
+
realmInstalls(hostId: string): Promise<readonly RealmPluginInstall[]>;
|
|
54
|
+
/** The site's records ({@link readSiteConnectionRecords}, cached). */
|
|
55
|
+
records(hostId: string): Promise<readonly SiteConnectionRecordEntry[]>;
|
|
56
|
+
}
|
|
57
|
+
type PageNode = {
|
|
58
|
+
$id?: unknown;
|
|
59
|
+
componentId?: unknown;
|
|
60
|
+
} | null | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Node id → listing id, for every element on the page in a signed install's
|
|
63
|
+
* namespace (`<identity>.<role>`). A node is attributed to the install whose
|
|
64
|
+
* identity is the LONGEST prefix of its component id, so one identity being a
|
|
65
|
+
* prefix of another (`acme.reviews` and `acme.reviews.pro`) cannot hand an
|
|
66
|
+
* element the other's records.
|
|
67
|
+
*/
|
|
68
|
+
export declare function signedElementOwners(nodes: Record<string, PageNode> | null | undefined, installs: readonly RealmPluginInstall[]): Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* The enricher. A page with no element in a marketplace namespace — nearly
|
|
71
|
+
* every page — returns before reading anything.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createConnectionRecordsEnricher(deps: ConnectionRecordsEnricherDeps): SitePageEnricher;
|
|
74
|
+
/**
|
|
75
|
+
* Adds each record's summary-field variables to the site's own. A site's own
|
|
76
|
+
* variable is never replaced: the ids cannot collide by construction
|
|
77
|
+
* (`pluginConnectionVariableId`), and this holds even if one somehow did.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createConnectionVariableComputer(deps: Pick<ConnectionRecordsEnricherDeps, 'records'>): VariableComputer;
|
|
80
|
+
export interface PluginImageHostsDeps {
|
|
81
|
+
realmInstalls(hostId: string): Promise<readonly RealmPluginInstall[]>;
|
|
82
|
+
/** The `capabilities.imageHosts` the pinned version's manifest declares, unvalidated. */
|
|
83
|
+
declaredImageHosts(listingId: string, version: string): Promise<unknown>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The image hosts a site's signed plugins declare, each re-validated, in
|
|
87
|
+
* install order, de-duplicated. A signed (realm) install is the only kind
|
|
88
|
+
* whose elements draw into the page itself; a sandboxed plugin draws in a
|
|
89
|
+
* frame of its own origin, which the page's `img-src` does not govern. A
|
|
90
|
+
* declaration that no longer validates admits nothing at all.
|
|
91
|
+
*/
|
|
92
|
+
export declare function siteDeclaredImageHosts(deps: PluginImageHostsDeps, hostId: string): Promise<string[]>;
|
|
93
|
+
/** The CSP source the tenant's verdict asks. */
|
|
94
|
+
export declare function createPluginImageHostsSource(hosts: (hostId: string) => Promise<string[]>): PluginSiteCspSource;
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
|
+
import { pluginConnectionRecordVariables, validatePluginImageHosts } from "@aglyn/aglyn/app-utils/plugin-connections";
|
|
19
|
+
import { PLUGIN_CONNECTION_RECORDS_PROP } from "@aglyn/aglyn/app-utils/plugin-connection-records";
|
|
20
|
+
import { PLUGIN_CONNECTIONS_FEATURE, pluginConnectionRecordDocId } from "../../model/plugin-connections.js";
|
|
21
|
+
import { connectionsCollection } from "./connection-store.js";
|
|
22
|
+
import { installedConnectionsForHost } from "./installed-connection.js";
|
|
23
|
+
/*
|
|
24
|
+
* WHAT A SITE SHOWS OF ITS PLUGINS' DECLARED CONNECTIONS (AGL-4195).
|
|
25
|
+
*
|
|
26
|
+
* The sync (AGL-4194) writes, per site, the record each connection's
|
|
27
|
+
* declaration maps (`hosts/{hostId}/pluginConnections/record-…`). That
|
|
28
|
+
* collection is server-only, so the published page reads it here, in three
|
|
29
|
+
* places, all on the server:
|
|
30
|
+
*
|
|
31
|
+
* - the PAGE ENRICHER hands each signed element on the page its own
|
|
32
|
+
* listing's records, as one page prop, so the element renders in the
|
|
33
|
+
* page's HTML (`usePluginConnectionRecord` in core looks it up);
|
|
34
|
+
* - the VARIABLE COMPUTER turns each record's declared summary fields into
|
|
35
|
+
* site variables a page binds as `{{var:…}}`;
|
|
36
|
+
* - the CSP SOURCE names the image hosts the site's signed plugins declare
|
|
37
|
+
* (`capabilities.imageHosts`), so the tenant admits them to `img-src` on
|
|
38
|
+
* the sites that run those plugins and nowhere else.
|
|
39
|
+
*
|
|
40
|
+
* A record is exposed only while the plugin is still installed at a version
|
|
41
|
+
* that declares the connection (not revoked), and only on a plan with the
|
|
42
|
+
* connections feature. What is exposed is held to the declaration again:
|
|
43
|
+
* the record's keys are filtered to the declared ones, so a record written
|
|
44
|
+
* under an older version's schema cannot show a field the pinned version no
|
|
45
|
+
* longer declares.
|
|
46
|
+
*/ /** Records read for one site at most: four connections a plugin, a page of plugins. */ export const SITE_CONNECTION_RECORDS_READ = 40;
|
|
47
|
+
const isPrimitive = (value)=>value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value);
|
|
48
|
+
/** `source` held to the declared keys: anything else, or a non-primitive, is dropped. */ function declaredOnly(source, declared) {
|
|
49
|
+
const out = {};
|
|
50
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return out;
|
|
51
|
+
for (const key of Object.keys(declared)){
|
|
52
|
+
const value = source[key];
|
|
53
|
+
if (value !== undefined && isPrimitive(value)) out[key] = value;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/** A stored record, held to what `connection` declares, or `null` when it is not one. */ export function toSiteRecord(data, connection) {
|
|
58
|
+
var _ref, _schema_fields;
|
|
59
|
+
var _connection_sync, _schema_items;
|
|
60
|
+
const schema = (_connection_sync = connection.sync) == null ? void 0 : _connection_sync.record;
|
|
61
|
+
if (!data || data['kind'] !== 'record' || !schema) return null;
|
|
62
|
+
const syncedAtMs = Number(data['syncedAtMs']);
|
|
63
|
+
const items = Array.isArray(data['items']) ? data['items'] : [];
|
|
64
|
+
const itemFields = (_ref = (_schema_items = schema.items) == null ? void 0 : _schema_items.fields) != null ? _ref : {};
|
|
65
|
+
return {
|
|
66
|
+
fields: declaredOnly(data['fields'], (_schema_fields = schema.fields) != null ? _schema_fields : {}),
|
|
67
|
+
items: schema.items ? items.slice(0, schema.items.maxItems).map((item)=>declaredOnly(item, itemFields)) : [],
|
|
68
|
+
syncedAtMs: Number.isFinite(syncedAtMs) ? syncedAtMs : 0
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Every record the site's installed plugins may show, read uncached.
|
|
73
|
+
*
|
|
74
|
+
* One query when the site has none — the case for nearly every site — and
|
|
75
|
+
* the install pins and the workspace only when it has some.
|
|
76
|
+
*/ export async function readSiteConnectionRecords(deps, hostId) {
|
|
77
|
+
const snapshot = await connectionsCollection(deps.firestore, hostId).where('kind', '==', 'record').limit(SITE_CONNECTION_RECORDS_READ).get();
|
|
78
|
+
if (snapshot.empty) return [];
|
|
79
|
+
const owner = await deps.orgForHost(hostId);
|
|
80
|
+
// A site with no workspace has no plan to hold the feature (`checkEntitlement`
|
|
81
|
+
// fails open on a missing org, which is right for a page and wrong here).
|
|
82
|
+
if (!owner || !checkEntitlement(owner.org, PLUGIN_CONNECTIONS_FEATURE)) return [];
|
|
83
|
+
const installed = await installedConnectionsForHost(deps.firestore, {
|
|
84
|
+
hostId,
|
|
85
|
+
orgId: owner.orgId
|
|
86
|
+
});
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const doc of snapshot.docs){
|
|
89
|
+
var _data_listingId, _data_connectionId;
|
|
90
|
+
var _installed_find;
|
|
91
|
+
const data = doc.data();
|
|
92
|
+
const listingId = String((_data_listingId = data['listingId']) != null ? _data_listingId : '');
|
|
93
|
+
const connectionId = String((_data_connectionId = data['connectionId']) != null ? _data_connectionId : '');
|
|
94
|
+
// The id is the contract (`record-{listingId}__{connectionId}`); a record
|
|
95
|
+
// whose body names another connection than its id is not trusted.
|
|
96
|
+
if (!listingId || !connectionId || doc.id !== pluginConnectionRecordDocId(listingId, connectionId)) continue;
|
|
97
|
+
const connection = (_installed_find = installed.find((plugin)=>plugin.listingId === listingId)) == null ? void 0 : _installed_find.connections.find((declared)=>declared.id === connectionId);
|
|
98
|
+
if (!connection) continue;
|
|
99
|
+
const record = toSiteRecord(data, connection);
|
|
100
|
+
if (!record) continue;
|
|
101
|
+
out.push({
|
|
102
|
+
listingId,
|
|
103
|
+
connectionId,
|
|
104
|
+
record,
|
|
105
|
+
variables: pluginConnectionRecordVariables(listingId, connection, record.fields)
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Node id → listing id, for every element on the page in a signed install's
|
|
112
|
+
* namespace (`<identity>.<role>`). A node is attributed to the install whose
|
|
113
|
+
* identity is the LONGEST prefix of its component id, so one identity being a
|
|
114
|
+
* prefix of another (`acme.reviews` and `acme.reviews.pro`) cannot hand an
|
|
115
|
+
* element the other's records.
|
|
116
|
+
*/ export function signedElementOwners(nodes, installs) {
|
|
117
|
+
const owners = {};
|
|
118
|
+
if (!nodes || typeof nodes !== 'object') return owners;
|
|
119
|
+
const namespaces = installs.filter((install)=>typeof install.identity === 'string' && install.identity).map((install)=>({
|
|
120
|
+
prefix: `${install.identity}.`,
|
|
121
|
+
listingId: install.listingId
|
|
122
|
+
})).sort((a, b)=>b.prefix.length - a.prefix.length);
|
|
123
|
+
if (!namespaces.length) return owners;
|
|
124
|
+
for (const [key, node] of Object.entries(nodes)){
|
|
125
|
+
const componentId = node == null ? void 0 : node.componentId;
|
|
126
|
+
if (typeof componentId !== 'string' || !componentId.includes('.')) continue;
|
|
127
|
+
const owner = namespaces.find((namespace)=>componentId.startsWith(namespace.prefix));
|
|
128
|
+
if (!owner) continue;
|
|
129
|
+
const nodeId = typeof (node == null ? void 0 : node.$id) === 'string' && node.$id ? node.$id : key;
|
|
130
|
+
owners[nodeId] = owner.listingId;
|
|
131
|
+
}
|
|
132
|
+
return owners;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The enricher. A page with no element in a marketplace namespace — nearly
|
|
136
|
+
* every page — returns before reading anything.
|
|
137
|
+
*/ export function createConnectionRecordsEnricher(deps) {
|
|
138
|
+
return async (context)=>{
|
|
139
|
+
const nodes = context.nodes;
|
|
140
|
+
if (!nodes || typeof nodes !== 'object') return undefined;
|
|
141
|
+
const anyNamespaced = Object.values(nodes).some((node)=>typeof (node == null ? void 0 : node.componentId) === 'string' && node.componentId.includes('.'));
|
|
142
|
+
if (!anyNamespaced) return undefined;
|
|
143
|
+
const owners = signedElementOwners(nodes, await deps.realmInstalls(context.hostId));
|
|
144
|
+
const listings = new Set(Object.values(owners));
|
|
145
|
+
if (!listings.size) return undefined;
|
|
146
|
+
const records = {};
|
|
147
|
+
for (const entry of (await deps.records(context.hostId))){
|
|
148
|
+
var _records, _entry_listingId, _;
|
|
149
|
+
if (!listings.has(entry.listingId)) continue;
|
|
150
|
+
((_ = (_records = records)[_entry_listingId = entry.listingId]) != null ? _ : _records[_entry_listingId] = {})[entry.connectionId] = entry.record;
|
|
151
|
+
}
|
|
152
|
+
const attributed = {};
|
|
153
|
+
for (const [nodeId, listingId] of Object.entries(owners)){
|
|
154
|
+
if (records[listingId]) attributed[nodeId] = listingId;
|
|
155
|
+
}
|
|
156
|
+
if (!Object.keys(attributed).length) return undefined;
|
|
157
|
+
const data = {
|
|
158
|
+
nodes: attributed,
|
|
159
|
+
records
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
[PLUGIN_CONNECTION_RECORDS_PROP]: data
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/* ------------------------------------------------------------------ */ /* The variable computer */ /* ------------------------------------------------------------------ */ const unchanged = ({ variables })=>variables;
|
|
167
|
+
/**
|
|
168
|
+
* Adds each record's summary-field variables to the site's own. A site's own
|
|
169
|
+
* variable is never replaced: the ids cannot collide by construction
|
|
170
|
+
* (`pluginConnectionVariableId`), and this holds even if one somehow did.
|
|
171
|
+
*/ export function createConnectionVariableComputer(deps) {
|
|
172
|
+
return {
|
|
173
|
+
prepare: async (hostId)=>{
|
|
174
|
+
const entries = await deps.records(hostId);
|
|
175
|
+
const computed = {};
|
|
176
|
+
for (const entry of entries)Object.assign(computed, entry.variables);
|
|
177
|
+
if (!Object.keys(computed).length) return unchanged;
|
|
178
|
+
return ({ variables })=>{
|
|
179
|
+
const out = _extends({}, variables);
|
|
180
|
+
for (const [id, variable] of Object.entries(computed)){
|
|
181
|
+
if (!Object.prototype.hasOwnProperty.call(variables, id)) out[id] = variable;
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The image hosts a site's signed plugins declare, each re-validated, in
|
|
190
|
+
* install order, de-duplicated. A signed (realm) install is the only kind
|
|
191
|
+
* whose elements draw into the page itself; a sandboxed plugin draws in a
|
|
192
|
+
* frame of its own origin, which the page's `img-src` does not govern. A
|
|
193
|
+
* declaration that no longer validates admits nothing at all.
|
|
194
|
+
*/ export async function siteDeclaredImageHosts(deps, hostId) {
|
|
195
|
+
const installs = await deps.realmInstalls(hostId);
|
|
196
|
+
if (!installs.length) return [];
|
|
197
|
+
const declared = await Promise.all(installs.map(async (install)=>{
|
|
198
|
+
try {
|
|
199
|
+
const verdict = validatePluginImageHosts(await deps.declaredImageHosts(install.listingId, install.version));
|
|
200
|
+
return verdict.ok ? verdict.value : [];
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(`plugin-connections: the image hosts of ${install.listingId} were unreadable`, error);
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}));
|
|
206
|
+
return [
|
|
207
|
+
...new Set(declared.flat())
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
/** The CSP source the tenant's verdict asks. */ export function createPluginImageHostsSource(hosts) {
|
|
211
|
+
return {
|
|
212
|
+
hosts: async ({ hostId })=>{
|
|
213
|
+
const img = await hosts(hostId);
|
|
214
|
+
return img.length ? {
|
|
215
|
+
img
|
|
216
|
+
} : {};
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
//# sourceMappingURL=site-records.js.map
|