import type { PluginBridgeAction, PluginBridgeExecuteEnvelope, PluginBridgeExecutionResult, PluginBridgeHostContext, PluginBridgeManifestContract, PluginBridgeThemeTokens, PluginPermission } from "../contracts/pluginBridge"; import type { PlatformApiClient } from "../api/client"; export function createPluginBridgeHostContext(input: { plugin: PluginBridgeManifestContract; routeKey: string; serverInstanceId?: string; themeTokens: PluginBridgeThemeTokens; }): PluginBridgeHostContext { const page = input.plugin.pages.find((candidate) => candidate.key === input.routeKey); const permissions = filterAllowedPermissions(input.plugin.declaredPermissions, page?.permissions); const bridgeActions = page?.bridgeActions === undefined ? [...input.plugin.bridgeActions] : page.bridgeActions.filter((action) => input.plugin.bridgeActions.includes(action)); return { pluginId: input.plugin.id, routeKey: input.routeKey, serverInstanceId: input.serverInstanceId, themeTokens: input.themeTokens, permissions, bridgeActions, aiPurposes: [...(input.plugin.aiPurposes ?? [])] }; } export function filterAllowedPermissions( declaredPermissions: PluginPermission[], pagePermissions: PluginPermission[] | undefined ): PluginPermission[] { if (pagePermissions === undefined) { return [...declaredPermissions]; } return pagePermissions.filter((permission) => declaredPermissions.includes(permission)); } export function createPluginBridgeDispatcher(context: PluginBridgeHostContext, client: Pick) { return async function dispatchBridgeRequest(envelope: PluginBridgeExecuteEnvelope, signal?: AbortSignal): Promise { const localError = validateBridgeExecutionRequest(context, envelope); if (localError) { return { requestId: envelope.requestId, action: envelope.action, status: "denied", error: localError }; } if (signal?.aborted) { return { requestId: envelope.requestId, action: envelope.action, status: "cancelled", error: { code: "cancelled", message: "bridge request was cancelled" } }; } try { const response = await client.executePluginBridge({ requestId: envelope.requestId, pluginId: context.pluginId, routeKey: context.routeKey, serverInstanceId: context.serverInstanceId, action: envelope.action, aiPurpose: envelope.aiPurpose, payload: envelope.payload }); return { requestId: response.requestId, action: response.action as PluginBridgeAction, status: response.status, result: response.result, error: response.error }; } catch (error) { return { requestId: envelope.requestId, action: envelope.action, status: "error", error: { code: "platform_error", message: error instanceof Error ? error.message : "bridge execution failed" } }; } }; } export function validateBridgeExecutionRequest(context: PluginBridgeHostContext, envelope: PluginBridgeExecuteEnvelope): PluginBridgeExecutionResult["error"] | null { if (!envelope.requestId.trim()) { return { code: "validation", message: "requestId is required" }; } if (!context.bridgeActions.includes(envelope.action)) { return { code: "unsupported_action", message: "bridge action is not allowed for this page" }; } for (const permission of requiredPermissions(envelope.action)) { if (!context.permissions.includes(permission)) { return { code: "permission_denied", message: "bridge action is missing required permission" }; } } if (envelope.action === "ai.invoke" && (!envelope.aiPurpose || !context.aiPurposes.includes(envelope.aiPurpose))) { return { code: "ai_purpose_denied", message: "AI purpose is not allowed for this plugin" }; } const payload = envelope.payload ?? {}; if (Object.keys(payload).length > 16) { return { code: "payload_too_large", message: "bridge payload has too many keys" }; } const encodedSize = Object.entries(payload).reduce((sum, [key, value]) => sum + key.length + value.length, 0); if (encodedSize > 4096) { return { code: "payload_too_large", message: "bridge payload is too large" }; } for (const [key, value] of Object.entries(payload)) { if (!key.trim() || key !== key.trim()) { return { code: "validation", message: "bridge payload key is invalid" }; } if (containsUnsafeBridgeContent(key) || containsUnsafeBridgeContent(value)) { return { code: "unsafe_payload", message: "bridge payload contains unsafe content" }; } } return null; } export interface PluginArtifactReference { artifactId: string; filename: string; contentType: string; sizeBytes: number; checksum: string; downloadUrl: string; expiresAt: string; rangeSupported: boolean; chunkSizeBytes: number; storageBehavior?: string; } export function parsePluginArtifactReference(result: Record | undefined): PluginArtifactReference | null { if (!result) { return null; } const sizeBytes = Number(result.sizeBytes); const chunkSizeBytes = Number(result.chunkSizeBytes); const reference: PluginArtifactReference = { artifactId: result.artifactId ?? "", filename: result.filename ?? "artifact.bin", contentType: result.contentType ?? "application/octet-stream", sizeBytes, checksum: result.checksum ?? "", downloadUrl: result.downloadUrl ?? "", expiresAt: result.expiresAt ?? "", rangeSupported: result.rangeSupported === "true", chunkSizeBytes, storageBehavior: result.storageBehavior }; if (!reference.artifactId || !Number.isFinite(sizeBytes) || sizeBytes <= 0 || !Number.isFinite(chunkSizeBytes) || chunkSizeBytes <= 0) { return null; } if (!reference.downloadUrl.startsWith(`/api/v1/artifacts/${encodeURIComponent(reference.artifactId)}/content`)) { return null; } for (const value of Object.values(reference)) { if (typeof value === "string" && containsUnsafeBridgeContent(value)) { return null; } } return reference; } function requiredPermissions(action: PluginBridgeAction): PluginPermission[] { switch (action) { case "server.instances.read": return ["server.read"]; case "jobs.dispatch": return ["server.lifecycle"]; case "logs.query": return ["server.logs.read"]; case "artifacts.open": return ["server.artifacts.read"]; case "files.request": return ["server.files.read"]; case "ai.invoke": return ["ai.invoke"]; default: return []; } } function containsUnsafeBridgeContent(value: string): boolean { const lowered = value.trim().toLowerCase(); if (!lowered) { return false; } return ( lowered.includes("/users/") || lowered.includes("/private/") || lowered.includes("unix://") || lowered.includes("tcp://") || lowered.includes("bearer ") || lowered.startsWith("sk-") || lowered.includes("password=") || lowered.includes("api_key=") || lowered.includes("apikey=") || lowered.includes("host path") || lowered.includes("rawapikey") ); }