@mochabug/adapt-sdk
v2.0.0-beta.19
Published
Application APIs, routers, browser utilities, and shared contracts for Mochabug Adapt v2
Readme
@mochabug/adapt-sdk
Application APIs, routers, and browser utilities for Mochabug Adapt v2.
npm install @mochabug/adapt-sdk
npm install -D @mochabug/adapt-builderAdaptkit scaffolds the project and its TypeScript environments. Adapt Builder owns runtime declarations and builds the plugin. The SDK wraps runtime services and provides application utilities; it does not install ambient types.
| Import | Environment | Purpose |
| --- | --- | --- |
| @mochabug/adapt-builder/runtime | Adapt backend | Types for handlers, injected environments, HTTP, TCP, datagrams, and the three native mb:* modules |
| @mochabug/adapt-sdk/api | Adapt backend | Executor/configurator APIs and native Connect transport |
| @mochabug/adapt-sdk/router | Adapt backend | HTTP, Connect RPC, WebSocket, WebTransport, tunnels, lifecycle |
| @mochabug/adapt-sdk/browser | Browser | Plugin token and frame resizing |
| @mochabug/adapt-sdk/react | Browser | React components; CSS at @mochabug/adapt-sdk/react/styles.css |
| @mochabug/adapt-sdk/jtd | Any | RFC 8927 schema compilation, exact raw JSON validation, and compatibility checks |
| @mochabug/adapt-sdk/proto | Any | Generated protobuf messages and service descriptors |
| @mochabug/adapt-sdk/cel, /signals, /openai, /anthropic, /gemini | Any | Expressions, signals, and message conversion |
Backend TypeScript uses lib: ["ES2024", "DOM", "DOM.Iterable"] and
types: ["@mochabug/adapt-builder/runtime"], which includes Node 24 declarations.
Browser TypeScript uses lib: ["ES2024", "DOM", "DOM.Iterable"] and
types: ["vite/client"]. Config/SSG files have their own host-tools project.
TypeScript's DOM library supplies standard Fetch types such as HeadersInit,
which Connect RPC requires. Native interfaces are explicit imports from
@mochabug/adapt-builder/runtime; neither package adds standard global aliases.
Server rendering uses the same backend configuration. DOM declarations make
browser globals visible to TypeScript but do not provide window or document
in the runtime. Keep browser API use in browser code. All environments are
checked with skipLibCheck: false.
There is no SDK root import or builder browser-types wrapper.
Protobuf and Connect service definitions are shared unchanged between server and
browser. Only the transport changes: the runtime uses native HttpBinding;
browsers use @connectrpc/connect-web. Neutral SDK entries need neither the
builder nor Node types.
JTD schemas
/jtd owns schema and value validation; it has no third-party JTD dependency.
Compile a schema once with compile(schema), then reuse .validate(value),
.validateJson(source) or .subsumes(target). An empty error array means a
match. Invalid schemas, invalid JSON, and resource limits throw distinct errors.
Use raw JSON validation when integer precision matters: JavaScript numbers may
already have been rounded before .validate(value) receives them.
The generated JTDSchemaSchema accepts ordinary JTD JSON with fromJson or
fromJsonString using their defaults. JTD JSON uses camelCase keywords, including
optionalProperties and additionalProperties; default protobuf JSON output
uses the same names.
Empty properties/optionalProperties maps retain their presence through binary
and JSON round trips. Known schema keywords set to null are treated as omitted,
including nested schemas; unknown
keywords, null schema-map entries, and null enum members remain invalid. Opaque
metadata and instance nulls retain their meaning. normalize(schema) validates
and omits null schema keywords, preserving metadata
and user-defined property, definition, and tag names.
Numbers inside opaque metadata use standard ProtoJSON/Struct double precision;
round trips do not preserve arbitrary numeric precision there. Instance validation
uses the raw JSON validator to retain numeric lexemes.
Validation bounds are exported as limits. They include 16 MiB of JSON, schema
and reference depth 32, JSON depth 128, 16,384 schema nodes, 1,048,576 JSON values
and validation work units, and 100 diagnostics totaling at most 64 KiB. Limit
exhaustion never indicates success. Timestamps use RFC 3339 calendar and offset
rules and the 27 announced leap seconds through IERS Bulletin C 72 (July 2026);
a new leap announcement requires updating the SDK and platform tables together.
Beta.11 changes the JTD protobuf wire representation. Rebuild plugins against this version; SDK code does not read legacy persisted messages.
HTTP, TCP, and datagram exports
In src/<vertex>/executor.ts, export the handlers enabled by vertex.json.
Each receives (streamOrSocket, env, ctx); its environment is inferred from the
explicit handler type:
// runtime/native.ts
import type { ExecutorEnvironment, ExportedHandler } from '@mochabug/adapt-builder/runtime';
export const external_http = {
async http(stream) {
if (stream.method !== 'GET' || stream.path !== '/health') {
await stream.respond({ status: 404 }).end();
return;
}
await stream.respond({ headers: { 'content-type': 'application/json' } }).json({ ok: true });
},
} satisfies ExportedHandler<ExecutorEnvironment>;
export const external_connect = {
async connect(socket) {
await socket.readable.pipeTo(socket.writable);
},
} satisfies ExportedHandler<ExecutorEnvironment>;
export const external_datagram = {
async datagram(socket) {
for (;;) {
const packet = await socket.receive();
if (packet === null) return;
await socket.send(packet);
}
},
} satisfies ExportedHandler<ExecutorEnvironment>;TCP connect is a raw socket handler. Connect RPC is an HTTP service.
Credit to Cloudflare for the TCP Socket interface design, familiar from
Workers' connect() API.
This attribution applies specifically to the interface design.
Datagrams are RFC 9221 QUIC DATAGRAM, with one socket per connection. Keep the
handler pending for the entire session. Await sends; a send may transfer the
buffer, so do not reuse it afterwards. maxDatagramSize is available after
socket.opened resolves.
All executors also need internal_http for platform lifecycle RPCs:
// runtime/lifecycle.ts
import { createIERouter } from '@mochabug/adapt-sdk/router';
export const internal_http = createIERouter((router) => {
router.onStart(async (_request, api) => {
console.log(await api.getConfig());
});
});env.assets.fetch() reads managed assets. env.httpproxy, env.plugin,
env.fileforge, and the role's env.executor or env.configurator expose
.http(). SDK API classes wrap these bindings; router callbacks receive the
appropriate API instance. Import outbound http from mb:http, raw TCP
connect from mb:connect, and QUIC datagram from mb:datagram. Each module
exports only its named function; see the builder's native examples.
Native calls use binding.http(url, { method, headers, signal }) or
http(url, options). The returned exchange owns the upload through end(body),
write(), or writable; it snapshots metadata and preserves the supplied signal.
The SDK's userHttpProxy() and systemHttpProxy() keep their Fetch-compatible
(input, init) interface, including Request bodies and cancellation. Their
adapter forwards metadata directly and allows a response to arrive during an
unfinished upload.
HTTP routers
Router factories return the native { http } handler directly. This executor
example combines HTTP, SSE, Connect RPC, WebSocket, WebTransport, and tunnels.
EchoService is your Buf-generated service descriptor with unary echo and
server-streaming watch methods, each using messages with a text field.
| Factory | Role | Configuration |
| --- | --- | --- |
| createEERouter | External executor | Middleware and routes |
| createIERouter | Internal executor | Middleware and onStart, onStop, onExchange, onCron |
| createECRouter | External configurator | Middleware and routes |
| createICRouter | Internal configurator | No callback; no incoming application protocol |
// runtime/routes.ts
import { setTimeout as delay } from 'node:timers/promises';
import { createEERouter } from '@mochabug/adapt-sdk/router';
import { EchoService } from '../gen/echo_pb.js';
export const external_http = createEERouter((router) => {
router.useH3Upgrade();
router.use(async (exchange, next) => {
exchange.response.headers.set('x-content-type-options', 'nosniff');
await next();
});
router.addRoute('GET', '/users/:id', async (_request, api, route) => {
return Response.json({ id: route.params.id, config: await api.getConfig() });
});
router.addRoute('GET', '/events', (request) => {
const cancelled = new AbortController();
const signal = AbortSignal.any([request.signal, cancelled.signal]);
const encoder = new TextEncoder();
const events = new ReadableStream<Uint8Array>({
async pull(controller) {
await delay(1000, undefined, { signal });
controller.enqueue(encoder.encode(`event: tick\ndata: ${Date.now()}\n\n`));
},
cancel() { cancelled.abort(); },
});
return new Response(events, {
headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' },
});
});
router.addConnect(EchoService, {
echo: (request) => ({ text: request.text }),
async *watch(request, context) {
for (let i = 1; i <= 3; i++) {
context.signal.throwIfAborted();
yield { text: `${request.text}:${i}` };
}
},
}, { grpc: true, grpcWeb: true, connect: true });
router.addWebSocket('/socket', async (socket) => {
for (;;) {
const message = await socket.receive();
if (message === null) return;
await socket.send(message.data, { type: message.type });
}
});
router.addWebTransport('/transport', async (transport) => {
for (;;) {
const packet = await transport.datagrams.receive();
if (packet === null) return;
await transport.datagrams.send(packet);
}
});
router.addTunnel('/tunnel', async (tunnel) => {
await tunnel.readable.pipeTo(tunnel.writable);
}, { protocol: 'bytestream' });
});HTTP middleware covers ordinary requests, RPCs, and accepted HTTP sessions.
Set response headers before next(); call it once or return a Response to
reject the request. useH3Upgrade() advertises HTTP/3. Add
router.useBearerAuthorization(['/private']) to authorize matching paths before
user middleware or transport acceptance. Routes and protected prefixes are
case-sensitive. Bearer credentials use the canonical Authorization: Bearer <token>
form. The platform's x-mochabug-adapt-plugin-token supplies trusted route context;
caller authorization is separate. Raw TCP and datagram exports bypass
HTTP middleware. See middleware and lifecycle details.
Plugin services always use the trusted platform token. Executor configuration
and persistent stores also use it, including inside lifecycle callbacks. Session
stores, signals, sends, and exchanges use the session caller token; getAuthHeader()
returns only that caller header. Each API instance verifies a caller against its
trusted route before the first caller-scoped RPC.
Direct configurator construction accepts separate credentials:
new ConfiguratorApi(env, platformToken, callerAuthorizationHeader). Configuration,
incoming signals, and configurator stores retain the caller's permissions. Omitting
the third argument permits plugin services, assets, and caller verification, but
configuration operations fail with Unauthenticated.
All backend API constructors require a valid, nonempty platform token.
RPC failures
SDK API calls do not automatically replay failed RPCs. Returned application
statuses, including ResourceExhausted and Unavailable, propagate to the caller.
A lost reply can mean the operation already ran. Only the native transport can
recover a request it knows was unprocessed.
Browser UI
Call init({ resize: true }) from /browser once per document. Appearance follows
standard CSS color-scheme; see browser appearance and resizing.
sample/ is the runnable React showcase (npm run sample). The usage
examples above live only in this README and are compiled and exercised by tests.
Development
npm run build generates protos, checks the separate environments, runs tests,
and builds ESM bundles and declarations. npm run test:types checks packed
package consumers in browser and Node environments, plus the README examples.
License
Apache-2.0
