@microboxlabs/miot-harness-client
v0.2.0
Published
A zero-runtime-dep, typed HTTP + SSE client for the [`miot-harness`](../../../miot-harness) streaming API. Sibling of [`@microboxlabs/miot-calendar-client`](../miot-calendar-client) — same conventions, same tooling, different backend.
Readme
@microboxlabs/miot-harness-client
A zero-runtime-dep, typed HTTP + SSE client for the miot-harness streaming API. Sibling of @microboxlabs/miot-calendar-client — same conventions, same tooling, different backend.
Role in the ecosystem
This package is the one place where the harness HTTP + SSE contract is described in TypeScript. Every consumer — terminal, web, future tools — imports from here so they share the same types, the same error class, and the same wire-format guarantees.
miot-harness backend (FastAPI)
REST POST /runs:start
SSE GET /runs/{id}/stream
REST GET /runs/{id}
▲
│ HTTP + SSE (fetch)
┌──────────────────────────────────┐
│ @microboxlabs/ │ zero runtime deps
│ miot-harness-client │ dual ESM (.mjs) + CJS (.cjs)
│ (typed, hand-written, this pkg) │ shared by every consumer
└──────────────────────────────────┘
▲ ▲ ▲
│ │ │
┌──────────────────┐ ┌─────────────┐ ┌──────────────────┐
│ @microboxlabs/ │ │ @microboxlabs/│ │ turbo-repo/ │
│ miot-chat │ │ miot-cli │ │ apps/app │
│ live SSE REPL │ │ scripting │ │ (future) │
│ + slash commands │ │ create / get │ │ Next.js admin │
└──────────────────┘ └─────────────┘ └──────────────────┘| Consumer | Role | Status |
|---|---|---|
| @microboxlabs/miot-chat | Interactive Copilot-style REPL — owns the live SSE UX, slash commands, conversation memory. | ✅ Consumes this library |
| @microboxlabs/miot-cli | Operator CLI — miot harness create / miot harness runs get for scripting; no live streaming UX. | ✅ Consumes this library |
| turbo-repo/apps/app | Next.js admin — can consume from server-side API routes the same way it consumes @microboxlabs/miot-calendar-client. | 🔜 Future |
Why a separate library (and not a folder inside miot-chat)
- Single source of truth. Anytime the backend contract evolves, exactly one TypeScript surface needs to change. Admins, CLIs, agents, and future automation cannot drift apart on event shapes or error codes.
- Two surfaces, one engine. The interactive REPL and the scriptable
miot harness …subcommands share validation, error class, and request plumbing — a bug fixed inside this library is fixed everywhere at once. - Independent release cadence. Bump the CLI without touching the library, or ship a new library version and let consumers adopt it on their own schedule.
This is the same shape that @microboxlabs/miot-calendar-client has with miot-cli + apps/app — applied to the harness's streaming surface.
Install
Workspace consumers reference it as "@microboxlabs/miot-harness-client": "*". After publish:
npm install @microboxlabs/miot-harness-clientAuthentication
In production the harness sits behind the Quarkus reverse proxy at
/api/v1/orgs/{slug}/harness and rejects unauthenticated /runs*
requests. The Quarkus side terminates the user-facing Auth0 token
and resolves the caller's tenant from Alfresco; the harness
re-verifies the token as defense in depth. See the
Authentication doc
for the full trust model.
Practical implications for this client:
- Point
baseUrlat the proxy:https://api.miot.example.com/api/v1/orgs/{slug}/harness. Hitting the harness directly only works for local dev, where the harness defaults to its legacy unauthenticated mode. tokenis the Auth0 access token from the rest of the MIOT API. It must carry the harness audience and be unexpired; the client sends it asAuthorization: Bearer <token>. With auth enabled on the harness, missing or invalid tokens surface asMiotHarnessApiErrorwithcode: "http_401".tenant_idin the request body is ignored in production — the proxy injects anX-Miot-Tenant-Client-Idheader from the verified org membership, and the harness uses that value. The field is kept on the wire for now to preserve local-dev ergonomics; it will be removed from the schema in a follow-up release (issue #522).
Quickstart
import {
createMiotHarnessClient,
MiotHarnessApiError,
} from "@microboxlabs/miot-harness-client";
const client = createMiotHarnessClient({
// Production: hit the Quarkus proxy for the target org.
// Local dev: "http://localhost:8000" hits the harness directly
// (legacy unauthenticated mode).
baseUrl: "https://api.miot.example.com/api/v1/orgs/gama-mobility/harness",
token: process.env.MIOT_HARNESS_TOKEN, // Auth0 access token
});
// 1. Dispatch a run (async; returns immediately with the id).
// `tenant_id` is overridden server-side from the verified
// proxy header — pass anything in dev, ignore in prod.
const { run_id } = await client.runs.create({
message: "what's in stock?",
mode: "agentic",
conversation_id: crypto.randomUUID(),
});
// 2. Stream events as they arrive (SSE)
try {
for await (const evt of client.runs.stream(run_id)) {
console.log(evt.type, evt.message);
if (evt.type === "run.completed" || evt.type === "run.failed") break;
}
} catch (e) {
if (e instanceof MiotHarnessApiError) {
console.error(`harness error: ${e.code} — ${e.message}`);
} else {
throw e;
}
}
// 3. Fetch the authoritative record for the final answer
const record = await client.runs.get(run_id);
console.log(record.answer);API surface
createMiotHarnessClient(config: ClientConfig) => MiotHarnessClient| Group | Method | HTTP | Notes |
|---|---|---|---|
| runs | create(body, opts?) | POST /runs:start | Returns { run_id } (the harness assigns the id). opts.signal is forwarded to fetch. |
| runs | stream(id, opts?) | GET /runs/{id}/stream | AsyncIterable<HarnessEvent>. Pass opts.lastEventId for resume; pass opts.signal to cancel. Throws MiotHarnessApiError on event: error frames. |
| runs | get(id) | GET /runs/{id} | Full HarnessRunRecord with events[], answer, artifacts, conversation_id. |
ClientConfig
| Field | Type | Default |
|---|---|---|
| baseUrl | string | required. In prod set it to the Quarkus proxy path: …/api/v1/orgs/{slug}/harness |
| token | string \| null | undefined — no Authorization header sent. Required when targeting an auth-enabled harness (any production deployment); send the Auth0 access token from the rest of the MIOT API |
| headers | Record<string, string> | merged into every request (request-level headers win) |
| fetch | typeof globalThis.fetch | globalThis.fetch — pass a custom impl for testing or polyfills |
Errors
Every failure surfaces as MiotHarnessApiError:
class MiotHarnessApiError extends Error {
readonly code: string; // "http_404" | "unknown_run_id" | "no_body" | ...
readonly runId?: string; // set for stream/getRun errors
readonly body?: ErrorResponse | string;
readonly status?: number; // set for HTTP-origin errors
}- HTTP non-2xx →
code = http_<status>,statusset,bodyparsed (JSON) or string fallback. - SSE
event: errorframe →code= whatever the harness sent (e.g.unknown_run_id),runIdset, nostatus. - Stream body missing →
code = no_body. - Unparseable SSE payload →
code = unparseable_eventorunparseable_error.
Error.message is derived from body.message ?? body.error ?? body.detail so harness, FastAPI, and plain-text errors all surface a useful string.
Develop
npm run build # tsup, dual ESM (.mjs) + CJS (.cjs), dts on both
npm test # vitest
npm run lint
npm run check-types
npm pack --dry-run # confirm shapeDesign notes
- Zero runtime dependencies.
fetch,ReadableStream,TextDecoder,URLare native on Node ≥18 and any modern browser. - Dual ESM + CJS build with conditional
exportsso the package works under bothimportandrequirewithouttype: "module". - SSE parser is a pure async iterator over
ReadableStream<Uint8Array>— survives chunk-boundary splits, multi-linedata:fields,\r\n, leading-space stripping after:, comment lines, and the harness'sevent: errorclose frame. - Resource grouping (
runs.create / .stream / .get) mirrors@microboxlabs/miot-calendar-client'scalendars.* / slots.* / bookings.*so future harness resources (conversations, artifacts, etc.) drop in naturally.
