# Mockingbird Every service README, generated from the packages. The HTML site is a view of this same text. ----- # Junction Package: `@crvouga/mockingbird-service-junction` Tier: Ready Source: https://github.com/crvouga/mockingbird/tree/main/packages/service/junction # @crvouga/mockingbird-service-junction Stateful mock of the [Junction (formerly Vital) API](https://docs.junction.com/): users (`/v2/user`), the lab-testing catalog, lab orders (create, cancel, simulate, results, requisitions), at-home phlebotomy and patient-service-center (PSC) scheduling, and the `labtest.order.*` / `labtest.appointment.updated` webhooks. All 40 operations in the vendored OpenAPI subset are served, and behaviour is checked by differential property tests against the Junction sandbox. Use it to take Junction off your suite's critical path: no sandbox key, no shared 50-user sandbox cap (the mock enforces no sandbox limit unless a test [asks for one](#sandbox-limits)), no cross-suite interference. Serve it as a local origin with one command, or run it in-process. ```bash npx mockingbird-junction serve # http://127.0.0.1:8787, recorded sandbox corpus loaded ``` - Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/SUPPORT.md) - Drop-in readiness and the sandbox quirks the mock mirrors: [docs/drop-in.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/drop-in.md) - Behaviour notes: [docs/behavior.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/behavior.md) - **What the mock does not model:** [below](#what-is-and-is-not-modelled). Read it before trusting a green suite. ## Install ```bash npm install -D @crvouga/mockingbird-service-junction ``` ESM only; Node >= 22 or Bun >= 1.2 (CommonJS callers on Node >= 22.12 can `require()` it). No native dependencies: state lives in an in-memory SQLite engine written in TypeScript, bundled in. | Entry point | Runtime | What it is | | --- | --- | --- | | `@crvouga/mockingbird-service-junction` | any (Node, Bun, Workers, browsers) | `JunctionAPI`, `createRuntime`, corpus and verify tools | | `@crvouga/mockingbird-service-junction/server` | Node | `createServer()` — a listening HTTP server | | `@crvouga/mockingbird-service-junction/corpus` | any | `defaultCorpus` — the recording shipped in the package (a separate entry, so importing the mock never parses it) | | `mockingbird-junction` (bin) | Node | `serve`, `corpus pull`, `corpus diff`, `verify` | ## Usage ### `mockingbird-junction serve` ```bash npx mockingbird-junction serve --port 8787 npx mockingbird-junction serve --corpus ./test/junction-corpus.json --lab-accounts ./test/lab-accounts.json npx mockingbird-junction serve --webhook-url http://127.0.0.1:3100/webhooks/junction --webhook-secret whsec_... npx mockingbird-junction serve --help ``` | Flag | Default | Meaning | | --- | --- | --- | | `--port ` | `8787` | Port to listen on (`0` for any free port; the bound URL is printed) | | `--host ` | `127.0.0.1` | Interface to bind | | `--corpus ` | `default` | The shipped corpus, no corpus (synthetic data), or a file from `corpus pull` | | `--geo ` | `corpus` with a corpus | How unknown ZIPs are answered; see [Geo](#geo-corpus-or-synthetic) | | `--lab-accounts ` | built-in fixtures | [Lab accounts](#lab-accounts): a JSON array, or `{ "presets": [...], "accounts": [...] }` | | `--team-id ` | the corpus's team | The [team](#team-identity) the mock answers as | | `--max-users ` | unlimited | Enforce the sandbox's [live-user cap](#sandbox-limits) | | `--identity ` | `strict` | Unknown `user_id`s: 404, or [create on first use](#fixtures-and-identity) | | `--fixtures ` | — | [Users and orders](#fixtures-and-identity) every namespace starts with | | `--journal-size ` | `1000` | Requests each namespace's [journal](#is-this-the-mock) keeps | | `--webhook-url ` / `--webhook-secret ` | off | Deliver [signed webhooks](#webhooks) (secret also from `MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET`) | | `--webhook-retry-delays ` | Svix's schedule | Delay before each delivery attempt, e.g. `0,1000,5000` in tests | | `--webhook-scope ` | — | Sent as `x-mockingbird-scope` on every delivery | | `--admin-key ` | open | Require `x-mockingbird-admin-key` on `/__admin/*` (also `MOCKINGBIRD_ADMIN_KEY`) | | `--seed ` | `0` | Seeds fault rates and retry jitter | | `--log ` | `pretty` | One line per request: operation id, status, duration, namespace, fault | | `--log-requests` | — | Same as `--log json`: one JSON line per request with the ids it touched, never bodies | | `--seed-url ` / `--seed-key ` | — | Pull a corpus from a live team at boot. Slow and a live dependency; prefer a committed `corpus pull` file | | `--config ` | — | Serve every service in a `mockingbird.json` instead ([below](#many-services-from-one-config)) | At startup it prints the listen address, the loaded corpus (its version label, observation, ZIP, lab-test and lab-account counts, recording date and source), the geo mode, the team id, the lab-account count, which sandbox limits are on (normally none), the identity mode, and whether webhook delivery is on. ### Many services from one config Any Mockingbird service's CLI can boot every service in a config file, so a stack adds a config entry per vendor instead of a wrapper process per vendor. Each service named must be installed (`junction` loads `@crvouga/mockingbird-service-junction`). ```json { "log": "json", "services": { "junction": { "port": 8787, "adminKey": "local-admin", "options": { "corpus": "./test/junction-corpus.json", "webhook-url": "http://127.0.0.1:3100/webhooks/junction" } }, "stripe": { "port": 12111 } } } ``` ```bash npx mockingbird-junction serve --config mockingbird.json ``` `options` takes the service's own `serve` flags by long name. ### `createServer` (Node) ```ts import { createServer } from "@crvouga/mockingbird-service-junction/server" const server = await createServer() // shipped corpus, any free port const health = await fetch(`${server.url}/health`) console.log(health.status) // 200 // Point the Vital SDK (or your backend's Junction base URL) at server.url, then: await server.close() ``` `createServer(options)` takes every [`createRuntime`](#createruntime-any-fetch-server) option plus `port` (default `0`), `host` (default `127.0.0.1`) and `corpus` (`"default"`, `"none"`, a file path or a loaded corpus). The result has `url`, `port`, `runtime` and `close()`. ### `createRuntime` (any Fetch server) `createRuntime` is the whole served mock — health, admin, namespaces, clock, faults, metrics — as a single runtime-neutral `fetch(request)`. Hand it to `Bun.serve`, a Worker, or call it directly: ```ts import { createRuntime } from "@crvouga/mockingbird-service-junction" import { defaultCorpus } from "@crvouga/mockingbird-service-junction/corpus" const junction = createRuntime({ corpus: defaultCorpus }) const as = (worker: string) => ({ "x-vital-api-key": "sk_us_mockingbird", "x-mockingbird-namespace": worker, // isolates this worker's data "content-type": "application/json", }) await junction.fetch( new Request("http://junction.local/v2/user", { method: "POST", headers: as("worker-1"), body: JSON.stringify({ client_user_id: "patient-1" }), }), ) const other = await junction.fetch(new Request("http://junction.local/v2/user", { headers: as("worker-2") })) console.log(await other.json()) // worker-2 sees none of worker-1's users await junction.reset("worker-1") // or junction.reset("*") for every namespace ``` ### In-process (inject `fetch`) ```ts import { JunctionAPI } from "@crvouga/mockingbird-service-junction" const junction = new JunctionAPI({ now: () => Date.UTC(2030, 0, 1) }) const headers = { "x-vital-api-key": "sk_us_mockingbird", "content-type": "application/json" } const call = async (method: string, path: string, body?: unknown) => { const response = await junction.fetch( new Request(`https://api.sandbox.tryvital.io${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }), ) return { status: response.status, json: (await response.json()) as Record } } const user = await call("POST", "/v2/user", { client_user_id: "app-user-1" }) const userId = String(user.json.user_id) const catalog = await call("GET", "/v3/lab_test") const labTests = catalog.json.data as { id: string; method: string }[] const testkit = labTests.find((test) => test.method === "testkit") if (testkit === undefined) throw new Error("default catalog has a testkit test") const order = await call("POST", "/v3/order", { user_id: userId, patient_details: { first_name: "Ada", last_name: "Lovelace", dob: "1990-01-01", gender: "female", phone_number: "+14155551234", email: "ada@example.com", }, patient_address: { first_line: "1 N Central Ave", city: "Phoenix", state: "AZ", zip: "85004", country: "US", }, order_set: { lab_test_ids: [testkit.id] }, }) console.log(order.status, (order.json.order as { id: string }).id) // 200 "" // Every event the mock published, oldest first: console.log(junction.webhookEvents().map((event) => event.event_type)) // ["labtest.order.created"] ``` `now` (milliseconds) drives `created_on`/`updated_at` fields, webhook timestamps, and when delayed simulations (`POST /v3/order/{id}/test?final_status=completed&delay=`) become due; the delayed transition is applied on the next `GET /v3/order/{id}` whose `now` is past the due time. ### Pointing `@tryvital/vital-node` at it The official SDK takes the base URL as `environment` (this is how the package's SDK drop-in test constructs it): ```js import { VitalClient } from "@tryvital/vital-node" const client = new VitalClient({ apiKey: "sk_us_mockingbird", environment: baseUrl }) const user = await client.user.create({ clientUserId: "app-user-1" }) ``` If your app guards Junction hosts with an allowlist, allow `127.0.0.1` / `localhost` for tests while keeping the production and sandbox guards intact. ## The service contract Every Mockingbird service answers the same control surface, outside the vendor's auth gate: - `GET /health` — unauthenticated readiness probe: `{ "status": "ok", "service": "junction", "corpus": "