Skip to main content

@tokenring-ai/web-host

The @tokenring-ai/web-host package provides a high-performance Bun web server with a pluggable resource registration system for the Token Ring ecosystem. It serves as the foundation for hosting web UIs, REST APIs, and real-time communication endpoints with comprehensive support for static files, SPAs, JSON-RPC APIs, WebSocket RPC, and authentication.

Overview

The web-host package provides a complete web server solution using Bun's native HTTP and WebSocket capabilities. It features a flexible resource registration system that allows both configuration-based and programmatic resource registration. The package integrates seamlessly with the Token Ring agent framework, automatically registering RPC endpoints and providing a /webhost command for monitoring.

Key Features

  • Bun Server: High-performance HTTP server using Bun's native HTTP and WebSocket support
  • Authentication: Basic and Bearer token authentication with per-user credentials
  • Resource Registration: Pluggable system for registering web resources via KeyedRegistry
  • Static File Serving: Serve static files and directories with customizable index and 404 handling
  • Single-Page Applications: Serve SPA applications with proper client-side routing support
  • Configurable Port: Flexible port configuration with automatic port assignment (port 0)
  • JSON-RPC API: Built-in JSON-RPC 2.0 support with streaming via Server-Sent Events
  • WebSocket RPC: WebSocket-based RPC with real-time streaming support
  • Command Integration: /webhost command for monitoring and management
  • Automatic Endpoint Registration: Automatic registration of RPC endpoints during startup
  • Type-Safe Configuration: Full TypeScript support with Zod schema validation

Core Components

WebHostService

Central service managing the Bun server and resource registration.

class WebHostService implements TokenRingService {
readonly name = "WebHostService";
readonly description = "Bun web host for serving resources and APIs";

resources: KeyedRegistry<WebResource>;
registerResource: (name: string, resource: WebResource) => void;
getResourceEntries: () => Iterable<[string, WebResource]>;

constructor(app: TokenRingApp, config: ParsedWebHostConfig);

async start(signal: AbortSignal): Promise<void>;
async stop(): Promise<void>;
getURL(): URL;
}

Properties:

PropertyTypeDescription
namestringService name ("WebHostService")
descriptionstringService description
resourcesKeyedRegistry<WebResource>Registry of registered resources
registerResource(name: string, resource: WebResource) => voidMethod to register resources
getResourceEntries() => Iterable<[string, WebResource]>Method to get all resources

Methods:

MethodSignatureDescription
start(signal: AbortSignal) => Promise<void>Start the Bun server and register all resources
stop() => Promise<void>Stop the server and close all connections
getURL() => URLGet the current server URL

Server Lifecycle:

  1. Start Phase:

    • Registers authentication if configured
    • Registers all web resources
    • Binds to configured host and port using Bun.serve
    • Logs the server URL
  2. Stop Phase:

    • Closes the server and all active connections

WebResource Interface

Interface for pluggable web resources.

interface WebResource {
register(router: BunRouter): Promise<void>;
}

BunRouter Interface

The router interface used by resources to register handlers:

interface BunRouter {
get(path: string, handler: RouteHandler): void;
post(path: string, handler: RouteHandler): void;
put(path: string, handler: RouteHandler): void;
delete(path: string, handler: RouteHandler): void;
ws(path: string, handler: WebSocketHandler): void;
static(prefix: string, root: string, options?: StaticOptions): void;
fallback(handler: RouteHandler): void;
}

BunRequest Interface

Request object passed to route handlers:

interface BunRequest {
method: string;
url: string;
path: string;
headers: Headers;
body: () => Promise<any>;
json: () => Promise<any>;
text: () => Promise<string>;
arrayBuffer: () => Promise<ArrayBuffer>;
}

BunResponse Interface

Response utilities:

interface BunResponse {
json(data: any, status?: number): Response;
text(data: string, status?: number): Response;
file(path: string): Promise<Response>;
html(data: string, status?: number): Response;
redirect(url: string, status?: number): Response;
stream(callback: (controller: ReadableStreamDefaultController) => Promise<void>): Response;
}

StaticResource Class

Serves static files from a directory using Bun's native file serving.

class StaticResource implements WebResource {
constructor(config: z.output<typeof staticResourceConfigSchema>)

async register(router: BunRouter): Promise<void> {
router.static(this.config.prefix, this.config.root, {
index: this.config.indexFile,
notFound: this.config.notFoundFile
});
}
}

Configuration:

OptionTypeDescription
type"static"Resource type discriminator
rootstringDirectory containing static files
descriptionstringHuman-readable description
indexFilestringDefault file to serve for directory requests
notFoundFilestringOptional custom 404 page (note: not currently used by Bun)
prefixstringURL prefix (e.g., /static)

Behavior:

  • Files are served under the specified prefix
  • Index file is served for directory requests
  • Uses Bun's native static file serving

SPAResource Class

Serves single-page applications with proper client-side routing support.

class SPAResource implements WebResource {
constructor(config: z.output<typeof spaResourceConfigSchema>)

async register(router: BunRouter): Promise<void> {
// Validates file exists
// Registers static file serving for SPA directory
// Handles root path with index.html
// Sets fallback handler for client-side routes
}
}

Configuration:

OptionTypeDescription
type"spa"Resource type discriminator
filestringPath to the SPA index.html file
descriptionstringHuman-readable description
prefixstringURL prefix (e.g., /app)

Routing Behavior:

  • Static files: Served directly by Bun's native file serving
  • Root path: Serves the SPA index.html file
  • Client-side routes: All non-static-file requests serve index.html
  • Missing files: Returns 404 if static file doesn't exist

Example Routing:

/app/           → index.html
/app/dashboard → index.html (client-side routing)
/app/main.js → main.js (static file)
/app/missing.css → 404 (file doesn't exist)

JsonRpcResource Class

Provides JSON-RPC 2.0 API endpoints with streaming support via Server-Sent Events.

class JsonRpcResource implements WebResource {
constructor(app: TokenRingApp, jsonRpcEndpoint: RpcEndpoint)

async register(router: BunRouter): Promise<void> {
// Registers POST endpoint for JSON-RPC calls
// Handles query, mutation, and stream methods
// Stream methods use Server-Sent Events (text/event-stream)
}
}

JSON-RPC Error Codes:

Error CodeDescription
-32700Parse error (invalid JSON)
-32600Invalid Request (wrong JSON-RPC version)
-32601Method not found
-32603Internal error (validation or execution error)

Streaming Behavior:

  • Stream methods return Server-Sent Events (SSE) format
  • Content-Type: text/event-stream
  • Each event is prefixed with data: and followed by \n\n
  • Client can abort the stream by closing the connection

WsRpcResource Class

Provides WebSocket-based RPC endpoints for real-time communication.

class WsRpcResource implements WebResource {
constructor(app: TokenRingApp, jsonRpcEndpoint: RpcEndpoint)

async register(router: BunRouter): Promise<void> {
// Registers WebSocket endpoint at the same path as JSON-RPC
// Handles query, mutation, and stream methods
// Stream methods emit individual JSON-RPC responses
}
}

WebSocket Message Format:

{
"jsonrpc": "2.0",
"id": 1,
"result": { ... }
}

Streaming Behavior:

  • Stream methods emit individual JSON-RPC responses
  • Stream ends with {"stream": "end"} marker
  • Errors are sent as JSON-RPC error responses
  • Client can close the WebSocket to abort streaming

Services

WebHostService

The primary service provided by this package. It implements the TokenRingService interface and manages the Bun web server lifecycle.

Service Registration:

import { TokenRingApp } from "@tokenring-ai/app";
import webHostPackage from "@tokenring-ai/web-host";

const app = new TokenRingApp({
webHost: {
port: 3000,
host: "127.0.0.1"
}
});

await app.addPlugin(webHostPackage);
await app.start();

// Access the service
const webHost = app.getServiceByType(WebHostService);

Service Methods:

// Get the server URL
const url = webHost.getURL();
console.log(url.toString()); // http://127.0.0.1:3000

// Register a custom resource
webHost.registerResource("myResource", customResource);

// Get all registered resources
for (const [name, resource] of webHost.getResourceEntries()) {
console.log(name);
}

Provider Documentation

The web-host package uses a plugin-based registration pattern rather than a traditional provider architecture. Resources are registered through:

  1. Configuration-based registration: Define resources in the plugin configuration
  2. Programmatic registration: Call registerResource on the WebHostService

KeyedRegistry Pattern:

The package uses KeyedRegistry from @tokenring-ai/utility to manage resources:

import KeyedRegistry from "@tokenring-ai/utility/registry/KeyedRegistry";

// Resources are stored in a KeyedRegistry
resources = new KeyedRegistry<WebResource>();

// Register a resource
registerResource = this.resources.register;

// Get all resources
getResourceEntries = this.resources.entries;

RPC Endpoints

The web-host package automatically registers JSON-RPC and WebSocket RPC endpoints for all endpoints registered with the RpcService from @tokenring-ai/rpc.

Automatic Endpoint Registration

During the plugin's start phase:

// 1. Get all endpoints from RpcService
const rpcService = app.getService(RpcService);

// 2. For each endpoint:
for (const endpoint of rpcService.getAllEndpoints()) {
// Create JSON-RPC resource
webHostService.registerResource(
endpoint.name,
new JsonRpcResource(app, endpoint)
);

// Create WebSocket RPC resource
webHostService.registerResource(
`${endpoint.name} (WS)`,
new WsRpcResource(app, endpoint)
);
}

Creating RPC Endpoints

import { createRPCEndpoint } from "@tokenring-ai/rpc/createRPCEndpoint";
import { z } from "zod";

const calculatorSchema = {
name: "Calculator",
path: "/api/calc",
methods: {
add: {
type: "query" as const,
input: z.object({ a: z.number(), b: z.number() }),
result: z.object({ result: z.number() })
},
streamResult: {
type: "stream" as const,
input: z.object({ steps: z.number() }),
result: z.object({ step: number, value: number })
}
}
};

const calculatorImplementation = {
add: async (params: { a: number, b: number }, app) => ({
result: params.a + params.b
}),

streamResult: async function* (params: { steps: number }, app, signal) {
let value = 0;
for (let i = 0; i < params.steps; i++) {
if (signal.aborted) break;
value += Math.random();
yield { step: i, value };
await new Promise(resolve => setTimeout(resolve, 500));
}
}
};

const endpoint = createRPCEndpoint(calculatorSchema, calculatorImplementation);

Chat Commands

/webhost Command

The web-host package provides a /webhost command for monitoring:

/webhost

# Output:
# Web host running at: http://localhost:3000
# Registered resources:
# - static-files
# - spa
# - calculator
# - customAPI

Command Description: Displays the current web host URL and lists all registered resources.

Help Text:

# /webhost

## Description
Displays the current web host URL and lists all registered resources.

## Usage
/webhost

## Output
- Web host URL with port
- List of registered resources and their names

## Example
/webhost
# Output:
# Web host running at: http://localhost:3000
# Registered resources:
# - trpcBackend
# - defaultFrontend

Configuration

WebHostConfigSchema

The main configuration schema for the web host service:

export const WebHostConfigSchema = z.object({
host: z.string().default("127.0.0.1"),
port: z.number().default(0),
auth: AuthConfigSchema.optional(),
resources: z.record(z.string(), z.discriminatedUnion("type", [
staticResourceConfigSchema,
spaResourceConfigSchema
])).optional(),
})

Configuration Options:

OptionTypeRequiredDefaultDescription
hoststringNo"127.0.0.1"Host address to bind to
portnumberNo0Port number. If 0 or not specified, an available port is automatically assigned
authAuthConfigNo-Authentication configuration
resourcesRecordNo-Web resources to register at startup

AuthConfigSchema

Authentication configuration schema supporting both Basic and Bearer token authentication:

export const AuthConfigSchema = z.object({
users: z.record(z.string(), z.object({
password: z.string().optional(),
bearerToken: z.string().optional(),
}))
})

Authentication Options:

OptionTypeDescription
usersRecordMap of usernames to credentials
passwordstringOptional password for Basic authentication
bearerTokenstringOptional bearer token for Bearer authentication

Note: Each user can have either a password, a bearer token, or both. Users without either credential cannot authenticate.

Static Resource Configuration

export const staticResourceConfigSchema = z.object({
type: z.literal("static"),
root: z.string(),
description: z.string(),
indexFile: z.string(),
notFoundFile: z.string().optional(),
prefix: z.string()
})
OptionTypeDescription
type"static"Discriminator for static resource type
rootstringDirectory path for static files
descriptionstringHuman-readable description
indexFilestringDefault index file name
notFoundFilestringOptional custom 404 page (note: not currently used by Bun)
prefixstringURL prefix for this resource

SPA Resource Configuration

export const spaResourceConfigSchema = z.object({
type: z.literal("spa"),
file: z.string(),
description: z.string(),
prefix: z.string()
})
OptionTypeDescription
type"spa"Discriminator for SPA resource type
filestringPath to the index.html file
descriptionstringHuman-readable description
prefixstringURL prefix for SPA routing

Integration

Plugin Registration

The web-host package integrates as a plugin with two phases:

Install Phase:

  1. Creates WebHostService with provided configuration
  2. Registers resources from configuration (StaticResource, SPAResource)
  3. Registers /webhost command with AgentCommandService

Start Phase:

  1. Starts the Bun server
  2. Automatically creates JsonRpcResource and WsRpcResource for each RPC endpoint registered with RpcService
  3. Logs registered endpoint paths

Plugin Configuration

import { TokenRingApp } from "@tokenring-ai/app";
import webHostPackage from "@tokenring-ai/web-host";

const app = new TokenRingApp({
webHost: {
port: 3000,
host: "0.0.0.0",
auth: {
users: {
"admin": {
password: "secret123",
bearerToken: "admin-token"
}
}
},
resources: {
"static-files": {
type: "static",
root: "./public",
description: "Static files",
indexFile: "index.html",
prefix: "/static"
},
"spa": {
type: "spa",
file: "./dist/index.html",
description: "Main application",
prefix: "/"
}
}
}
});

await app.addPlugin(webHostPackage);
await app.start();

Usage Examples

Basic Setup

import { TokenRingApp } from "@tokenring-ai/app";
import webHostPackage from "@tokenring-ai/web-host";

const app = new TokenRingApp({
webHost: {
port: 3000,
host: "127.0.0.1"
}
});

await app.addPlugin(webHostPackage);
await app.start();

// Server will be available at http://127.0.0.1:3000

Complete Configuration with Authentication

const app = new TokenRingApp({
webHost: {
port: 3000,
host: "0.0.0.0",
auth: {
users: {
"admin": {
password: "admin123",
bearerToken: "admin-token"
},
"api-user": {
bearerToken: "api-key-abc123"
}
}
},
resources: {
"public": {
type: "static",
root: "./public",
description: "Public static files",
indexFile: "index.html",
prefix: "/"
}
}
}
});

await app.addPlugin(webHostPackage);
await app.start();

Registering Custom Resources Programmatically

import { WebHostService } from "@tokenring-ai/web-host";
import type { WebResource } from "@tokenring-ai/web-host/types";

// Get the web host service
const webHost = app.getServiceByType(WebHostService);

if (webHost) {
// Create a custom API resource
const apiResource: WebResource = {
async register(router) {
router.get("/api/health", async (request, response) => {
return response.json({ status: "ok" });
});

router.post("/api/data", async (request, response) => {
const data = await request.json();
return response.json({ received: data });
});

// Access authenticated user if authentication is enabled
router.get("/api/whoami", async (request, response) => {
return response.json({ user: (request as any).user });
});
}
};

webHost.registerResource("customAPI", apiResource);
}

Serving Static Files

import { StaticResource } from "@tokenring-ai/web-host";

// Add custom static resource
const staticResource = new StaticResource({
type: "static",
root: "./public",
description: "Public static files",
indexFile: "index.html",
notFoundFile: "404.html",
prefix: "/static"
});

webHost.registerResource("public", staticResource);

Serving Single-Page Application

import { SPAResource } from "@tokenring-ai/web-host";

const spaResource = new SPAResource({
type: "spa",
file: "./dist/index.html",
description: "Main application",
prefix: "/"
});

webHost.registerResource("frontend", spaResource);

JSON-RPC Client Usage

import { createJsonRPCClient } from "@tokenring-ai/web-host/createJsonRPCClient";
import type { RPCSchema } from "@tokenring-ai/rpc/types";
import { z } from "zod";

const calculatorSchema: RPCSchema = {
name: "Calculator",
path: "/api/calc",
methods: {
add: {
type: "query" as const,
input: z.object({ a: z.number(), b: z.number() }),
result: z.object({ result: z.number() })
},
streamResult: {
type: "stream" as const,
input: z.object({ steps: z.number() }),
result: z.object({ step: number, value: number })
}
}
};

const client = createJsonRPCClient(new URL("http://localhost:3000"), calculatorSchema);

// Call query/mutation methods
const result = await client.add({ a: 5, b: 3 });
console.log(result); // { result: 8 }

// Stream methods return async generators
for await (const update of client.streamResult({ steps: 5 }, signal)) {
console.log(update);
}

WebSocket RPC Client Usage

import { createWsRPCClient } from "@tokenring-ai/web-host/createWsRPCClient";
import type { RPCSchema } from "@tokenring-ai/rpc/types";
import { z } from "zod";

const wsClient = createWsRPCClient(new URL("http://localhost:3000"), calculatorSchema);

// Call query/mutation methods
const result = await wsClient.add({ a: 5, b: 3 });
console.log(result); // { result: 8 }

// Stream methods return async generators
for await (const update of wsClient.streamResult({ steps: 5 }, signal)) {
console.log(update);
}

Best Practices

  1. Use Resource Registration: Register resources at startup or through the plugin system for consistent initialization.

  2. Validate Configuration: Use the provided Zod schemas to validate configuration before creating resources.

  3. Handle Streaming Properly: When implementing stream methods, always check the AbortSignal to support graceful shutdown.

  4. Use Type Safety: Leverage the type utilities (FunctionTypeOfRPCCall, ResultOfRPCCall, ParamsOfRPCCall) for type-safe RPC interactions.

  5. Configure Authentication: Use authentication for all production deployments to secure your APIs.

  6. Automatic Endpoint Registration: Let the web-host plugin automatically create JSON-RPC and WebSocket RPC resources from RpcService endpoints.

  7. SPA Routing: Use SPAResource for single-page applications to ensure proper client-side routing.

  8. Error Handling: Implement proper error handling in RPC methods to provide meaningful error messages.

Testing and Development

Running Tests

bun test                    # Run all tests
bun test:watch # Watch mode
bun test:coverage # Coverage report

Test Files

  • WebHostService.test.ts - Service lifecycle and resource registration
  • StaticResource.test.ts - Static file serving
  • SPAResource.test.ts - SPA routing
  • JsonRpcResource.test.ts - JSON-RPC API endpoints
  • WsRpcResource.test.ts - WebSocket RPC endpoints
  • auth.test.ts - Authentication
  • integration.test.ts - Integration tests
  • createJsonRPCClient.test.ts - HTTP client
  • createWsRPCClient.test.ts - WebSocket client

Development Setup

# Install dependencies
bun install

# Run tests
bun test

# Type check
bun run build

Package Structure

pkg/web-host/
├── index.ts # Main entry point and exports
├── plugin.ts # Plugin definition for TokenRing integration
├── package.json # Package manifest
├── WebHostService.ts # Main service implementation
├── StaticResource.ts # Static file resource
├── SPAResource.ts # SPA resource implementation
├── JsonRpcResource.ts # JSON-RPC resource implementation
├── WsRpcResource.ts # WebSocket RPC resource implementation
├── auth.ts # Authentication utilities
├── types.ts # Type definitions
├── schema.ts # Configuration schemas
├── createJsonRPCClient.ts # HTTP JSON-RPC client
├── createWsRPCClient.ts # WebSocket RPC client
├── commands/
│ └── webhost.ts # /webhost command
└── vitest.config.ts # Vitest configuration

API Reference

Exports from index.ts

export { default as WebHostService } from "./WebHostService.js";
export { default as StaticResource } from "./StaticResource.js";
export { default as JsonRpcResource } from "./JsonRpcResource.js";
export { default as WsRpcResource } from "./WsRpcResource.js";
export { default as SPAResource } from "./SPAResource.js";
export { default as createJsonRPCClient } from "./createJsonRPCClient.js";
export { default as createWsRPCClient } from "./createWsRPCClient.js";
export type { WebResource } from "./types.js";
export { AuthConfigSchema, WebHostConfigSchema } from "./schema.js";
export type { ParsedAuthConfig, ParsedWebHostConfig } from "./schema.js";
export { spaResourceConfigSchema } from "./SPAResource.js";
export { staticResourceConfigSchema } from "./StaticResource.js";

Exports from schema.ts

export const AuthConfigSchema: ZodSchema<ParsedAuthConfig>;
export const WebHostConfigSchema: ZodSchema<ParsedWebHostConfig>;
export type ParsedAuthConfig: z.output<typeof AuthConfigSchema>;
export type ParsedWebHostConfig: z.output<typeof WebHostConfigSchema>;

Exports from auth.ts

export function registerAuth(router: BunRouter, config: ParsedAuthConfig): void;
export function checkAuth(request: BunRequest, config: ParsedAuthConfig): string | null;
export function unauthorizedResponse(response: BunResponse): Response;

Exports from types.ts

export interface WebResource;
export interface BunRouter;
export interface BunRequest;
export interface BunResponse;
export type RouteHandler;
export interface WebSocketHandler;
export interface BunWebSocket;
export interface StaticOptions;

Dependencies

Production Dependencies

  • @tokenring-ai/app (0.2.0) - Base application framework with service management and plugin architecture
  • @tokenring-ai/agent (0.2.0) - Agent system with state management
  • @tokenring-ai/chat (0.2.0) - Chat service for human interaction
  • @tokenring-ai/utility (0.2.0) - Shared utilities and helpers
  • @tokenring-ai/rpc (0.2.0) - RPC endpoint registration and execution
  • zod (^4.3.6) - Schema validation

Development Dependencies

  • vitest (^4.1.0) - Testing framework
  • typescript (^5.9.3) - TypeScript compiler

Authentication

Basic Authentication

curl -u admin:admin123 http://localhost:3000/api/status

Bearer Token Authentication

curl -H "Authorization: Bearer admin-token" http://localhost:3000/api/status

Accessing User Information

When authentication is enabled, the authenticated username is available in request handlers:

router.get("/api/whoami", async (request: BunRequest, response: BunResponse) => {
const user = (request as any).user;
return response.json({ user });
});

Note: The user property is added to the request object by the authentication hook.

Error Handling

JSON-RPC Errors

The JSON-RPC implementation includes proper error handling with standard JSON-RPC 2.0 error codes:

Error CodeDescription
-32700Parse error (invalid JSON)
-32600Invalid Request (wrong JSON-RPC version)
-32601Method not found
-32603Internal error (validation or execution error)

HTTP Errors

  • 401 Unauthorized: Authentication required but not provided or invalid credentials
  • 404 Not Found: Resource not found (for static files)

WebSocket Errors

  • Connection errors are propagated to the client
  • Stream errors are sent as JSON-RPC error responses
  • @tokenring-ai/rpc - RPC endpoint definition and execution
  • @tokenring-ai/agent - Agent system integration
  • @tokenring-ai/app - Base application framework
  • @tokenring-ai/utility - KeyedRegistry and utilities

License

MIT License - see LICENSE file for details.