packages/plugin-sdk/protocol.ts
Rama main · réplica de lectura del repositorio original.
/**
* The bridge wire format, shared in spirit by the surface (this package) and
* the host (`packages/views/plugins`). It is duplicated rather than imported:
* the SDK ships to plugin authors and must not pull `@multica/core` or
* `@multica/ui` in behind it, and the host must not depend on a package a
* third party could install a different version of.
*
* Two properties matter more than the shape:
*
* - Identity is bound by a MessagePort, not by `event.origin`. A surface runs
* in a sandboxed iframe with no `allow-same-origin`, so its origin is the
* opaque string "null" — every surface would look identical. The generated
* guest bootstrap creates one channel, and the host accepts its peer only
* after source, protocol and single-use launch checks. From then on, "which
* plugin sent this" is answered by which private channel it arrived on.
* - No credential crosses this boundary. The surface asks; the host performs
* the call on the user's session and returns the result.
*/
export const BRIDGE_PROTOCOL_VERSION = 2;
/**
* Installed by Multica's bootstrap before any plugin code executes. The guest
* creates this port; the trusted wrapper transfers its peer to the host only
* after the launch challenge and navigation policy have passed.
*/
export const BRIDGE_PORT_GLOBAL = "__multicaPluginBridgePortV2";
/** Design tokens the host pushes so a surface matches the product it sits in. */
export type ThemeTokens = Record<string, string>;
export type BridgeMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
export type BridgeRequest =
| {
id: string;
kind: "action";
method: BridgeMethod;
/** Path under the plugin Action API, e.g. "/issues/{id}/comments". */
path: string;
body?: unknown;
}
| {
id: string;
kind: "ui.resize";
height: number;
};
export type BridgeResponse =
| { id: string; ok: true; status: number; data: unknown }
| { id: string; ok: false; status: number; error: string };
/** Host-initiated, unsolicited: theme changes while the surface is mounted. */
export type BridgeEvent = { kind: "theme"; theme: ThemeTokens };
export type BridgeMessage = BridgeResponse | BridgeEvent;
export function isBridgeResponse(message: unknown): message is BridgeResponse {
if (typeof message !== "object" || message === null) return false;
const candidate = message as Record<string, unknown>;
return typeof candidate.id === "string" && typeof candidate.ok === "boolean";
}
export function isBridgeEvent(message: unknown): message is BridgeEvent {
if (typeof message !== "object" || message === null) return false;
return (message as Record<string, unknown>).kind === "theme";
}
export function isBridgeRequest(message: unknown): message is BridgeRequest {
if (typeof message !== "object" || message === null) return false;
const candidate = message as Record<string, unknown>;
if (typeof candidate.id !== "string") return false;
if (candidate.kind === "ui.resize") return typeof candidate.height === "number";
return candidate.kind === "action" && typeof candidate.path === "string" && typeof candidate.method === "string";
}