Plugin UI Bridge
@latticenet/plugin-bridge is the single bridge client every Lattice plugin UI consumes. It replaces the per-plugin bridge.ts copies, so one reviewed implementation sets the security bar for all of them.
Why a package
A plugin UI ships as static assets inside the plugin's signed bundle and runs in an opaque-origin sandbox="allow-scripts" iframe served with default-src 'none'; … connect-src 'none'. The document cannot fetch anything: its only channel is postMessage to the dashboard host, and the host rejects any service/method the signed manifest does not declare. The bridge client is that channel — handshake, origin validation, call transport, error mapping — and it must fail closed everywhere, in every plugin, identically.
Usage
import { BridgeClient, canCall } from "@latticenet/plugin-bridge";
const bridge = new BridgeClient({
window,
expectedPluginId: "latticenet.example", // your signed manifest id
expectedRoutes: ["example"], // your manifest ui.views routes
idPrefix: "example", // optional call-id prefix
});
const init = await bridge.init;
if (canCall(init, "latticenet.example/items", "list")) {
const { promise, cancel } = bridge.call("latticenet.example/items", "list", {});
const result = await promise;
}
bridge.resize(document.documentElement.scrollHeight);Security invariants
All of the following fail closed and are covered by package tests:
- The frame URL fragment must carry
lattice_nonce(16–128 chars) and ahost_originthat reparses to exactly itself as an absolute http(s) origin. Absence, malformed values, and non-exact origins throwBridgeHandshakeErrorin the constructor — never a wildcard fallback. - Inbound messages must match the nonce, the pinned origin exactly, and
event.source === window.parent, in that order. lattice.host.initmust declare protocolversion: "1", your plugin id, and one of your registered view routes.- Host design tokens are filtered to a fixed CSS-variable allowlist before they touch the document.
- Every frame reload is a new trust boundary: the host re-keys the element and mints a fresh nonce.
Typed errors
BridgeError subclasses let UIs distinguish failure classes without string matching: BridgeHandshakeError (invalid channel), BridgeRemoteError (host answered an error; carries the wire code when supplied, e.g. scope denials), BridgeCancelledError (cancel()), BridgeTimeoutError (per-call timeout, default 15 s), BridgeDisposedError (frame torn down).
Migrating a plugin off its local copy
- Depend on the exact published version, delete
ui/src/bridge.tsand its test (the package's invariant suite replaces them). - Construct with the options object above; the local copy's hard-coded plugin id and routes become constructor arguments.
- Keep the UI's
test/typecheck/build/verify:buildentries green.
The protocol itself is versioned in the Lattice API contract (contract/api-contract.md §1, bridge protocol v1); changes there are contract changes, not package refactors.
Releases
Prerelease lane 0.x-alpha.N, published to GitHub Packages by the lattice-plugin-bridge repository's tag-triggered CI. Consumers pin exact versions; a prerelease never becomes the registry's latest. First published version: 0.1.0-alpha.1. GitHub Packages requires an authenticated token for installs — see the package README's Consuming section for the .npmrc shape.