Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
# `define-plugin` — author-facing plugin DX
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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:
```ts
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.