9.2 KiB
Plugin SDK Reference
@hivegrid/plugin-sdk— the typed surface plugin authors compile against. Mirrors the runtime capability set so an SDK upgrade is a straight version bump on the author side.
Installing
The SDK lives in the host monorepo today as workspace:*. External
plugin authors install it as a regular npm dependency:
pnpm add -D @hivegrid/plugin-sdk
Top-level exports
| Export | Kind | Purpose |
|---|---|---|
defineHook |
fn | Identity wrapper that captures Input/Output types at the call site |
createPluginSettingsClient |
fn | Settings iframe helper around the host postMessage protocol |
HookHandler |
type | (input, ctx) => Output | Promise<Output> |
PluginContext |
type | Per-invocation context (logger, capabilities, tenant info) |
HostApi |
type | Aggregate of optional bindings; only the declared ones are bound |
PluginCapabilityName |
type | Closed enum of capabilities |
PluginLogger |
type | Pino-subset (info/warn/error) |
PluginHttpClient |
type | http.outbound capability surface |
PluginStorage |
type | storage.read / storage.write capability surface |
PluginAudit |
type | audit capability surface; production-available but still capability-gated |
PluginTenantInfo |
type | Read-only tenant metadata (tenant.info) |
PluginSettingsClient |
type | Settings iframe helper returned by createPluginSettingsClient |
PluginSettingsClientError |
class | Host-declared settings request failure |
PluginSettingsClientTimeoutError |
class | Settings request timeout failure |
PluginSettingsFrameMessage |
type | Settings iframe postMessage request union |
PluginSettingsFrameResultMessage |
type | Host response union for settings iframe requests |
PluginSettingValue |
type | JSON-safe value shape accepted by per-user plugin settings |
PLUGIN_SETTINGS_FRAME_PROTOCOL_VERSION |
const | Current settings iframe protocol version (1) |
PLUGIN_SETTINGS_FRAME_CHANNEL_META_NAME |
const | Meta tag name that carries the host-generated channel id |
Authoring shape
import { defineHook, type HookHandler } from "@hivegrid/plugin-sdk";
export const handleHello: HookHandler<{ name: string }, { greeting: string }> = defineHook((input, ctx) => {
ctx.log.info({ name: input.name }, "hello");
return { greeting: `Hello, ${input.name}!` };
});
Every hook export resolves to a single WebAssembly function with a
(arg) -> result shape. The runtime serialises both directions as
JSON; complex types fall through the standard JSON.stringify lens.
Payload ABI
The foundation runtime exposes the active invocation payload as UTF-8 JSON through two raw WebAssembly imports:
| Import | Signature | Purpose |
|---|---|---|
env.hivegrid_payload_len |
() -> i32 |
Length in bytes of the JSON payload |
env.hivegrid_payload_byte |
(index: i32) -> i32 |
Byte value at index, or 0 outside the payload |
The SDK exports PLUGIN_PAYLOAD_ABI_VERSION and
PLUGIN_PAYLOAD_ABI_IMPORTS so toolchain bindings can assert the host
contract. Higher-level AssemblyScript/Rust helpers are still follow-up
work; raw WASM plugins can already decode the JSON bytes themselves.
Settings iframe protocol
When a manifest declares ui.settings.entry, the host renders that HTML
inside a sandboxed PluginSettingsFrame. The iframe has an opaque
origin (sandbox="allow-scripts", no allow-same-origin) and a deny-by-default
CSP, so settings UIs communicate with the host only through the versioned
postMessage protocol exported by the SDK.
Use createPluginSettingsClient() to read the injected channel id, send
ready, get, and set messages to window.parent, and accept responses
only when the message came from window.parent and matches the expected
protocol version, plugin id, channel id, and request id:
import { createPluginSettingsClient, PluginSettingsClientError, PluginSettingsClientTimeoutError } from "@hivegrid/plugin-sdk";
const settings = createPluginSettingsClient({
pluginId: "com.acme.settings",
});
settings.ready();
try {
const theme = await settings.get("theme");
applyTheme(theme);
await settings.set("theme", "dark");
} catch (error) {
if (error instanceof PluginSettingsClientError) {
console.warn("setting request failed", error.code, error.message);
} else if (error instanceof PluginSettingsClientTimeoutError) {
console.warn("setting request timed out", error.message);
} else {
throw error;
}
}
The host validates pluginId, channelId, requestId, keys, and values.
Values must be JSON-safe (PluginSettingValue) and stay within the host
size/depth budgets. Persistent writes are CSRF-protected, tenant/user scoped,
and rate-limited by the route action.
Per-capability bindings on ctx
Each capability bound to the invocation appears as an optional field on
ctx. The runtime sets a field only when the manifest declared it AND
the gateway has a host implementation for it. Always null-check before
use:
if (!ctx.http) {
ctx.log.warn({}, "http capability not bound — manifest must declare http.outbound");
return { ok: false };
}
await ctx.http.fetch({ url: "https://example.com", method: "GET" });
This shape gives you compile-time documentation: every capability the plugin uses is visible at the top of the handler.
Logger
ctx.log is a structured logger. The host writes the message to its
own logging pipeline (pino under the hood). The signature mirrors pino:
ctx.log.info({ tenantId, recordId }, "entity created");
ctx.log.warn({ retries: 3 }, "retrying");
ctx.log.error({ err: errSerializable }, "fatal");
Never log secrets — output sanitisation is best-effort, not a guarantee.
HTTP client (ctx.http)
Reserved SDK shape for http.outbound. It is not bound in the current
production/foundation runtime; manifests may declare it only as a reviewed
future capability, and calls fail closed until the host binding is explicitly
enabled after security review.
const response = await ctx.http!.fetch({
url: "https://hooks.slack.com/services/T0/B0/abc",
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "hello" }),
});
When enabled, the host validates the URL against allowedHosts from the manifest's
http.outbound scope before issuing the request. Any other host
results in a denied audit row.
Storage (ctx.storage)
Reserved SDK shape for storage.read and storage.write. It is not bound in
the current production/foundation runtime; tenant-scoped storage remains
fail-closed until the capability receives a dedicated security review.
const channel = await ctx.storage!.get(`slack:${ctx.tenantId}:default-channel`);
await ctx.storage!.put(`slack:${ctx.tenantId}:default-channel`, "#alerts", {
ttlSeconds: 86_400,
});
Keys are byte-strings (no length cap today, but treat 256B as a soft budget). Values are byte-strings; encode JSON yourself if you need structured data.
Audit (ctx.audit)
Production-available when the plugin manifest declares audit and the tenant
has granted that capability. Records a tenant-visible audit event:
await ctx.audit.record({
action: "slack.notification-sent",
target: `entity:${recordId}`,
meta: { channel: "#alerts" },
});
Use for state-change observability. Do not abuse as a queue — metadata payloads have a hard cap.
Errors
Throwing from a handler aborts the invocation. The runtime captures the
exception, records outcome=error against every in-flight capability,
flushes the audit batch, and marks the invocation failed. The plugin
crash-tracker auto-disables a plugin after repeated failures.
Versioning
The SDK follows SemVer. Major bumps may add or remove capability
surfaces; consult docs/plugins/versioning.md before pinning to a new
major. The runtime advertises its host range through minHostVersion /
maxHostVersion in the manifest — when in doubt, lint your manifest
against the running host.