@crvouga/mockingbird-service-junction 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +448 -133
  3. package/dist/chunk-4C6VYRJN.js +18 -0
  4. package/dist/chunk-4C6VYRJN.js.map +7 -0
  5. package/dist/chunk-KKNULV7X.js +406 -0
  6. package/dist/chunk-KKNULV7X.js.map +7 -0
  7. package/dist/chunk-MA3HSUVD.js +7771 -0
  8. package/dist/chunk-MA3HSUVD.js.map +7 -0
  9. package/dist/chunk-MD4KYEIM.js +145758 -0
  10. package/dist/chunk-MD4KYEIM.js.map +7 -0
  11. package/dist/cli.js +214 -0
  12. package/dist/cli.js.map +7 -0
  13. package/dist/corpus.d.ts +97 -0
  14. package/dist/corpus.js +8 -0
  15. package/dist/corpus.js.map +7 -0
  16. package/dist/index.d.ts +1131 -21
  17. package/dist/index.js +82 -120
  18. package/dist/index.js.map +7 -1
  19. package/dist/server.d.ts +978 -0
  20. package/dist/server.js +16 -0
  21. package/dist/server.js.map +7 -0
  22. package/package.json +32 -12
  23. package/dist/catalog.d.ts +0 -63
  24. package/dist/catalog.d.ts.map +0 -1
  25. package/dist/catalog.js +0 -736
  26. package/dist/catalog.js.map +0 -1
  27. package/dist/coverage-corpus.d.ts +0 -76
  28. package/dist/coverage-corpus.d.ts.map +0 -1
  29. package/dist/coverage-corpus.js +0 -166
  30. package/dist/coverage-corpus.js.map +0 -1
  31. package/dist/generated/openapi.d.ts +0 -7
  32. package/dist/generated/openapi.d.ts.map +0 -1
  33. package/dist/generated/openapi.js +0 -5
  34. package/dist/generated/openapi.js.map +0 -1
  35. package/dist/index.d.ts.map +0 -1
  36. package/dist/lab-accounts.d.ts +0 -38
  37. package/dist/lab-accounts.d.ts.map +0 -1
  38. package/dist/lab-accounts.js +0 -213
  39. package/dist/lab-accounts.js.map +0 -1
  40. package/dist/orders.d.ts +0 -18
  41. package/dist/orders.d.ts.map +0 -1
  42. package/dist/orders.js +0 -1247
  43. package/dist/orders.js.map +0 -1
  44. package/dist/prefetch.d.ts +0 -43
  45. package/dist/prefetch.d.ts.map +0 -1
  46. package/dist/prefetch.js +0 -131
  47. package/dist/prefetch.js.map +0 -1
  48. package/dist/reshape.d.ts +0 -22
  49. package/dist/reshape.d.ts.map +0 -1
  50. package/dist/reshape.js +0 -190
  51. package/dist/reshape.js.map +0 -1
  52. package/dist/results.d.ts +0 -19
  53. package/dist/results.d.ts.map +0 -1
  54. package/dist/results.js +0 -221
  55. package/dist/results.js.map +0 -1
  56. package/dist/scheduling.d.ts +0 -113
  57. package/dist/scheduling.d.ts.map +0 -1
  58. package/dist/scheduling.js +0 -1508
  59. package/dist/scheduling.js.map +0 -1
  60. package/dist/sealed-corpus.d.ts +0 -28
  61. package/dist/sealed-corpus.d.ts.map +0 -1
  62. package/dist/sealed-corpus.js +0 -17
  63. package/dist/sealed-corpus.js.map +0 -1
  64. package/dist/seed-from.d.ts +0 -27
  65. package/dist/seed-from.d.ts.map +0 -1
  66. package/dist/seed-from.js +0 -523
  67. package/dist/seed-from.js.map +0 -1
  68. package/dist/seeds.d.ts +0 -9
  69. package/dist/seeds.d.ts.map +0 -1
  70. package/dist/seeds.js +0 -9
  71. package/dist/seeds.js.map +0 -1
  72. package/dist/state.d.ts +0 -270
  73. package/dist/state.d.ts.map +0 -1
  74. package/dist/state.js +0 -448
  75. package/dist/state.js.map +0 -1
  76. package/dist/users.d.ts +0 -13
  77. package/dist/users.d.ts.map +0 -1
  78. package/dist/users.js +0 -669
  79. package/dist/users.js.map +0 -1
package/README.md CHANGED
@@ -1,20 +1,25 @@
1
1
  # @crvouga/mockingbird-service-junction
2
2
 
3
- Stateful, in-process mock of the [Junction (formerly Vital) API](https://docs.junction.com/) for
4
- test suites: users (`/v2/user`), the lab-testing catalog, lab orders (create, cancel, simulate,
5
- results, requisitions), and at-home phlebotomy / patient-service-center (PSC) scheduling
6
- (availability, booking, reschedule, cancel), plus the `labtest.order.*` and
7
- `labtest.appointment.updated` webhook events. All 39 operations in the vendored OpenAPI subset are
8
- served; behaviour is verified by differential property tests against the Junction sandbox.
3
+ Stateful mock of the [Junction (formerly Vital) API](https://docs.junction.com/): users
4
+ (`/v2/user`), the lab-testing catalog, lab orders (create, cancel, simulate, results,
5
+ requisitions), at-home phlebotomy and patient-service-center (PSC) scheduling, and the
6
+ `labtest.order.*` / `labtest.appointment.updated` webhooks. All 39 operations in the vendored
7
+ OpenAPI subset are served, and behaviour is checked by differential property tests against the
8
+ Junction sandbox.
9
9
 
10
- Use it when your backend calls Junction via `fetch` or `@tryvital/vital-node` and you want tests to
11
- run offline, deterministically, with no sandbox key.
10
+ Use it to take Junction off your suite's critical path: no sandbox key, no shared 50-user sandbox
11
+ cap, no cross-suite interference. Serve it as a local origin with one command, or run it in-process.
12
+
13
+ ```bash
14
+ npx mockingbird-junction serve # http://127.0.0.1:8787, recorded sandbox corpus loaded
15
+ ```
12
16
 
13
17
  - Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/SUPPORT.md)
14
- - Drop-in readiness, sealed corpus and sandbox quirks the mock mirrors:
18
+ - Drop-in readiness and the sandbox quirks the mock mirrors:
15
19
  [docs/drop-in.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/drop-in.md)
16
- - Consumer wiring checklist: [docs/qa-followon.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/qa-followon.md)
17
20
  - Behaviour notes: [docs/behavior.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/behavior.md)
21
+ - **What the mock does not model:** [below](#what-is-and-is-not-modelled). Read it before trusting a
22
+ green suite.
18
23
 
19
24
  ## Install
20
25
 
@@ -22,23 +27,118 @@ run offline, deterministically, with no sandbox key.
22
27
  npm install -D @crvouga/mockingbird-service-junction
23
28
  ```
24
29
 
25
- ESM only. Requires Node >= 22 or Bun >= 1.2. No native dependencies: state lives in an in-memory
26
- SQLite engine (`@crvouga/mockingbird-service-sqlite`, pure TypeScript). To serve it over HTTP also
27
- install `@crvouga/mockingbird-adapter-node` (works under Node and Bun) or
28
- `@crvouga/mockingbird-adapter-bun`.
30
+ ESM only; Node >= 22 or Bun >= 1.2 (CommonJS callers on Node >= 22.12 can `require()` it). No
31
+ native dependencies: state lives in an in-memory SQLite engine written in TypeScript, bundled in.
32
+
33
+ | Entry point | Runtime | What it is |
34
+ | --- | --- | --- |
35
+ | `@crvouga/mockingbird-service-junction` | any (Node, Bun, Workers, browsers) | `JunctionAPI`, `createRuntime`, corpus and verify tools |
36
+ | `@crvouga/mockingbird-service-junction/server` | Node | `createServer()` — a listening HTTP server |
37
+ | `@crvouga/mockingbird-service-junction/corpus` | any | `defaultCorpus` — the recording shipped in the package (a separate entry, so importing the mock never parses it) |
38
+ | `mockingbird-junction` (bin) | Node | `serve`, `corpus pull`, `corpus diff`, `verify` |
29
39
 
30
40
  ## Usage
31
41
 
32
- Behaviour the examples rely on (all from the source):
42
+ ### `mockingbird-junction serve`
43
+
44
+ ```bash
45
+ npx mockingbird-junction serve --port 8787
46
+ npx mockingbird-junction serve --corpus ./test/junction-corpus.json --lab-accounts ./test/lab-accounts.json
47
+ npx mockingbird-junction serve --webhook-url http://127.0.0.1:3100/webhooks/junction --webhook-secret whsec_...
48
+ npx mockingbird-junction serve --help
49
+ ```
50
+
51
+ | Flag | Default | Meaning |
52
+ | --- | --- | --- |
53
+ | `--port <n>` | `8787` | Port to listen on (`0` for any free port; the bound URL is printed) |
54
+ | `--host <host>` | `127.0.0.1` | Interface to bind |
55
+ | `--corpus <default\|none\|file>` | `default` | The shipped corpus, no corpus (synthetic data), or a file from `corpus pull` |
56
+ | `--geo <corpus\|synthetic>` | `corpus` with a corpus | How unknown ZIPs are answered; see [Geo](#geo-corpus-or-synthetic) |
57
+ | `--lab-accounts <file>` | built-in fixtures | JSON array of [lab accounts](#lab-accounts) |
58
+ | `--webhook-url <url>` / `--webhook-secret <whsec_…>` | off | Deliver [signed webhooks](#webhooks) (secret also from `MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET`) |
59
+ | `--webhook-retry-delays <ms,…>` | Svix's schedule | Delay before each delivery attempt, e.g. `0,1000,5000` in tests |
60
+ | `--webhook-scope <scope>` | — | Sent as `x-mockingbird-scope` on every delivery |
61
+ | `--admin-key <key>` | open | Require `x-mockingbird-admin-key` on `/__admin/*` (also `MOCKINGBIRD_ADMIN_KEY`) |
62
+ | `--seed <seed>` | `0` | Seeds fault rates and retry jitter |
63
+ | `--log <pretty\|json\|off>` | `pretty` | One line per request: operation id, status, duration, namespace, fault |
64
+ | `--seed-url <url>` / `--seed-key <key>` | — | Pull a corpus from a live team at boot. Slow and a live dependency; prefer a committed `corpus pull` file |
65
+ | `--config <file>` | — | Serve every service in a `mockingbird.json` instead ([below](#many-services-from-one-config)) |
66
+
67
+ At startup it prints the listen address, the loaded corpus (its version label, observation, ZIP,
68
+ lab-test and lab-account counts, recording date and source), the geo mode, and whether webhook
69
+ delivery is on.
70
+
71
+ ### Many services from one config
72
+
73
+ Any Mockingbird service's CLI can boot every service in a config file, so a stack adds a config
74
+ entry per vendor instead of a wrapper process per vendor. Each service named must be installed
75
+ (`junction` loads `@crvouga/mockingbird-service-junction`).
76
+
77
+ ```json
78
+ {
79
+ "log": "json",
80
+ "services": {
81
+ "junction": {
82
+ "port": 8787,
83
+ "adminKey": "local-admin",
84
+ "options": { "corpus": "./test/junction-corpus.json", "webhook-url": "http://127.0.0.1:3100/webhooks/junction" }
85
+ },
86
+ "stripe": { "port": 12111 }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ```bash
92
+ npx mockingbird-junction serve --config mockingbird.json
93
+ ```
94
+
95
+ `options` takes the service's own `serve` flags by long name.
96
+
97
+ ### `createServer` (Node)
98
+
99
+ ```ts
100
+ import { createServer } from "@crvouga/mockingbird-service-junction/server"
101
+
102
+ const server = await createServer() // shipped corpus, any free port
103
+ const health = await fetch(`${server.url}/health`)
104
+ console.log(health.status) // 200
105
+
106
+ // Point the Vital SDK (or your backend's Junction base URL) at server.url, then:
107
+ await server.close()
108
+ ```
109
+
110
+ `createServer(options)` takes every [`createRuntime`](#createruntime-any-fetch-server) option plus
111
+ `port` (default `0`), `host` (default `127.0.0.1`) and `corpus` (`"default"`, `"none"`, a file path
112
+ or a loaded corpus). The result has `url`, `port`, `runtime` and `close()`.
113
+
114
+ ### `createRuntime` (any Fetch server)
33
115
 
34
- - **Any host works.** Routing uses only the path (`/v2/...`, `/v3/...`).
35
- - **Auth:** every request needs an `x-vital-api-key` header; any value is accepted (sandbox keys
36
- look like `sk_us_*` / `sk_eu_*`). Without it the response is `401 {"detail":"Missing x-vital-api-key"}`.
37
- State is not partitioned by key: all requests share one team (`team_id`
38
- `11111111-1111-4111-8111-111111111111`).
39
- - Bodies are JSON with the API's snake_case field names.
40
- - A default synthetic catalog is installed at construction (lab tests with methods
41
- `walk_in_test`, `testkit` and `at_home_phlebotomy`); `installCorpus` replaces it with a recording.
116
+ `createRuntime` is the whole served mock — health, admin, namespaces, clock, faults, metrics — as a
117
+ single runtime-neutral `fetch(request)`. Hand it to `Bun.serve`, a Worker, or call it directly:
118
+
119
+ ```ts
120
+ import { createRuntime } from "@crvouga/mockingbird-service-junction"
121
+ import { defaultCorpus } from "@crvouga/mockingbird-service-junction/corpus"
122
+
123
+ const junction = createRuntime({ corpus: defaultCorpus })
124
+ const as = (worker: string) => ({
125
+ "x-vital-api-key": "sk_us_mockingbird",
126
+ "x-mockingbird-namespace": worker, // isolates this worker's data
127
+ "content-type": "application/json",
128
+ })
129
+
130
+ await junction.fetch(
131
+ new Request("http://junction.local/v2/user", {
132
+ method: "POST",
133
+ headers: as("worker-1"),
134
+ body: JSON.stringify({ client_user_id: "patient-1" }),
135
+ }),
136
+ )
137
+ const other = await junction.fetch(new Request("http://junction.local/v2/user", { headers: as("worker-2") }))
138
+ console.log(await other.json()) // worker-2 sees none of worker-1's users
139
+
140
+ await junction.reset("worker-1") // or junction.reset("*") for every namespace
141
+ ```
42
142
 
43
143
  ### In-process (inject `fetch`)
44
144
 
@@ -96,26 +196,6 @@ console.log(junction.webhookEvents().map((event) => event.event_type)) // ["labt
96
196
  simulations (`POST /v3/order/{id}/test?final_status=completed&delay=<seconds>`) become due; the
97
197
  delayed transition is applied on the next `GET /v3/order/{id}` whose `now` is past the due time.
98
198
 
99
- ### Over HTTP
100
-
101
- ```ts
102
- import { serve } from "@crvouga/mockingbird-adapter-node"
103
- import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
104
-
105
- const junction = new JunctionAPI()
106
- const server = await serve(junction, { port: 0, host: "127.0.0.1" }) // port 0 = ephemeral
107
- const address = server.address()
108
- if (address === null || typeof address === "string") throw new Error("not listening")
109
- const baseUrl = `http://127.0.0.1:${address.port}`
110
-
111
- const response = await fetch(`${baseUrl}/v2/user`, {
112
- headers: { "x-vital-api-key": "sk_us_mockingbird" },
113
- })
114
- console.log(response.status) // 200
115
-
116
- server.close()
117
- ```
118
-
119
199
  ### Pointing `@tryvital/vital-node` at it
120
200
 
121
201
  The official SDK takes the base URL as `environment` (this is how the package's SDK drop-in test
@@ -131,121 +211,355 @@ const user = await client.user.create({ clientUserId: "app-user-1" })
131
211
  If your app guards Junction hosts with an allowlist, allow `127.0.0.1` / `localhost` for tests
132
212
  while keeping the production and sandbox guards intact.
133
213
 
134
- ### Webhooks
214
+ ## The service contract
135
215
 
136
- `onWebhook` is called synchronously with every event the mock records
137
- (`labtest.order.created`, `labtest.order.updated`, `labtest.appointment.updated`). Delivery and
138
- signing are up to you. Junction delivers through Svix; the contributor server script signs with
139
- `svix-id`, `svix-timestamp` and `svix-signature: v1,<base64 HMAC-SHA256 of "<timestamp>.<body>">`,
140
- keyed by the base64-decoded secret after stripping a `whsec_` prefix:
216
+ Every Mockingbird service answers the same control surface, outside the vendor's auth gate:
141
217
 
142
- ```ts
143
- import { createHmac, randomUUID } from "node:crypto"
144
- import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
218
+ - `GET /health` — unauthenticated readiness probe:
219
+ `{ "status": "ok", "service": "junction", "corpus": "<label>", "geo": "corpus", … }`.
220
+ - `/__admin/*` — the control plane. Open by default; with `--admin-key` / `adminKey` it requires
221
+ `x-mockingbird-admin-key`, which is separate from any vendor key. Admin errors have one shape,
222
+ `{ "error": { "type": "mockingbird_admin", "message": "…" } }`, so they can never be confused
223
+ with a Junction error. `GET /__admin` lists every route.
224
+ - `x-mockingbird-namespace: <name>` — isolates a request's data (`[A-Za-z0-9_.-]{1,64}`). Each
225
+ namespace is a separate team over one shared database, so parallel workers share one process
226
+ without seeing each other. The corpus is shared read-only; creating a namespace is cheap. Admin
227
+ routes take the namespace from `?namespace=`, then the header, then `default`.
145
228
 
146
- const WEBHOOK_URL = "http://127.0.0.1:3100/webhooks/junction"
147
- const WEBHOOK_SECRET = "whsec_bW9ja2luZ2JpcmQtdGVzdC1zZWNyZXQ="
148
-
149
- const junction = new JunctionAPI({
150
- onWebhook: (event) => {
151
- const timestamp = Math.floor(Date.now() / 1000).toString()
152
- const body = JSON.stringify(event)
153
- const key = Buffer.from(WEBHOOK_SECRET.replace(/^whsec_/, ""), "base64")
154
- const signature = createHmac("sha256", key).update(`${timestamp}.${body}`).digest("base64")
155
- void fetch(WEBHOOK_URL, {
156
- method: "POST",
157
- headers: {
158
- "content-type": "application/json",
159
- "svix-id": randomUUID(),
160
- "svix-timestamp": timestamp,
161
- "svix-signature": `v1,${signature}`,
162
- },
163
- body,
164
- }).catch(() => undefined)
165
- },
166
- })
167
- console.log(junction.webhookEvents().length) // 0
229
+ | Route | Does |
230
+ | --- | --- |
231
+ | `POST /__admin/reset` | Reset the request's namespace; `?all=1` resets every namespace |
232
+ | `GET /__admin/namespaces` | Namespaces created so far |
233
+ | `POST /__admin/snapshots` | Snapshot the namespace; returns `{ id }` |
234
+ | `POST /__admin/snapshots/{id}/restore` | Restore it (an assignment: later writes are discarded) |
235
+ | `DELETE /__admin/snapshots/{id}` | Forget a snapshot |
236
+ | `GET` / `POST /__admin/clock` | Read or move the clock: `{ "set": "2030-01-01T00:00:00Z" }`, `{ "advance": "2h" }`, `{ "freeze": true }`, `{ "reset": true }` |
237
+ | `GET` / `POST` / `DELETE /__admin/faults` | List, add or clear [fault rules](#faults-and-error-shapes) (`DELETE ?id=` removes one) |
238
+ | `POST /__admin/faults/presets/{name}` | Add a named Junction fault; body may override `count`, `rate`, `operationId`… |
239
+ | `GET /__admin/metrics` | Requests by operation and status, fault count, and **unmatched routes** |
240
+ | `GET /__admin/orders` | Orders in the namespace with their current status |
241
+ | `POST /__admin/orders/{id}/transition` | Move an order: `{ "to": "completed", "result": "abnormal" }` ([order control](#order-control)) |
242
+ | `GET` / `PUT /__admin/results/{id}` | Read or install an exact result payload / PDF for an order |
243
+ | `GET /__admin/result-fixtures` | The named results |
244
+ | `GET` / `PUT /__admin/lab-accounts` | Read or replace the namespace's [lab accounts](#lab-accounts) (`{ "accounts": null }` restores the default) |
245
+ | `GET /__admin/corpus`, `PUT /__admin/geo` | The loaded corpus; switch [geo mode](#geo-corpus-or-synthetic) |
246
+ | `GET /__admin/webhooks` | [Deliveries](#webhooks) with every attempt |
247
+ | `POST /__admin/webhooks/{id}/replay`, `POST /__admin/webhooks/flush` | Redeliver one message; run pending retries now |
248
+ | `GET /__admin/webhooks/events` | Every event recorded, delivered or not |
249
+
250
+ `GET /__admin/metrics` → `unmatched` counts requests to paths the contract does not have. It is how
251
+ a new SDK call shows up — as a count — before it fails a suite as a 404.
252
+
253
+ ## Corpus
254
+
255
+ Provider-owned inventory — area serviceability, PSC site lists, the lab catalog, labs, lab
256
+ accounts — cannot be synthesized faithfully. A corpus is an exact recording of those
257
+ parameter-stable reads, and the mock answers them byte for byte.
258
+
259
+ **The shipped corpus** (`defaultCorpus`, and what `serve` loads unless told otherwise) was recorded
260
+ from the Junction sandbox. It covers area and PSC serviceability for 57 ZIPs across the US, the PSC
261
+ labs those ZIPs reach, and the recording team's catalog. The catalog and lab accounts belong to the
262
+ **recording team**: lab-test ids are per team, so your product's ids will not be in it. For your
263
+ catalog and your lab accounts, record your own team and commit the file:
264
+
265
+ ```bash
266
+ npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --out test/junction-corpus.json
267
+ npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --base test/junction-corpus.json \
268
+ --zip 10001,94105 --coverage-only --out test/junction-corpus.json # add ZIPs to it
269
+ npx mockingbird-junction corpus diff test/junction-corpus.json /tmp/fresh.json # exit 1 when they differ
168
270
  ```
169
271
 
170
- `webhookDeliveryAttempts()` returns the retry schedule Junction would follow for each event
171
- (8 attempts: immediately, then 5s, 5m, 30m, 2h, 5h, 10h, 10h, with jitter controlled by the
172
- `webhook` option). It is a record only; nothing is re-sent.
272
+ `corpus pull` only issues GETs, refuses non-sandbox keys (`sk_us_…` / `sk_eu_…`) unless given
273
+ `--allow-any-key`, and stamps the file with a SHA-256 `fingerprint` of its content (not its
274
+ recording time), so re-pulling unchanged data yields the same fingerprint. Availability is never
275
+ recorded: its slot dates and single-use `booking_key`s cannot be replayed, so it always comes from
276
+ the deterministic generator.
277
+
278
+ ### Geo: corpus or synthetic
279
+
280
+ With a corpus loaded, serviceability reads (`area/info`, `psc/info`, phlebotomy and PSC
281
+ availability) answer **only** for ZIPs the corpus covers. Any other well-formed ZIP gets:
282
+
283
+ ```json
284
+ {
285
+ "detail": {
286
+ "error_type": "MOCKINGBIRD_UNKNOWN_ZIP",
287
+ "error_message": "ZIP 00501 is not in the loaded Junction corpus (57 ZIPs covered). Record it with `mockingbird-junction corpus pull --zip 00501`, or serve with --geo synthetic to invent coverage.",
288
+ "zip_code": "00501",
289
+ "corpus": "v1-2026-09-18-HqFDuEAuhkwD"
290
+ }
291
+ }
292
+ ```
173
293
 
174
- ### Resetting between tests
294
+ with status **424**, which Junction never sends and HTTP clients do not retry: a fixture gap fails
295
+ loudly instead of silently disagreeing with production. A malformed ZIP still gets Junction's own
296
+ 422. `--geo synthetic` (the default without a corpus) invents plausible, deterministic coverage
297
+ instead — right for property tests that walk random ZIPs, wrong for a suite standing in for
298
+ production.
175
299
 
176
- `reset()` clears users, orders, appointments and webhook events, then re-installs the default
177
- catalog, or the corpus passed to `installCorpus` if there was one:
300
+ ## Lab accounts
178
301
 
179
- ```ts
180
- import { beforeEach, expect, test } from "bun:test"
181
- import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
302
+ `create_order` routes, accepts and rejects `lab_account_id` against the team's lab accounts,
303
+ following Junction's documented rules: an explicit id must exist, be linked to the team, belong to
304
+ the ordered lab and be active; with no id, one active account for the lab is selected, several are
305
+ an error, and a lab with none falls back to Junction's platform account. The billing type must be
306
+ allowed in the patient's state by the account's `allowed_billing`.
182
307
 
183
- const junction = new JunctionAPI()
184
- beforeEach(() => junction.reset())
308
+ The accounts come from, in order: `labAccounts` / `--lab-accounts`, else the corpus (a pulled
309
+ corpus carries your team's real accounts), else built-in fixtures. Configure them as
310
+ `ClientFacingLabAccount`s, with `states` as shorthand for client-bill states:
185
311
 
186
- test("starts with no users", async () => {
187
- const response = await junction.fetch(
188
- new Request("https://api.sandbox.tryvital.io/v2/user", {
189
- headers: { "x-vital-api-key": "sk_us_mockingbird" },
190
- }),
191
- )
192
- expect(await response.json()).toMatchObject({ users: [], total: 0 })
312
+ ```ts
313
+ import { createRuntime, US_STATES } from "@crvouga/mockingbird-service-junction"
314
+
315
+ const junction = createRuntime({
316
+ labAccounts: [
317
+ { id: "acct-bioref-nynj", lab: "bioreference", states: ["NY", "NJ"] },
318
+ {
319
+ id: "acct-labcorp-47",
320
+ lab: "labcorp",
321
+ states: US_STATES.filter((state) => !["NY", "NJ", "RI"].includes(state)),
322
+ },
323
+ { id: "acct-quest", lab: "quest", delegated_flow: "order_delegated" },
324
+ ],
193
325
  })
326
+ console.log(junction.instance().labAccounts().length) // 3
327
+ ```
328
+
329
+ Rejections keep Junction's body shape, `400 {"detail": "Lab account is not associated with lab labcorp"}`.
330
+ `PUT /__admin/lab-accounts` swaps them per namespace at runtime. `delegated_flow` is stored and
331
+ listed, but see [not modelled](#what-is-and-is-not-modelled).
332
+
333
+ ## Order control
334
+
335
+ `simulate_order` (`POST /v3/order/{id}/test`) behaves like the sandbox's, including its stepwise
336
+ rules. For direct control, the admin API moves an order straight to a status, recording the same
337
+ event, dates and appointment cascade, and publishing the same webhook, a real transition does:
338
+
339
+ ```bash
340
+ curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "collected"}'
341
+ curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "completed", "result": "critical"}'
342
+ curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "cancelled.testkit.do_not_process"}'
343
+ ```
344
+
345
+ `to` is a full status (`completed.testkit.completed`), a `<phase>.<event>` pair, or a name that maps
346
+ onto the contract's own status for the order's collection method:
347
+
348
+ | Name | `at_home_phlebotomy` | `on_site_collection` | `testkit` | `walk_in_test` |
349
+ | --- | --- | --- | --- | --- |
350
+ | `requisition_created` | `received.…requisition_created` | same | same | same |
351
+ | `scheduled` | `collecting_sample.…appointment_scheduled` | — | — | `collecting_sample.…appointment_scheduled` |
352
+ | `shipped` / `delivered` | — | — | `collecting_sample.…transit_customer` / `…with_customer` | — |
353
+ | `collected` | `collecting_sample.…draw_completed` | `sample_with_lab.…draw_completed` | `collecting_sample.…transit_lab` | — |
354
+ | `at_lab` | — | `sample_with_lab.…draw_completed` | `sample_with_lab.…delivered_to_lab` | — |
355
+ | `partial_results` | `sample_with_lab.…partial_results` | same | — | same |
356
+ | `completed` / `corrected` / `cancelled` / `failed` | `completed.…completed` / `completed.…corrected` / `cancelled.…cancelled` / `failed.…sample_error` | same | same | same |
357
+
358
+ A name with no real status for the method (`collected` for a walk-in test) is refused with the
359
+ valid choices, never approximated. `result` picks a named result — `normal`, `abnormal`,
360
+ `critical`, `missing_results` — which steers the generator so the payload keeps Junction's exact
361
+ shape. For an exact payload, `PUT /__admin/results/{id}` with `{ "results": [...],
362
+ "missing_results": null, "interpretation": "critical", "pdf_base64": "…" }`.
363
+
364
+ There is no `isTestCancelled` in Junction's contract; cancelling produces the vendor's
365
+ `cancelled.<method>.cancelled` event and webhook, which is what a consumer derives that flag from.
366
+
367
+ **Time.** Every timestamp reads the runtime clock. `POST /__admin/clock {"advance": "2h"}` makes
368
+ delayed simulations (`?delay=<seconds>`) due and moves appointment windows, with no sleeping.
369
+
370
+ ## Faults and error shapes
371
+
372
+ Error bodies the mock reproduces byte for byte, checked against the sandbox by `verify` and the
373
+ parity suite:
374
+
375
+ | Case | Status | Body |
376
+ | --- | --- | --- |
377
+ | `GET /v2/user/{unknown id}` | 404 | `{"detail":"Not found"}` |
378
+ | `GET /v2/user/resolve/{unknown client_user_id}` | 404 | `{"detail":"User not found"}` |
379
+ | Missing `x-vital-api-key` | 401 | `{"detail":"Missing x-vital-api-key"}` |
380
+ | Request validation | 422 | `{"detail":[{"type":…,"loc":[…],"msg":…,"input":…}]}` (Pydantic shape) |
381
+ | Business rule | 400 | `{"detail":"<message>"}` or `{"detail":{"error_type":"INVALID_REQUEST","error_message":"…"}}` |
382
+
383
+ Faults reproduce the sandbox's failure modes on demand, for retry paths and error handling:
384
+
385
+ | Preset | Fault | Fidelity |
386
+ | --- | --- | --- |
387
+ | `sandbox_user_quota` | `create_user` → 400 `{"detail":{"error_type":"INVALID_REQUEST","error_message":"You have reached the maximum of 50 Sandbox users"}}` | byte-exact, as observed from a sandbox team |
388
+ | `rate_limited` | 429 `{"detail":"Too Many Requests"}`, `retry-after: 1` | shape-plausible, not verified |
389
+ | `server_error` / `bad_gateway` / `unavailable` | 500 / 502 / 503 | shape-plausible, not verified |
390
+
391
+ ```ts
392
+ import { createRuntime, FAULT_PRESETS } from "@crvouga/mockingbird-service-junction"
393
+
394
+ const junction = createRuntime()
395
+ junction.faults.add({ ...FAULT_PRESETS.sandbox_user_quota, id: "quota", count: 1 }) // next create_user only
396
+ junction.faults.add({ id: "flaky-orders", operationId: "create_order_v3_order_post", status: 503, rate: 0.25 })
194
397
  ```
195
398
 
196
- ### Sealed corpus (optional, higher fidelity)
399
+ A rule matches on `operationId`, `method`, `pathPrefix` and `namespace` (all optional), fires
400
+ `count` times or forever, for a `rate` of matching requests (seeded, so a run replays identically),
401
+ after an optional `delayMs`. A rule added through the admin API defaults to the calling namespace,
402
+ so one worker's injected failure never lands on another's request; one added in-process, as above,
403
+ applies to every namespace unless given a `namespace`. Over HTTP: `POST /__admin/faults` with the same fields, or
404
+ `POST /__admin/faults/presets/sandbox_user_quota`.
405
+
406
+ ## Webhooks
407
+
408
+ With `webhooks` / `--webhook-url`, every event is delivered signed, with retries:
197
409
 
198
- Provider-owned inventory (area serviceability, PSC site lists, the team's lab catalog, labs, lab
199
- accounts) cannot be synthesized faithfully. A sealed corpus is an exact sandbox recording of those
200
- parameter-stable GETs; installing it makes the mock answer them byte-for-byte. The recording in the
201
- repo (`corpus/sandbox-sealed.json`) is not shipped in the npm package; copy it or record your own
202
- (see Development).
410
+ - **Standard Svix signatures**: `svix-id`, `svix-timestamp`, and `svix-signature: v1,<base64
411
+ HMAC-SHA256 of "<svix-id>.<svix-timestamp>.<body>">`, keyed by the base64 part of `whsec_…`. The
412
+ official `svix` verifier, and a receiver written for real Junction, accept it unchanged. (Signing
413
+ `"<timestamp>.<body>"` without the message id, as this package's own server script did before,
414
+ is not Svix-compatible.)
415
+ - Timestamps are wall-clock even when the mock clock is moved, because receivers reject timestamps
416
+ outside a few minutes of their own clock.
417
+ - Any 2xx is delivered; anything else is retried on Svix's schedule (immediately, 5 s, 5 min,
418
+ 30 min, 2 h, 5 h, 10 h, 10 h) or `retryDelaysMs`. `POST /__admin/webhooks/flush` runs pending
419
+ retries now; `POST /__admin/webhooks/{id}/replay` redelivers one message.
203
420
 
204
421
  ```ts
205
- import { readFileSync } from "node:fs"
206
- import { JunctionAPI, parseSealedCorpus } from "@crvouga/mockingbird-service-junction"
422
+ import { createRuntime, verifySvix } from "@crvouga/mockingbird-service-junction"
207
423
 
208
- const junction = new JunctionAPI()
209
- const recording: unknown = JSON.parse(readFileSync("test/junction-sandbox-sealed.json", "utf8"))
210
- junction.installCorpus(parseSealedCorpus(recording)) // re-applied on every reset()
424
+ const secret = "whsec_bW9ja2luZ2JpcmQtdGVzdC1zZWNyZXQ="
425
+ const junction = createRuntime({
426
+ webhooks: { url: "http://127.0.0.1:3100/webhooks/junction", secret, retryDelaysMs: [0, 500, 2000] },
427
+ })
428
+
429
+ // In a test receiver:
430
+ const accept = async (request: Request) =>
431
+ verifySvix(
432
+ secret,
433
+ {
434
+ "svix-id": request.headers.get("svix-id") ?? "",
435
+ "svix-timestamp": request.headers.get("svix-timestamp") ?? "",
436
+ "svix-signature": request.headers.get("svix-signature") ?? "",
437
+ },
438
+ await request.text(),
439
+ )
440
+ console.log(typeof accept, junction.webhooks?.deliveries().length) // "function" 0
211
441
  ```
212
442
 
213
- Availability is never recorded (its slot dates and single-use `booking_key`s cannot be replayed);
214
- it always comes from the deterministic generator.
443
+ ## Verify
444
+
445
+ `verify` is the fidelity guarantee: it runs the same requests against real Junction and the mock
446
+ and reports every divergence.
447
+
448
+ ```bash
449
+ npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --corpus test/junction-corpus.json
450
+ npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --orders --sample 50 --json
451
+ ```
452
+
453
+ - **Drift**: re-fetches each recorded observation (or `--sample n` of them) and reports any that no
454
+ longer match production — the signal to re-pull.
455
+ - **Scenario**: creates a user, reads it back, provokes the documented 404s, reads the catalog and a
456
+ covered ZIP's serviceability, and deletes the user; with `--orders`, also places, reads and
457
+ cancels an order for the corpus's first lab test. Status codes must match; bodies must match in
458
+ shape, and byte for byte for the documented error bodies.
459
+
460
+ It exits 1 on any divergence. The scenario always deletes its user (and cancels its order), because
461
+ it runs against the same capped sandbox the mock exists to relieve. `verifyAgainstReal` is the same
462
+ check as a function. This repo runs it in CI against the Junction sandbox.
463
+
464
+ ## Determinism
465
+
466
+ With a fixed clock (`clock` / `POST /__admin/clock {"set": …, "freeze": true}`) and `seed`, a run
467
+ replays exactly. Stable across runs: user ids, order ids, event ids, sample ids, booking keys and
468
+ webhook message ids (derived from sequence numbers), timestamps (from the clock), generated
469
+ availability, and which requests a partial-`rate` fault hits. Wall-clock by design: webhook
470
+ `svix-timestamp`, `/health` uptime, and request log durations.
471
+
472
+ ## What is and is not modelled
473
+
474
+ A mock that is silent about its gaps is how a green suite starts lying. Specifically:
475
+
476
+ - **Corpus-backed** (byte-exact): area and PSC serviceability for covered ZIPs; the catalog, labs,
477
+ expected results and lab accounts of the team the corpus was recorded from.
478
+ - **Synthetic** (deterministic, shape-faithful, values invented): availability slots and booking
479
+ keys; results (biomarker lines and PDFs) unless a fixture is installed; requisition PDFs;
480
+ coverage for any ZIP in `--geo synthetic` mode.
481
+ - **Not modelled**:
482
+ - `delegated_flow`-specific ordering. Accounts carry the field and orders follow the documented
483
+ selection and billing rules above, but any additional behaviour Junction applies to delegated
484
+ accounts is not reproduced. Run `verify --orders` with your team's corpus to check what you rely on.
485
+ - Rate limits and outages, except when injected as faults.
486
+ - Real 429 / 5xx bodies: the presets are plausible, not recorded.
487
+ - Phlebotomy availability breadth: the sandbox serves it only for `85004`; the mock generates slots
488
+ for any covered ZIP.
489
+ - Webhook retry jitter across a snapshot restore (the jitter stream is not part of a snapshot).
490
+ - Operations outside the vendored OpenAPI subset: they return 404 and are counted as
491
+ `unmatched` in `/__admin/metrics`.
215
492
 
216
493
  ## API
217
494
 
495
+ Main entry (`@crvouga/mockingbird-service-junction`, runtime-neutral):
496
+
218
497
  | Export | Description |
219
498
  | --- | --- |
220
- | `JunctionAPI` | Class. `new JunctionAPI(options?)`; implements `FetchAPI` (`fetch(request: Request): Promise<Response>`). |
221
- | `JUNCTION_NAMESPACE` | `"junction"` — SQLite namespace holding the mock's state when sharing a `sqlite` client. |
499
+ | `createRuntime` | `(options?: JunctionRuntimeOptions) => JunctionRuntime` — the served mock (health, admin, namespaces, clock, faults, metrics, webhooks) as one `fetch`. Options: `corpus`, `geo`, `labAccounts`, `webhooks`, `onWebhook`, `sqlite`, `clock`, `seed`, `adminKey`, `onLog`. |
500
+ | `JunctionAPI` | Class. `new JunctionAPI(options?)`: one namespace's mock, implementing `fetch(request: Request): Promise<Response>`. |
501
+ | `JUNCTION_NAMESPACE` | `"junction"` — the default namespace's storage key when sharing a `sqlite` client. |
222
502
  | `document` | The vendored Junction OpenAPI document (Mockingbird subset) that drives routing. |
223
503
  | `operationIds` | Every `operationId` in `document`. |
224
504
  | `supportedOperationIds` | The `operationId`s the mock implements (all of them). |
505
+ | `pullCorpus` | `(options: PullCorpusOptions) => Promise<SealedCorpus>` — record a corpus from a real team (GETs only). |
506
+ | `diffCorpus` | `(before, after) => CorpusDiff` — ZIPs, lab tests, lab accounts and observations added / removed / changed. |
507
+ | `fingerprintCorpus` | `(corpus) => Promise<string>` — SHA-256 of a corpus's content. |
508
+ | `corpusLabel` | `(corpus) => string` — the short label `/health` and logs show, e.g. `v1-2026-09-18-HqFDuEAuhkwD`. |
225
509
  | `parseSealedCorpus` | `(value: unknown) => SealedCorpus`; throws on a non-object or unsupported `version`. |
226
- | `SEALED_CORPUS_VERSION` | Current sealed-corpus format version (`1`). |
227
- | `observationCacheKey` | `(url, method = "GET", body?) => string` — the `"<METHOD> <path>?<sorted query> <json>"` key used by the observation cache and corpus. |
228
- | `prefetchCoverageObservations` | Parity helper: records coverage-ZIP area/PSC/availability reads from the real sandbox into an observation cache before `seedFrom`. |
510
+ | `SEALED_CORPUS_VERSION` | Current corpus format version (`1`). |
511
+ | `verifyAgainstReal` | `(options: VerifyOptions) => Promise<VerifyReport>` — the `verify` command as a function. |
512
+ | `DEFAULT_JUNCTION_BASE_URL` | `"https://api.sandbox.tryvital.io"`. |
513
+ | `isSandboxKey` / `SANDBOX_KEY_PREFIXES` | Whether a key is a sandbox team key (`sk_us_` / `sk_eu_`). |
514
+ | `UNKNOWN_ZIP_STATUS` / `UNKNOWN_ZIP_ERROR_TYPE` | `424` / `"MOCKINGBIRD_UNKNOWN_ZIP"` — the corpus-mode unknown-ZIP error. |
515
+ | `labAccountFromInput` | `(input: LabAccountInput) => LabAccountRecord` — normalize (and validate) a configured account. |
516
+ | `TEAM_LAB_ACCOUNTS` | The built-in lab-account fixtures. |
517
+ | `US_STATES` | The 50 `USState` codes Junction accepts. |
518
+ | `ORDER_STATUSES_BY_METHOD` | Every order status in the contract, by collection method. |
519
+ | `TRANSITION_ALIASES` | The transition names above, by method. |
520
+ | `resolveTransition` | `(method, target) => { status } \| { error }` — how `/__admin/orders/{id}/transition` reads `to`. |
521
+ | `RESULT_FIXTURES` | Named results (`normal`, `abnormal`, `critical`, `missing_results`). |
522
+ | `FAULT_PRESETS` | Named Junction faults (`sandbox_user_quota`, `rate_limited`, `server_error`, `bad_gateway`, `unavailable`). |
523
+ | `createWebhookDispatcher` | `(endpoint: WebhookEndpoint) => WebhookDispatcher` — signed delivery with retries, standalone. |
524
+ | `signSvix` / `verifySvix` | Produce / check a standard Svix `svix-signature`. |
525
+ | `observationCacheKey` | `(url, method = "GET", body?) => string` — the `"<METHOD> <path>?<sorted query> <json>"` key corpora use. |
526
+ | `prefetchCoverageObservations` | Parity helper: records coverage-ZIP reads from the sandbox into an observation cache before `seedFrom`. |
229
527
  | `reshapeCoverageGeoCommand` | Parity-walk hook that pins geo/availability parameters onto the coverage corpus. |
230
- | `COVERAGE_ZIPS` | ZIP codes the coverage corpus seals for area/PSC reads. |
528
+ | `COVERAGE_ZIPS` | The ZIPs the shipped corpus covers. |
231
529
  | `PSC_AVAILABILITY_ZIPS` | ZIPs that also get PSC availability observations. |
232
- | `PHLEBOTOMY_AVAILABILITY_ZIPS` | ZIPs served for phlebotomy availability (`["85004"]`). |
530
+ | `PHLEBOTOMY_AVAILABILITY_ZIPS` | ZIPs the sandbox serves phlebotomy availability for (`["85004"]`). |
233
531
  | `PSC_LAB_IDS` | Lab ids used for PSC reads (`[4, 6, 13, 25]`). |
234
532
  | `AVAILABILITY_ADDRESS` | Stable Phoenix, AZ address used in availability requests so cache keys match. |
235
533
  | `AVAILABILITY_START_DATE` | Far-future availability `start_date` (`"2099-06-15"`). |
236
534
 
535
+ `./server` (Node): `createServer(options?) => Promise<JunctionServer>`, `serveTarget` (what
536
+ `serve` and `serve --config` use), `loadCorpus(source)`, `DEFAULT_PORT` (`8787`).
537
+ `./corpus`: `defaultCorpus`.
538
+
539
+ `JunctionRuntime` (from `createRuntime`) members: `fetch`, `instance(namespace?)`, `namespaces()`,
540
+ `reset(namespace? | "*")`, `snapshot(namespace?)`, `restore(snapshot, namespace?)`, `clock`
541
+ (`now`, `set`, `advance`, `freeze`, `unfreeze`, `reset`, `state`), `faults` (`add`, `list`,
542
+ `remove`, `clear`), `metrics` (`report`, `reset`), `webhooks` (`deliveries`, `replay`, `flush`,
543
+ `idle`, `clear`), `sqlite`, `rng`.
544
+
237
545
  `JunctionAPI` members:
238
546
 
239
547
  | Member | Description |
240
548
  | --- | --- |
241
549
  | `fetch(request)` | Handle one Junction REST request. |
242
- | `reset()` | `Promise<void>` — clear state; re-install the default catalog or the installed corpus. |
243
- | `installCorpus(corpus)` | Install a `SealedCorpus` (observation cache, catalog, labs, lab accounts). |
550
+ | `reset()` | `Promise<void>` — clear state; re-install the corpus or default catalog, and the configured lab accounts. |
551
+ | `installCorpus(corpus)` | Install a `SealedCorpus`: observations (shared, read-only), catalog, labs, lab accounts, ZIP coverage. |
552
+ | `corpusInfo()` | `CorpusInfo \| undefined` — label, recording date, source and counts. |
553
+ | `geoMode` | `"corpus" \| "synthetic"`, settable. |
554
+ | `configureLabAccounts(accounts?)` / `labAccounts()` | Replace (or restore with `undefined`) / read the team's lab accounts. |
555
+ | `order(id)` / `orders()` | Read orders. |
556
+ | `transitionOrder(id, status, { now, flags? })` | Move an order to a full status and publish its webhook. |
557
+ | `installResultFixture(orderId, fixture)` / `resultFixture(orderId)` | Serve an exact result payload / PDF for an order. |
244
558
  | `seedFrom(source, observations?)` | `Promise<SeedReport>` — copy users, orders, appointments, catalog and labs from a real Junction environment. |
245
559
  | `ensureLabTests(source, ids)` / `ensureOrders(source, ids)` | `Promise<number>` — fetch specific lab tests / orders from `source` if missing. |
246
560
  | `markUserDeleted(userId)` | Mark a user as deleted (subsequent reads behave like a deleted user). |
247
561
  | `webhookEvents()` | `JunctionWebhookEvent[]`, oldest first. |
248
- | `webhookDeliveryAttempts()` | Recorded retry schedule per event. |
562
+ | `webhookDeliveryAttempts()` | The retry schedule Junction would follow per event (a record; see `webhooks` for delivery). |
249
563
  | `app` / `sqlite` | The underlying Hono app and `SqliteClient`. |
250
564
 
251
565
  Options and types:
@@ -253,21 +567,27 @@ Options and types:
253
567
  ```text
254
568
  type JunctionAPIOptions = {
255
569
  sqlite?: SqliteClient // share one client across services; default: fresh in-memory DB
570
+ namespace?: string // storage namespace; default "junction"
256
571
  now?: () => number // clock in ms; default Date.now
572
+ corpus?: SealedCorpus // installed now and on every reset()
573
+ geo?: "corpus" | "synthetic" // default "corpus" with a corpus, else "synthetic"
574
+ labAccounts?: LabAccountInput[]
257
575
  onWebhook?: WebhookPublisher // called with every recorded event
258
576
  webhook?: JunctionWebhookOptions
259
577
  }
260
- type JunctionWebhookOptions = { seed?: number; jitterRatio?: number /* 0..1, default 0.1 */; timeoutMs?: number /* default 15000 */ }
578
+ type LabAccountInput = { id; lab; status?; delegated_flow?; account_name?; provider_account_id?; org_id?;
579
+ business_units?; default_clinical_notes?; allowed_billing?: Record<billingType, State[]>; states?: State[];
580
+ team_id_allowlist? }
581
+ type WebhookEndpoint = { url; secret: "whsec_…"; retryDelaysMs?: number[]; timeoutMs?; headers?; fetch? }
582
+ type ResultFixture = { name; results?: unknown[]; missing_results?: unknown[] | null; interpretation?; pdf_base64? }
583
+ type FaultRule = { id; operationId?; method?; pathPrefix?; status; body?; headers?; count?; rate?; delayMs? }
584
+ type JunctionWebhookOptions = { seed?: number; jitterRatio?: number /* 0..1, default 0.1 */; timeoutMs?: number }
261
585
  type JunctionWebhookEvent = {
262
586
  event_type: "labtest.order.created" | "labtest.order.updated" | "labtest.appointment.updated"
263
587
  data: Record<string, unknown>; team_id: string; user_id: string; client_user_id: string
264
588
  }
265
- type WebhookPublisher = (event: JunctionWebhookEvent) => void
589
+ type SealedCorpus = { version: 1; recordedAt; source; fingerprint?; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
266
590
  type SeedSource = { fetch: (request: Request) => Promise<Response>; baseUrl: string; headers: Record<string, string> }
267
- type SeedObservations = { getCache?: ReadonlyMap<string, GetCacheEntry> }
268
- type SeedReport = { users; orders; appointments; catalogTests; labs; cacheEntries: number }
269
- type GetCacheEntry = { status: number; headers: Record<string, string>; body: unknown }
270
- type SealedCorpus = { version: 1; recordedAt; source; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
271
591
  type OperationId / SupportedOperationId // string unions of operationIds / supportedOperationIds
272
592
  ```
273
593
 
@@ -287,23 +607,19 @@ console.log(report.users, report.orders)
287
607
 
288
608
  ## Development
289
609
 
290
- For contributors to the mockingbird repo only; these scripts and the corpus are not shipped in the
291
- npm package.
610
+ For contributors to the mockingbird repo only.
292
611
 
293
612
  ```bash
294
613
  bun test # offline suites, incl. scheduling state space
295
- bun test junction.seed.property.test.ts # offline monkey (no network)
614
+ bun test junction.runtime.property.test.ts # service contract, corpus, lab accounts, faults, webhooks, verify
296
615
  bun test junction.sdk.property.test.ts # drives the served mock through @tryvital/vital-node
297
616
 
298
- bun run mock:serve # HOST=127.0.0.1 PORT=8787; GET /health, POST /__admin/reset
299
- MOCKINGBIRD_JUNCTION_CORPUS=corpus/sandbox-sealed.json bun run mock:serve
300
- bun run corpus:record # record corpus/sandbox-sealed.json (needs a sandbox key)
617
+ bun run mock:serve # env-configured `serve`: HOST, PORT, MOCKINGBIRD_JUNCTION_CORPUS,
618
+ # MOCKINGBIRD_JUNCTION_WEBHOOK_URL / _SECRET / _SCOPE
619
+ bun run corpus:record -- --force # re-record the shipped corpus/sandbox-sealed.json (Vault key)
620
+ bun run verify -- --real-key "$KEY" # `mockingbird-junction verify` from source
301
621
  ```
302
622
 
303
- `mock:serve` installs `corpus/sandbox-sealed.json` when present, and signs webhooks to
304
- `MOCKINGBIRD_JUNCTION_WEBHOOK_URL` with `MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET` (delivery is disabled
305
- without a secret). `/health` and `/__admin/reset` bypass the `x-vital-api-key` gate.
306
-
307
623
  Live parity (primary proof is seedParity: warmup N on the sandbox, `seedFrom`, then lockstep M):
308
624
 
309
625
  ```bash
@@ -314,6 +630,5 @@ bun run parity -- --mode=empty # legacy empty-start differenti
314
630
 
315
631
  Webhook parity needs a public receiver; see
316
632
  [docs/webhook-parity.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/webhook-parity.md).
317
- The `client-parity*.ts` SDK scenarios are deprecated as proof and kept only as manual probes.
318
633
 
319
- Part of [mockingbird](https://github.com/crvouga/mockingbird) — agent integration guide: [`@crvouga/mockingbird`](https://github.com/crvouga/mockingbird/tree/main/packages/facade#readme).
634
+ Part of [mockingbird](https://github.com/crvouga/mockingbird) — agent integration guide: [README](https://github.com/crvouga/mockingbird#readme) · [llms.txt](https://github.com/crvouga/mockingbird/blob/main/llms.txt).