/** * Host side of the RPC channel. The host owns the plugin's iframe, answers the * plugin's capability calls (filtered against the manifest `permissions`), and * drives the plugin's lifecycle methods. */ import { capabilityForMethod, METHOD_CAPABILITY, PLUGIN_LIFECYCLE_METHODS } from './capabilities'; import type { PluginLifecycleMethod } from './capabilities'; import type { PluginManifest } from './manifest'; import { createRpcEndpoint, RpcErrorObject, type RpcEndpoint, type RpcHandler, type RpcRequestOptions, type RpcTransport, } from './rpc'; /** * The host's implementations of the plugin API methods, keyed by method name * (`getContent`, `listPages`, …). Each is executed against the viewing user's * session by the host; only methods whose capability the plugin declared are * ever reached (the router rejects the rest before calling in). */ export type HostCapabilityHandlers = Partial>; /** A capability call the gate refused, reported before the caller is told. */ export interface CapabilityViolation { method: string; capability?: string; code: 'unknown_method' | 'capability_not_permitted'; } export interface HostBridgeOptions { manifest: Pick; /** Host implementations of the plugin API methods. */ capabilities: HostCapabilityHandlers; transport: RpcTransport; /** Default timeout for host→plugin lifecycle calls, in ms. */ timeoutMs?: number; /** Called when the gate refuses a call (undeclared capability or unknown * method) — hosts log these; a misbehaving plugin must leave a trace. */ onViolation?: (violation: CapabilityViolation) => void; } export interface HostBridge { /** Invokes a plugin lifecycle method (`render`, `edit`, `destroy`). */ invoke: ( method: PluginLifecycleMethod, params?: unknown, options?: RpcRequestOptions, ) => Promise; /** Tears down the channel and rejects any in-flight lifecycle calls. */ dispose: () => void; /** The underlying endpoint, exposed for advanced host integrations. */ endpoint: RpcEndpoint; } /** * Builds the host end of the RPC channel for one plugin surface. Incoming * capability calls are routed through a permission gate: a method is answered * only if it belongs to a v1 capability, that capability is declared in the * manifest, and the host actually implements it — otherwise the caller gets a * `capability_not_permitted` or `unknown_method` rejection. */ export function createHostBridge(options: HostBridgeOptions): HostBridge { const declared = new Set(options.manifest.permissions); const endpoint = createRpcEndpoint({ ...options.transport, timeoutMs: options.timeoutMs, }); // Install one gated handler per known API method. The gate resolves the // capability, checks the manifest declared it, then delegates to the host // implementation (if any). const gate = (method: string): RpcHandler => { return async (params) => { const capability = capabilityForMethod(method); if (!capability) { options.onViolation?.({ method, code: 'unknown_method' }); throw new RpcErrorObject({ code: 'unknown_method', message: `"${method}" is not a plugin API method`, }); } if (!declared.has(capability)) { options.onViolation?.({ method, capability, code: 'capability_not_permitted' }); throw new RpcErrorObject({ code: 'capability_not_permitted', message: `plugin did not declare capability "${capability}" required for "${method}"`, }); } const impl = options.capabilities[method]; if (!impl) { throw new RpcErrorObject({ code: 'unknown_method', message: `host does not implement "${method}"`, }); } return impl(params); }; }; // Register a gated handler for every v1 API method — not just the ones the // host implements. That way an *undeclared* call is answered with // `capability_not_permitted` (and reported via `onViolation`) even when the // host has no implementation for it; only methods outside the v1 surface // fall through to the endpoint's plain `unknown_method` response. for (const method of Object.keys(METHOD_CAPABILITY)) { endpoint.setHandler(method, gate(method)); } return { invoke: (method, params, requestOptions) => { if (!PLUGIN_LIFECYCLE_METHODS.includes(method)) { return Promise.reject( new RpcErrorObject({ code: 'unknown_method', message: `"${method}" is not a plugin lifecycle method`, }), ); } return endpoint.request(method, params, requestOptions); }, dispose: () => endpoint.dispose(), endpoint, }; }