first commit
This commit is contained in:
157
docs/api-stack/api/api-host/src/host/host.md
Normal file
157
docs/api-stack/api/api-host/src/host/host.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# `createHost` — top-level host orchestration entry point
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`createHost(config)` composes every internal driver (`LifecycleDriver`, `ReloadDriver`, `PluginLedger`, `CapabilityRegistry`, `RequestPipeline`, admin API) into a runnable host. Production deployments hand their HTTP engine's `fetch(request)` handler straight to `host.fetch(...)`; integration tests drive the same surface programmatically.
|
||||
|
||||
The resulting `Host` instance carries an authoritative install order (topo-sorted by manifest deps), exposes start/stop hooks, and serves three kinds of traffic: admin endpoints under `adminPrefix` (default `/admin`), internal probes (`/internal/livez`, `/internal/readyz`), and per-request plugin traffic through the §6 11-step pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated config with stable `HOST.CONFIG_INVALID` failure code.
|
||||
- Topological dependency-graph install via `resolveInstallOrder`.
|
||||
- Lifecycle + reload + plugin-ledger + capability-registry shared across the admin API and the per-request pipeline.
|
||||
- Per-mode response-validation strictness — `production` defaults to `enforce`, `development` to `warn`.
|
||||
- Admin API mountable at any prefix or disable-able via `adminPrefix: null`.
|
||||
- `/internal/livez` returns `200 ok`; `/internal/readyz` returns `{state, plugins}` with the registered install order.
|
||||
- Graceful shutdown with `graceMs` budget; sequential reverse-topo stop sequence.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createHost } from "@suaveplan/api-host/host";
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
const host = await createHost({
|
||||
adapters: {
|
||||
router,
|
||||
auth,
|
||||
tenant,
|
||||
capabilities,
|
||||
rateLimit,
|
||||
idempotency,
|
||||
csrf,
|
||||
audit,
|
||||
span,
|
||||
db: new Database(":memory:"),
|
||||
},
|
||||
plugins: [
|
||||
{ module: billingModule, manifest: billingManifest, manifestHash: "sha256:...", specifier: "file:///billing.js" },
|
||||
],
|
||||
});
|
||||
await host.start();
|
||||
|
||||
Bun.serve({ port: 3000, fetch: (req) => host.fetch(req) });
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Disable the admin surface for non-admin pods + supply a request-id
|
||||
generator for log correlation:
|
||||
|
||||
```ts
|
||||
const host = await createHost({
|
||||
adapters,
|
||||
plugins,
|
||||
adminPrefix: null,
|
||||
mode: "development",
|
||||
requestId: () => `req_${process.pid}_${counter++}`,
|
||||
});
|
||||
```
|
||||
|
||||
Install a plugin at runtime (post-start):
|
||||
|
||||
```ts
|
||||
await host.installPlugin({
|
||||
module: notificationsModule,
|
||||
manifest: notificationsManifest,
|
||||
manifestHash: "sha256:...",
|
||||
specifier: "file:///notifications.js",
|
||||
});
|
||||
expect(host.plugins()).toContain("@acme/notifications");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
```ts
|
||||
function createHost(config: HostConfig): Promise<Host>;
|
||||
|
||||
interface Host {
|
||||
start(): Promise<void>;
|
||||
stop(graceMs?: number): Promise<void>;
|
||||
fetch(request: Request): Promise<Response>;
|
||||
installPlugin(install: HostPluginInstall): Promise<void>;
|
||||
uninstallPlugin(name: string): Promise<void>;
|
||||
plugins(): readonly string[];
|
||||
readonly instruments: HostInstruments;
|
||||
}
|
||||
|
||||
interface HostConfig {
|
||||
adapters: HostAdapters;
|
||||
plugins: ReadonlyArray<HostPluginInstall>;
|
||||
adminPrefix?: string | null;
|
||||
mode?: "production" | "development";
|
||||
instruments?: HostInstruments;
|
||||
logger?: BoundaryLogger & PipelineRequestLogger;
|
||||
hostId?: string;
|
||||
hostVersion?: string;
|
||||
/**
|
||||
* Fallback tenant — see "Default tenant" below. `undefined` /
|
||||
* `null` propagate `ctx.tenant === null` on resolver miss for
|
||||
* auth-optional routes; a `TenantContext` value preserves the
|
||||
* legacy `{id: "public", slug: "public"}` injection.
|
||||
*/
|
||||
defaultTenant?: TenantContext | null;
|
||||
}
|
||||
```
|
||||
|
||||
### Default tenant (foundation gap 5)
|
||||
|
||||
Prior to `0.1.0` the request pipeline injected a hard-coded
|
||||
`{id: "public", slug: "public"}` `TenantContext` whenever the configured
|
||||
`TenantResolverChain` returned `null` on a public (auth-optional) route.
|
||||
That made the `"public"` literal load-bearing across every handler that
|
||||
introspected `ctx.tenantId`, blocking deployments that wanted to
|
||||
distinguish "no tenant" from "the public tenant".
|
||||
|
||||
`HostConfig.defaultTenant` now controls the fallback explicitly:
|
||||
|
||||
| Value | Behaviour on resolver miss + auth-optional route |
|
||||
| --------------- | --------------------------------------------------------- |
|
||||
| `undefined` | `ctx.tenant === null` (the new default) |
|
||||
| `null` | Same as `undefined` — documents the intent explicitly |
|
||||
| `TenantContext` | Inject verbatim — preserves legacy `"public"` semantics |
|
||||
|
||||
Auth-required routes (`auth: <scope>`) still raise
|
||||
`HOST.TENANT_NOT_RESOLVED` (400) on a resolver miss regardless of
|
||||
`defaultTenant` — the fallback is a public-route convenience, not a
|
||||
silent bypass of tenant-required policy.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`createHost` is the only entry point that touches every internal
|
||||
driver; everything else in `@suaveplan/api-host` is composable in
|
||||
isolation. Tests SHOULD prefer the focused driver imports
|
||||
(`@suaveplan/api-host/lifecycle`, `@suaveplan/api-host/request-pipeline`,
|
||||
etc.) and reach for `createHost` only when the integration path matters.
|
||||
|
||||
The admin sub-router is mounted via Hono's `app.fetch(...)` directly
|
||||
against a re-shaped `Request` whose pathname strips the configured
|
||||
`adminPrefix`. This avoids a Hono `app.route(...)` import-cycle with
|
||||
the admin-api package and keeps the prefix configurable without
|
||||
forcing the admin router to know its own mount point.
|
||||
|
||||
`installPlugin` post-start short-circuits the topo-sort by appending
|
||||
to the existing install order — adding a plugin at runtime never
|
||||
re-orders previously-started plugins. A plugin with unresolved deps
|
||||
will fail lifecycle install just as it would at start; the host
|
||||
surfaces that via `LifecycleDriver`'s state-machine codes.
|
||||
|
||||
The reload resolver bag is intentionally minimal — Stage 3 will wire
|
||||
`@suaveplan/api-loader`'s `ModuleResolver` here. Until then, calling
|
||||
`reload(...)` throws `HOST.INTERNAL_INVARIANT` to make the missing
|
||||
adapter loud rather than silent.
|
||||
Reference in New Issue
Block a user