# @mochabug/adapt-sdk

Application APIs, routers, and browser utilities for Mochabug Adapt v2.

```bash
npm install @mochabug/adapt-sdk
npm install -D @mochabug/adapt-builder
```

Adaptkit 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:

```ts
// 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](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/).
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:

```ts
// 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](https://github.com/mochabug/adapt-builder#outbound-connections).

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 |

```ts
// 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](docs/routers.md).

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.

## Browser UI

Call `init({ resize: true })` from `/browser` once per document. Appearance follows
standard CSS `color-scheme`; see [browser appearance and resizing](docs/browser.md).
[`sample/`](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
