Files
suaveplan/packages/api-stack/api/api-host/src/define-plugin/define-plugin.md
2026-05-27 23:29:53 +01:00

3.7 KiB

define-plugin — author-facing plugin DX

(no submodules)

Purpose

definePlugin is the canonical entry point plugin authors call to declare their name / version / requires / isolation / trustLevel plus optional lifecycle hooks (install, start, stop, uninstall, health). It threads the literal-array requires through to the PluginContext<Caps> and RequestContext<Caps> so undeclared capability access becomes a compile-time TypeScript error — the type system, not a runtime guard, is what enforces the capability surface.

The runtime work is light: validate name / version are non-empty, reject duplicate ids inside requires, and return a frozen record. The heavy lifting (manifest cross-check, capability resolution, FSM hand-off) lives in the host's lifecycle driver; definePlugin is the type-safety + ergonomics layer.

Features

  • const Caps extends ReadonlyArray<CapabilityId> generic — captures literal-array requires so ctx.<cap> is narrowed exactly to the declared ids.
  • Synchronous validation for the two failure modes the type system cannot catch: empty name, empty version, duplicate id.
  • Frozen output record — safe to hash for the rollback journal.
  • Lightweight DefinePluginError (plain Error subclass) — no SuaveplanError dependency on the hot author-side path.
  • Type aliases PluginContextOf<P> / RequestContextOf<P> derive the per-hook context shape from the module record.

Usage: Basic

import { definePlugin } from "@suaveplan/api-host/define-plugin";

export default definePlugin({
    name: "billing",
    version: "1.2.3",
    requires: ["db", "auth.session", "logger"] as const,
    isolation: "trusted",
    trustLevel: "first-party",
    async start(ctx) {
        ctx.capabilities.logger.info("billing: started");
    },
});

Usage: Advanced

Derive request-context type for downstream router wiring:

import {
    definePlugin,
    type RequestContextOf,
} from "@suaveplan/api-host/define-plugin";

const plugin = definePlugin({
    name: "billing",
    version: "1.2.3",
    requires: ["db", "auth.session"] as const,
    isolation: "isolated",
    trustLevel: "third-party",
    async install(ctx) {
        // ctx.capabilities.db is `AdminDatabaseLike` here.
        await ctx.capabilities.db.execute("INSERT INTO ...");
    },
    async start(ctx) {
        ctx.capabilities.db; // ok
        // ctx.capabilities.events; // <- compile error: "events" not declared
    },
});

type Req = RequestContextOf<typeof plugin>;
// Req.capabilities.db is `TenantScopedDatabaseLike` here.

API Reference

  • definePlugin<const Caps>(spec): PluginModule<Caps> — validates + freezes.
  • DefinePluginSpec<Caps> — input shape with optional lifecycle hooks.
  • PluginModule<Caps> — Readonly<DefinePluginSpec<Caps>>.
  • PluginContextOf<P> / RequestContextOf<P> — per-module derived context types.
  • DefinePluginError — code ∈ DEFINE_PLUGIN.INVALID_NAME | DEFINE_PLUGIN.INVALID_VERSION | DEFINE_PLUGIN.DUPLICATE_CAPABILITY.

Implementation Notes

The const Caps extends ReadonlyArray<CapabilityId> constraint relies on TypeScript 5.0+ const generic inference — callers MUST write requires: ["db"] as const (or satisfies CapabilityRequirements) for the narrowing to work. Without as const, TypeScript widens the tuple to string[] and the capability map degrades to the full CapabilityId union. The validation is intentionally minimal: deeper manifest-vs-module cross-check is the host loader's job (Chunk B+) so author errors surface in two places — fast / type-only here, slow / runtime + manifest-aware there. The frozen output ensures the rollback journal can content-hash a stable representation.