Skip to main content
The TypeScript SDK is the public package every GCS plugin imports. It hides the postMessage envelope shape and gives you a typed PluginContext instead.

Install

The standalone npm publish lands with the hosted registry. Until then the supported workflow is to develop your plugin inside the altnautica/ADOSExtensions monorepo as a pnpm workspace member; the SDK resolves via workspace:^ against the in-tree packages/plugin-sdk. The Quickstart walks through the clone and scaffold flow. The package is a peer of the host iframe runtime. It has no runtime dependencies and ships ESM only.

definePlugin

The single entry point:
import { definePlugin } from "@altnautica/plugin-sdk";

definePlugin({
  id: "com.example.my-plugin",
  version: "1.0.0",
  locale: { "hello.title": "Hello" },
  async mount(ctx, info) {
    // wire up subscriptions, render the panel, etc.
  },
  async unmount(ctx, info) {
    // optional, runs when the host disposes the iframe
  },
});
mount is awaited; if it throws, the host receives a handler_error envelope and surfaces it in the install dialog. info is optional. Most plugins write async mount(ctx) and ignore it; the second argument carries the host build version, the plugin’s resolved permission set, and the operator-edited config. The same pattern applies to unmount.

PluginContext

The argument the SDK hands to mount:
interface PluginContext {
  client: PluginClient;
  telemetry: {
    subscribe<T>(topic: string, handler: (args: T) => void): Promise<() => void>;
  };
  command: {
    send(command: string, args?: unknown): Promise<unknown>;
  };
  notifications: {
    publish(payload: NotificationPayload): Promise<unknown>;
  };
  recording: {
    mark(payload: RecordingMark): Promise<unknown>;
  };
  mission: {
    read(missionId: string): Promise<unknown>;
    write(update: MissionUpdate): Promise<unknown>;
  };
  config: {
    onChange<T>(handler: (next: T) => void): () => void;
  };
  theme: {
    onChange(handler: (vars: Record<string, string>) => void): () => void;
  };
  i18n: {
    t(key: string, params?: Record<string, string | number>): string;
  };
}
Each domain group is small on purpose. Plugins drop down to ctx.client.request(method, capability, args) for any RPC the helpers do not expose.

PluginClient

The lower-level RPC client:
import { PluginClient } from "@altnautica/plugin-sdk";

const client = new PluginClient();
const result = await client.request<{ ok: boolean }>(
  "telemetry.subscribe",
  "telemetry.subscribe.battery",
  { topic: "battery" },
  { timeoutMs: 3000 },
);
Methods:
  • request(method, capability, args, options?): send a request and await the response. Default timeout 5s.
  • on(method, handler): subscribe to host-pushed events.
  • subscribeTelemetry(topic, handler): convenience that does both request("telemetry.subscribe", ...) and on("telemetry.<topic>", ...).
  • dispose(): tear down listeners and reject in-flight RPCs.

HostError

Thrown by client.request when the host returns an error envelope:
import { HostError } from "@altnautica/plugin-sdk";

try {
  await ctx.command.send("ARM");
} catch (err) {
  if (err instanceof HostError && err.code === "permission_denied") {
    // show a banner
  } else {
    throw err;
  }
}
The code field is the machine-readable error code from the envelope. The message is human-readable; do not branch on it.

Test harness

import { createPluginHarness } from "@altnautica/plugin-sdk/harness";

const harness = createPluginHarness({
  grantedCapabilities: ["telemetry.subscribe.battery"],
  mount: async (ctx) => {
    await ctx.telemetry.subscribe("battery", (s) => store.ingest(s));
  },
});

await harness.start();
harness.pushTelemetry("battery", mockSample);
expect(harness.notifications).toEqual([...]);
await harness.teardown();
The harness:
  • mounts the plugin against an in-memory transport (no real iframe);
  • captures every RPC in harness.calls, grouped helpers in harness.notifications and harness.recordingMarks;
  • accepts pushTelemetry, pushEvent, pushConfig, pushTheme;
  • supports failNext(method, code, message) to simulate host failures;
  • mirrors the real bridge’s capability gate.
This means you can validate a plugin end-to-end inside Vitest without running Mission Control or a real drone.

Building the bundle

The SDK does not ship a build step. Plugin repos use esbuild, Vite, or any other bundler that emits ESM. The create-ados-plugin template uses esbuild for zero-config builds:
esbuild src/plugin.ts --bundle --format=esm --target=es2022 --outfile=plugin.bundle.js --minify
Output goes to gcs/plugin.bundle.js. The manifest’s gcs.bundle field points at the same path. The pack script computes the SHA-256 and writes it into assets[].sha256.