@crvouga/mockingbird-service-junction 3.1.0 → 3.3.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.
package/CHANGELOG.md CHANGED
@@ -1,15 +1,27 @@
1
1
  # Changelog — @crvouga/mockingbird-service-junction
2
2
 
3
- ## 3.1.0 (2026-10-08)
3
+ ## 3.3.0 (2026-10-12)
4
4
 
5
5
  ### Features
6
6
 
7
- - redesign emulator experience ([d6038c2](https://github.com/crvouga/mockingbird/commit/d6038c27eebbaa1751f02bc9305002a5b1304779))
7
+ - unify the emulator control plane ([2068547](https://github.com/crvouga/mockingbird/commit/2068547bedde4e2e2331c1657c1a78120e84428b))
8
8
 
9
9
  ### Dependencies
10
10
 
11
11
  - `@crvouga/mockingbird-service-sqlite`
12
12
 
13
+ ## 3.2.0 (2026-10-08)
14
+
15
+ ### Features
16
+
17
+ - match Emulate provider coverage ([16b65ce](https://github.com/crvouga/mockingbird/commit/16b65ce791fa216f28ec1521df636f57771d9c5e))
18
+
19
+ ## 3.1.0 (2026-10-08)
20
+
21
+ ### Features
22
+
23
+ - redesign emulator experience ([d6038c2](https://github.com/crvouga/mockingbird/commit/d6038c27eebbaa1751f02bc9305002a5b1304779))
24
+
13
25
  ## 3.0.0 (2026-10-07)
14
26
 
15
27
  ### ⚠️ Breaking changes
package/DISCOVERY.md CHANGED
@@ -35,6 +35,7 @@ repository; live parity is an additional oracle check, not a substitute for the
35
35
 
36
36
  - `GET /__admin/health`
37
37
  - `GET /__admin`
38
+ - `GET /__admin/controls`
38
39
  - `GET /__admin/state`
39
40
  - `GET /__admin/requests`
40
41
  - `GET /__admin/metrics`
@@ -45,6 +46,28 @@ For HTTP services, use `x-mockingbird-namespace` (or the documented credential/p
45
46
  parallel tests do not share state. Admin state, journal, metrics and fault-preset endpoints are
46
47
  designed for assertions and diagnosis by consuming test suites.
47
48
 
49
+ ## Admin controls
50
+
51
+ Every HTTP emulator takes the same controls, and each means the same thing at boot
52
+ (`createRuntime({ adminControls })`, `serve --admin-controls`), over `/__admin`, and in a request header:
53
+
54
+ | Control | Admin API | Request header | Interface |
55
+ | --- | --- | --- | --- |
56
+ | Isolate state by key | `GET /__admin/namespaces` | `x-mockingbird-namespace: <key>` | [Namespaces](https://mockingbird.chrisvouga.dev/docs/admin-controls/namespaces) |
57
+ | Reset to boot | `POST /__admin/reset` | `x-mockingbird-reset: true` | [Reset](https://mockingbird.chrisvouga.dev/docs/admin-controls/reset) |
58
+ | Snapshot and restore | `POST /__admin/snapshots`, `POST /__admin/snapshots/<id>/restore` | `x-mockingbird-snapshot: <name>`, `x-mockingbird-restore: <name>` | [Snapshot and restore](https://mockingbird.chrisvouga.dev/docs/admin-controls/snapshot-and-restore) |
59
+ | Move the clock | `POST /__admin/clock` | `x-mockingbird-time: <instant>`, `x-mockingbird-advance: <duration>` | [Clock](https://mockingbird.chrisvouga.dev/docs/admin-controls/clock) |
60
+ | Read or branch from old state | `GET /__admin/timeline` | `x-mockingbird-at: <checkpoint>`, `x-mockingbird-branch: <name>` | [Checkpoints and branches](https://mockingbird.chrisvouga.dev/docs/admin-controls/checkpoints-and-branches) |
61
+ | Inject a fault | `POST /__admin/faults` | `x-mockingbird-fault: <fault>` | [Faults](https://mockingbird.chrisvouga.dev/docs/admin-controls/faults) |
62
+ | Latency and timeouts | `PUT /__admin/latency` | `x-mockingbird-delay: <duration>`, `x-mockingbird-timeout: <duration>` | [Latency and timeouts](https://mockingbird.chrisvouga.dev/docs/admin-controls/latency-and-timeouts) |
63
+ | Replay from a seed | `PUT /__admin/controls` | `x-mockingbird-seed: <seed>` | [Seed and determinism](https://mockingbird.chrisvouga.dev/docs/admin-controls/seed-and-determinism) |
64
+ | See what happened | `GET /__admin/requests`, `GET /__admin/state` | | [Journal, metrics and state](https://mockingbird.chrisvouga.dev/docs/admin-controls/journal-and-state) |
65
+
66
+ `GET /__admin` lists every route and header with a one-line summary, and `GET /__admin/controls`
67
+ is the namespace's current control document. The same seed and the same requests give the same
68
+ bytes. Setting controls as an emulator starts is [Boot](https://mockingbird.chrisvouga.dev/docs/admin-controls/boot); every route and header is in
69
+ the [Reference](https://mockingbird.chrisvouga.dev/docs/admin-controls/reference). The full guide: <https://mockingbird.chrisvouga.dev/docs/admin-controls>.
70
+
48
71
  ## Report a mismatch or missing capability
49
72
 
50
73
  Follow the [agent reporting contract](https://github.com/crvouga/mockingbird/blob/main/docs/REPORTING_ISSUES.md). Include package version,
package/README.md CHANGED
@@ -108,13 +108,16 @@ npx mockingbird-junction serve --config mockingbird.json
108
108
  ### `createServer` (Node)
109
109
 
110
110
  ```ts
111
+ import { VitalClient } from "@tryvital/vital-node"
111
112
  import { createServer } from "@crvouga/mockingbird-service-junction/server"
112
113
 
113
114
  const server = await createServer() // shipped corpus, any free port
114
- const health = await fetch(`${server.url}/__admin/health`)
115
- console.log(health.status) // 200
116
115
 
117
- // Point the Vital SDK (or your backend's Junction base URL) at server.url, then:
116
+ // The Vital SDK your application already uses. Only `environment` changes.
117
+ const junction = new VitalClient({ apiKey: "sk_us_mockingbird", environment: server.url })
118
+ const user = await junction.user.create({ clientUserId: "app-user-1" })
119
+ console.log(user.userId) // "<uuid>"
120
+
118
121
  await server.close()
119
122
  ```
120
123
 
@@ -125,100 +128,89 @@ or a loaded corpus). The result has `url`, `port`, `runtime` and `close()`.
125
128
  ### `createRuntime` (any Fetch server)
126
129
 
127
130
  `createRuntime` is the whole served emulator — health, admin, namespaces, clock, faults, metrics — as a
128
- single runtime-neutral `fetch(request)`. Hand it to `Bun.serve`, a Worker, or call it directly:
131
+ single runtime-neutral `fetch(request)`. Hand it to `Bun.serve`, a Worker, or any other Fetch server:
129
132
 
130
133
  ```ts
134
+ import { VitalClient } from "@tryvital/vital-node"
131
135
  import { createRuntime } from "@crvouga/mockingbird-service-junction"
132
136
  import { defaultCorpus } from "@crvouga/mockingbird-service-junction/corpus"
133
137
 
134
138
  const junction = createRuntime({ corpus: defaultCorpus })
135
- const as = (worker: string) => ({
136
- "x-vital-api-key": "sk_us_mockingbird",
137
- "x-mockingbird-namespace": worker, // isolates this worker's data
138
- "content-type": "application/json",
139
- })
139
+ const server = Bun.serve({ port: 0, fetch: (request) => junction.fetch(request) })
140
140
 
141
- await junction.fetch(
142
- new Request("http://junction.local/v2/user", {
143
- method: "POST",
144
- headers: as("worker-1"),
145
- body: JSON.stringify({ client_user_id: "patient-1" }),
146
- }),
147
- )
148
- const other = await junction.fetch(new Request("http://junction.local/v2/user", { headers: as("worker-2") }))
149
- console.log(await other.json()) // worker-2 sees none of worker-1's users
141
+ // One SDK client per worker. The URL prefix isolates that worker's data.
142
+ const as = (worker: string) =>
143
+ new VitalClient({
144
+ apiKey: "sk_us_mockingbird",
145
+ environment: `${server.url.origin}/__admin/ns/${worker}`,
146
+ })
147
+
148
+ await as("worker-1").user.create({ clientUserId: "patient-1" })
149
+ const other = await as("worker-2").user.getAll()
150
+ console.log(other.users) // [] (worker-2 sees none of worker-1's users)
150
151
 
151
152
  await junction.reset("worker-1") // or junction.reset("*") for every namespace
153
+ await server.stop()
152
154
  ```
153
155
 
154
- ### In-process (inject `fetch`)
156
+ ### Pointing `@tryvital/vital-node` at it
157
+
158
+ The official SDK takes the base URL as `environment` (this is how the package's SDK drop-in test
159
+ constructs it). It has no option for a custom `fetch`, so it needs a served URL; a client that
160
+ accepts a `fetch` can call `createRuntime().fetch` in-process instead.
155
161
 
156
162
  ```ts
157
- import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
163
+ import { VitalClient } from "@tryvital/vital-node"
164
+ import { createServer } from "@crvouga/mockingbird-service-junction/server"
158
165
 
159
- const junction = new JunctionAPI({ now: () => Date.UTC(2030, 0, 1) })
160
- const headers = { "x-vital-api-key": "sk_us_mockingbird", "content-type": "application/json" }
161
-
162
- const call = async (method: string, path: string, body?: unknown) => {
163
- const response = await junction.fetch(
164
- new Request(`https://api.sandbox.tryvital.io${path}`, {
165
- method,
166
- headers,
167
- body: body === undefined ? undefined : JSON.stringify(body),
168
- }),
169
- )
170
- return { status: response.status, json: (await response.json()) as Record<string, unknown> }
171
- }
166
+ // Test setup: the emulator, with its clock set.
167
+ const server = await createServer()
168
+ await fetch(`${server.url}/__admin/clock`, {
169
+ method: "POST",
170
+ body: JSON.stringify({ set: "2030-01-01T00:00:00Z" }),
171
+ })
172
172
 
173
- const user = await call("POST", "/v2/user", { client_user_id: "app-user-1" })
174
- const userId = String(user.json.user_id)
173
+ // The application's calls, through the SDK.
174
+ const junction = new VitalClient({ apiKey: "sk_us_mockingbird", environment: server.url })
175
+ const user = await junction.user.create({ clientUserId: "app-user-1" })
175
176
 
176
- const catalog = await call("GET", "/v3/lab_test")
177
- const labTests = catalog.json.data as { id: string; method: string }[]
177
+ const labTests = await junction.labTests.get()
178
178
  const testkit = labTests.find((test) => test.method === "testkit")
179
179
  if (testkit === undefined) throw new Error("default catalog has a testkit test")
180
180
 
181
- const order = await call("POST", "/v3/order", {
182
- user_id: userId,
183
- patient_details: {
184
- first_name: "Ada",
185
- last_name: "Lovelace",
181
+ const { order } = await junction.labTests.createOrder({
182
+ userId: user.userId,
183
+ patientDetails: {
184
+ firstName: "Ada",
185
+ lastName: "Lovelace",
186
186
  dob: "1990-01-01",
187
187
  gender: "female",
188
- phone_number: "+14155551234",
188
+ phoneNumber: "+14155551234",
189
189
  email: "ada@example.com",
190
190
  },
191
- patient_address: {
192
- first_line: "1 N Central Ave",
191
+ patientAddress: {
192
+ firstLine: "1 N Central Ave",
193
193
  city: "Phoenix",
194
194
  state: "AZ",
195
195
  zip: "85004",
196
196
  country: "US",
197
197
  },
198
- order_set: { lab_test_ids: [testkit.id] },
198
+ orderSet: { labTestIds: [testkit.id] },
199
199
  })
200
- console.log(order.status, (order.json.order as { id: string }).id) // 200 "<uuid>"
200
+ console.log(order.id, order.status) // "<uuid>" "received"
201
201
 
202
- // Every event the emulator published, oldest first:
203
- console.log(junction.webhookEvents().map((event) => event.event_type)) // ["labtest.order.created"]
204
- ```
202
+ // Test assertion: every event the emulator published, oldest first.
203
+ const events = server.runtime.instance().webhookEvents()
204
+ console.log(events.map((event) => event.event_type)) // ["labtest.order.created"]
205
205
 
206
- `now` (milliseconds) drives `created_on`/`updated_at` fields, webhook timestamps, and when delayed
207
- simulations (`POST /v3/order/{id}/test?final_status=completed&delay=<seconds>`) become due; the
208
- delayed transition is applied on the next `GET /v3/order/{id}` whose `now` is past the due time.
209
-
210
- ### Pointing `@tryvital/vital-node` at it
211
-
212
- The official SDK takes the base URL as `environment` (this is how the package's SDK drop-in test
213
- constructs it):
214
-
215
- ```js
216
- import { VitalClient } from "@tryvital/vital-node"
217
-
218
- const client = new VitalClient({ apiKey: "sk_us_mockingbird", environment: baseUrl })
219
- const user = await client.user.create({ clientUserId: "app-user-1" })
206
+ await server.close()
220
207
  ```
221
208
 
209
+ The clock (`POST /__admin/clock`, or `now` in milliseconds on `JunctionAPI`) drives
210
+ `created_on`/`updated_at` fields, webhook timestamps, and when delayed simulations
211
+ (`POST /v3/order/{id}/test?final_status=completed&delay=<seconds>`) become due; the delayed
212
+ transition is applied on the next `GET /v3/order/{id}` whose clock is past the due time.
213
+
222
214
  If your app guards Junction hosts with an allowlist, allow `127.0.0.1` / `localhost` for tests
223
215
  while keeping the production and sandbox guards intact.
224
216
 
@@ -646,24 +638,24 @@ With `webhooks` / `--webhook-url`, every event is delivered signed, with retries
646
638
  retries now; `POST /__admin/webhooks/{id}/replay` redelivers one message.
647
639
 
648
640
  ```ts
649
- import { createRuntime, verifySvix } from "@crvouga/mockingbird-service-junction"
641
+ import { Webhook } from "svix"
642
+ import { createRuntime } from "@crvouga/mockingbird-service-junction"
650
643
 
651
644
  const secret = "whsec_bW9ja2luZ2JpcmQtdGVzdC1zZWNyZXQ="
652
645
  const junction = createRuntime({
653
646
  webhooks: { url: "http://127.0.0.1:3100/webhooks/junction", secret, retryDelaysMs: [0, 500, 2000] },
654
647
  })
655
648
 
656
- // In a test receiver:
657
- const accept = async (request: Request) =>
658
- verifySvix(
659
- secret,
660
- {
661
- "svix-id": request.headers.get("svix-id") ?? "",
662
- "svix-timestamp": request.headers.get("svix-timestamp") ?? "",
663
- "svix-signature": request.headers.get("svix-signature") ?? "",
664
- },
665
- await request.text(),
666
- )
649
+ // The application's receiver, verifying with the official svix package as it does for real Junction:
650
+ const accept = async (request: Request) => {
651
+ const body = await request.text()
652
+ new Webhook(secret).verify(body, {
653
+ "svix-id": request.headers.get("svix-id") ?? "",
654
+ "svix-timestamp": request.headers.get("svix-timestamp") ?? "",
655
+ "svix-signature": request.headers.get("svix-signature") ?? "",
656
+ }) // throws unless the signature is valid
657
+ return JSON.parse(body) as { event_type: string }
658
+ }
667
659
  console.log(typeof accept, junction.webhooks?.deliveries().length) // "function" 0
668
660
  ```
669
661