@chamvi/http
REST, SSE, webhooks and MCP for chamvi agents, on routup. The public API of packages/http/src/index.ts.
npm install @chamvi/http @chamvi/core --save@chamvi/core is a peer dependency. See HTTP API & MCP for the behaviour.
createHttpApp
function createHttpApp(options: HttpAppOptions): App; // routup AppThe store, the agents and the clock come from engine (engine.store, engine.listAgents(), engine.clock); agents narrows the served agents to the named ones, the others get no routes, MCP tools or hooks in this app. Throws InvalidAgentDefinitionError for a name in agents the engine lacks or that is given twice, an empty auth array and an invalid routes entry (a path that is not static, a duplicate method and path, a first segment of a built-in route such as agents, runs or hooks, or the metrics path, all compared case-insensitively).
export type CorsOptions = CorsPluginOptions;
export type RateLimitOptions = RateLimitPluginOptions;
export type PrometheusOptions = PrometheusPluginOptions;
/** routup's trust-proxy input: a predicate, CIDRs, `'loopback'`, a hop count or a boolean. */
export type TrustProxyInput = NonNullable<AppOptionsInput['trustProxy']>;
export type McpOptions = {
name?: string;
version?: string;
maxWaitSeconds?: number;
};
export type HttpAppOptions = {
/** Its `store`, agents and `clock` serve every route. */
engine: Engine;
/**
* Names of the engine's agents to serve (routes, MCP tools, hooks, extra
* routes); the others are unknown to this app. Default: every agent.
*/
agents?: string[];
/** One authenticator or a chain; the first principal wins. */
auth: Authenticator | Authenticator[];
/** Prefix of every route, e.g. `/api`. Default: none. */
basePath?: string;
/** Which proxies `getRequestIP` trusts (and so the rate-limit key). Default: none. */
trustProxy?: TrustProxyInput;
/** Fails `/readyz` once shutting down; the app ends its streams on shutdown. */
shutdown?: IShutdown;
/** Bytes a request body may have. Default: 1 MiB. */
bodyLimit?: number;
stream?: {
/** How often a stream polls the store for runs executing elsewhere. Default: 2 s. */
pollMs?: number;
/**
* Open SSE run streams plus waiting MCP `run_<agent>` calls, per app
* (a positive integer). Over it a stream answers 503 with
* `Retry-After`, an MCP call answers without waiting. Default: 1000.
*/
maxOpen?: number;
};
mcp?: false | McpOptions;
/** CORS is off unless given. */
cors?: false | CorsOptions;
rateLimit?: false | Partial<RateLimitOptions>;
metrics?: false | Partial<PrometheusOptions>;
/** How long `/readyz` waits for the store. Default: 2 s. */
readyTimeoutMs?: number;
/** Receives 5xx causes and background failures. Default: `console.error`. */
onError?: (error: unknown) => void;
/** Extra routes other packages contribute (e.g. a GitHub webhook), served under `basePath`. */
routes?: HttpRoute[];
};routes takes extra routes from other packages, such as the GitHub webhook of @chamvi/github:
export type HttpRouteMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
export type HttpRouteRequest = {
/** URL, method, headers and signal; without the body, which only readBody() reads. */
request: Request;
/**
* The raw body, at most `bodyLimit` bytes: 413 above it (content-length
* refused unread, else drained like readJsonBody), 415 for a non-JSON
* media type with a non-empty body; an empty body is a 0-length array.
* Memoized: every call answers the same bytes (or the same error).
*/
readBody(): Promise<Uint8Array>;
};
export type HttpRouteResult = {
status: number;
/**
* Objects, arrays, numbers and booleans are sent as JSON, a string as
* `text/plain`; `undefined` or `null` answer an empty body.
*/
body?: unknown;
};
export type HttpRouteContext = {
engine: Engine;
agents: readonly Agent[];
clock: IClock;
onError: (error: unknown) => void;
};
export type HttpRoute = {
method: HttpRouteMethod;
/** Static path under basePath (no ':' '*' '{'), e.g. '/webhooks/github'; also its metrics label. */
path: string;
/** false: skipped by the app's rate limiter (callers that never retry, e.g. webhook senders). Default: limited. */
rateLimit?: false;
/**
* Not authenticated by the app's authenticators: the route checks its own
* credentials. Thrown @ebec/http and core errors render like any route's.
*/
handler: (request: HttpRouteRequest, ctx: HttpRouteContext) => Promise<HttpRouteResult>;
};Authenticators
type Authenticator = (request: Request) => Promise<Principal | undefined>;
function staticTokens(options: StaticTokensOptions): Authenticator;
function authup(options: AuthupOptions): Authenticator;
function oidc(options: OidcOptions): Authenticator;An authenticator resolves undefined for a request it does not recognize and throws a 401 for credentials it rejects. staticTokens throws InvalidAgentDefinitionError for an empty tokens array, a token value shorter than 16 characters, a missing id or a value given twice; oidc for an empty issuers list, an issuer without audience or a missing principal.
/*
* Copyright (c) 2026.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/
import type { Permission } from './constants.ts';
export type PermissionName = typeof Permission[keyof typeof Permission];
export type AgentAction = 'run' | 'approve';
export type Principal = {
id: string;
kind: 'token' | 'authup' | 'oidc' | (string & {});
/** Granted permissions, e.g. `agent:triage:run`, `run:read` or `*`. */
permissions: readonly string[];
claims?: Record<string, unknown>;
};
/**
* Resolves `undefined` when the request carries no credentials it
* recognizes, throws `UnauthorizedError` for credentials it rejects.
*/
export type Authenticator = (request: Request) => Promise<Principal | undefined>;Permissions
const Permission: {
ALL: '*';
RUN_READ: 'run:read';
RUN_CANCEL: 'run:cancel';
STATE_READ: 'state:read';
STATE_WRITE: 'state:write';
};
function agentPermission(agent: string, action: 'run' | 'approve'): string; // agent:<agent>:<action>Matching uses core's hasPermission(principal.permissions, permission): true for a granted *, or for a grant with the same number of : segments whose every segment is equal or *.
createShutdown
function createShutdown(options: ShutdownOptions): IShutdown;
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS: 30_000;
const DEFAULT_SHUTDOWN_CLOSE_TIMEOUT_MS: 5_000;/*
* Copyright (c) 2026.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/
import type { Engine } from '@chamvi/core';
export interface IShutdown {
/** True from the first `shutdown()` call on; `/readyz` then fails. */
readonly shuttingDown: boolean;
/** Runs `fn` during the shutdown, after the scheduler stopped; returns an unsubscribe. */
onShutdown(fn: () => void | Promise<void>): () => void;
/** Idempotent: every call returns the same promise. */
shutdown(): Promise<void>;
}
export type ShutdownOptions = {
/** Drained on shutdown; its `clock` times the delays. */
engine: Engine;
scheduler?: { stop(): Promise<void> };
/** srvx `Server` (pass `gracefulShutdown: false` to `serve`). */
server?: { close(closeActiveConnections?: boolean): Promise<void> };
/** Time for load balancers to see the failing readiness. Default 0. */
readinessDelayMs?: number;
/** Default 30 s. */
drainTimeoutMs?: number;
/** Wait for open connections after the drain, then close them. Default 5 s. */
closeTimeoutMs?: number;
/** Default `console.error`. */
onError?: (error: unknown) => void;
};Wire types
type InvokerView = Omit<Invoker, 'permissions'>;
type RunView = Omit<Run, 'claim' | 'trigger'> & {
trigger: Omit<TriggerEnvelope, 'invoker'> & { invoker?: InvokerView };
};
type RunSummary = Pick<Run, 'id' | 'agent' | 'status' | 'createdAt' | 'finishedAt' | 'error'>;
type RunToolResult = {
runId: string;
status: RunStatusLiteral;
deduplicated: boolean;
output?: unknown;
error?: { code: string; message: string };
waiting?: { toolName: string; input: unknown; expiresAt: string };
};AgentSummary and ScheduleSummary are the items of GET /agents and GET /schedules:
/** An agent as `GET /agents` lists it. */
export type AgentSummary = {
name: string;
/** `hook` is the ready-made path of a `webhook()` trigger. */
triggers: Array<{
source: TriggerSource;
id: string;
hook?: string
}>;
budget: Budget;
/** Tool names that need an approval. */
approvals: string[];
/** Declared state collections. */
state: string[];
notify?: string;
};
/** A `cron()` trigger as `GET /schedules` lists it (definitions only, not scheduler health). */
export type ScheduleSummary = {
agent: string;
trigger: string;
expression: string;
timezone?: string;
overlap: OverlapPolicy;
/** ISO time of the next fire after now. */
nextAt: string;
};