@crvouga/mockingbird-service-junction 0.2.0 → 0.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 +7 -5
- package/README.md +222 -21
- package/dist/{chunk-MD4KYEIM.js → chunk-C2UGIPPM.js} +2 -2
- package/dist/{chunk-4C6VYRJN.js → chunk-ISDNA2TU.js} +8 -3
- package/dist/chunk-ISDNA2TU.js.map +7 -0
- package/dist/{chunk-KKNULV7X.js → chunk-KLBGX23P.js} +76 -8
- package/dist/chunk-KLBGX23P.js.map +7 -0
- package/dist/{chunk-MA3HSUVD.js → chunk-NFWUEPW4.js} +1856 -778
- package/dist/chunk-NFWUEPW4.js.map +7 -0
- package/dist/cli.js +4 -4
- package/dist/corpus.d.ts +10 -2
- package/dist/corpus.js +2 -2
- package/dist/index.d.ts +322 -49
- package/dist/index.js +34 -2
- package/dist/server.d.ts +242 -10
- package/dist/server.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-4C6VYRJN.js.map +0 -7
- package/dist/chunk-KKNULV7X.js.map +0 -7
- package/dist/chunk-MA3HSUVD.js.map +0 -7
- /package/dist/{chunk-MD4KYEIM.js.map → chunk-C2UGIPPM.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-junction
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.3.0 (2026-09-21)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- team identity, lab-account admin, sandbox limits, fixtures and request journal ([6cb1be3](https://github.com/crvouga/mockingbird/commit/6cb1be324a048ac9fe832061632c0ee9c9d01f8b))
|
|
8
|
+
|
|
9
|
+
## 0.2.0 (2026-09-20)
|
|
4
10
|
|
|
5
11
|
### Features
|
|
6
12
|
|
|
@@ -12,10 +18,6 @@
|
|
|
12
18
|
- scope admin-injected faults to the calling namespace ([3c1d573](https://github.com/crvouga/mockingbird/commit/3c1d573ac0c404324bbd20855a5ac7a2cd50f738))
|
|
13
19
|
- take the admin namespace override only on admin routes ([37836a5](https://github.com/crvouga/mockingbird/commit/37836a5b0c51d17dd24e7b01c12dd6d8049646da))
|
|
14
20
|
|
|
15
|
-
### Dependencies
|
|
16
|
-
|
|
17
|
-
- `@crvouga/mockingbird-service-sqlite`
|
|
18
|
-
|
|
19
21
|
## 0.1.2 (2026-09-19)
|
|
20
22
|
|
|
21
23
|
### Fixes and improvements
|
package/README.md
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
Stateful mock of the [Junction (formerly Vital) API](https://docs.junction.com/): users
|
|
4
4
|
(`/v2/user`), the lab-testing catalog, lab orders (create, cancel, simulate, results,
|
|
5
5
|
requisitions), at-home phlebotomy and patient-service-center (PSC) scheduling, and the
|
|
6
|
-
`labtest.order.*` / `labtest.appointment.updated` webhooks. All
|
|
6
|
+
`labtest.order.*` / `labtest.appointment.updated` webhooks. All 40 operations in the vendored
|
|
7
7
|
OpenAPI subset are served, and behaviour is checked by differential property tests against the
|
|
8
8
|
Junction sandbox.
|
|
9
9
|
|
|
10
10
|
Use it to take Junction off your suite's critical path: no sandbox key, no shared 50-user sandbox
|
|
11
|
-
cap
|
|
11
|
+
cap (the mock enforces no sandbox limit unless a test [asks for one](#sandbox-limits)), no
|
|
12
|
+
cross-suite interference. Serve it as a local origin with one command, or run it in-process.
|
|
12
13
|
|
|
13
14
|
```bash
|
|
14
15
|
npx mockingbird-junction serve # http://127.0.0.1:8787, recorded sandbox corpus loaded
|
|
@@ -54,19 +55,26 @@ npx mockingbird-junction serve --help
|
|
|
54
55
|
| `--host <host>` | `127.0.0.1` | Interface to bind |
|
|
55
56
|
| `--corpus <default\|none\|file>` | `default` | The shipped corpus, no corpus (synthetic data), or a file from `corpus pull` |
|
|
56
57
|
| `--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 |
|
|
58
|
+
| `--lab-accounts <file>` | built-in fixtures | [Lab accounts](#lab-accounts): a JSON array, or `{ "presets": [...], "accounts": [...] }` |
|
|
59
|
+
| `--team-id <uuid>` | the corpus's team | The [team](#team-identity) the mock answers as |
|
|
60
|
+
| `--max-users <n>` | unlimited | Enforce the sandbox's [live-user cap](#sandbox-limits) |
|
|
61
|
+
| `--identity <strict\|adopt-users>` | `strict` | Unknown `user_id`s: 404, or [create on first use](#fixtures-and-identity) |
|
|
62
|
+
| `--fixtures <file>` | — | [Users and orders](#fixtures-and-identity) every namespace starts with |
|
|
63
|
+
| `--journal-size <n>` | `1000` | Requests each namespace's [journal](#is-this-the-mock) keeps |
|
|
58
64
|
| `--webhook-url <url>` / `--webhook-secret <whsec_…>` | off | Deliver [signed webhooks](#webhooks) (secret also from `MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET`) |
|
|
59
65
|
| `--webhook-retry-delays <ms,…>` | Svix's schedule | Delay before each delivery attempt, e.g. `0,1000,5000` in tests |
|
|
60
66
|
| `--webhook-scope <scope>` | — | Sent as `x-mockingbird-scope` on every delivery |
|
|
61
67
|
| `--admin-key <key>` | open | Require `x-mockingbird-admin-key` on `/__admin/*` (also `MOCKINGBIRD_ADMIN_KEY`) |
|
|
62
68
|
| `--seed <seed>` | `0` | Seeds fault rates and retry jitter |
|
|
63
69
|
| `--log <pretty\|json\|off>` | `pretty` | One line per request: operation id, status, duration, namespace, fault |
|
|
70
|
+
| `--log-requests` | — | Same as `--log json`: one JSON line per request with the ids it touched, never bodies |
|
|
64
71
|
| `--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
72
|
| `--config <file>` | — | Serve every service in a `mockingbird.json` instead ([below](#many-services-from-one-config)) |
|
|
66
73
|
|
|
67
74
|
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,
|
|
69
|
-
|
|
75
|
+
lab-test and lab-account counts, recording date and source), the geo mode, the team id, the
|
|
76
|
+
lab-account count, which sandbox limits are on (normally none), the identity mode, and whether
|
|
77
|
+
webhook delivery is on.
|
|
70
78
|
|
|
71
79
|
### Many services from one config
|
|
72
80
|
|
|
@@ -224,7 +232,12 @@ Every Mockingbird service answers the same control surface, outside the vendor's
|
|
|
224
232
|
- `x-mockingbird-namespace: <name>` — isolates a request's data (`[A-Za-z0-9_.-]{1,64}`). Each
|
|
225
233
|
namespace is a separate team over one shared database, so parallel workers share one process
|
|
226
234
|
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`.
|
|
235
|
+
routes take the namespace from `?namespace=`, then the header, then `default`. Configuration set
|
|
236
|
+
through the admin API (lab accounts, limits, identity, geo) belongs to one namespace and survives
|
|
237
|
+
that namespace's `POST /__admin/reset`; data does not.
|
|
238
|
+
- `x-mockingbird: junction@<version>; ns=<namespace>` — on **every** response: vendor answers,
|
|
239
|
+
errors, faults, admin and health. Junction never sends it, so it tells the mock from the vendor
|
|
240
|
+
(see [Is this the mock?](#is-this-the-mock)).
|
|
228
241
|
|
|
229
242
|
| Route | Does |
|
|
230
243
|
| --- | --- |
|
|
@@ -241,7 +254,19 @@ Every Mockingbird service answers the same control surface, outside the vendor's
|
|
|
241
254
|
| `POST /__admin/orders/{id}/transition` | Move an order: `{ "to": "completed", "result": "abnormal" }` ([order control](#order-control)) |
|
|
242
255
|
| `GET` / `PUT /__admin/results/{id}` | Read or install an exact result payload / PDF for an order |
|
|
243
256
|
| `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) |
|
|
257
|
+
| `GET` / `PUT /__admin/lab-accounts` | Read or replace the namespace's [lab accounts](#lab-accounts) (`{ "accounts": [...], "presets": [...] }`; `{ "accounts": null }` restores the default) |
|
|
258
|
+
| `POST /__admin/lab-accounts` | Add one account; 409 if the id exists |
|
|
259
|
+
| `PATCH` / `DELETE /__admin/lab-accounts/{id}` | Merge fields into an account (`{ "status": "suspended" }`, `{ "states": [...] }`) / remove it; 404 if absent |
|
|
260
|
+
| `GET /__admin/lab-accounts/presets` | Every [preset](#lab-account-presets), with its full record |
|
|
261
|
+
| `POST /__admin/lab-accounts/presets/{name}` | Add a preset account; body `{ "id": "…" }` overrides its id |
|
|
262
|
+
| `GET /__admin/team` | `{ "teamId" }` — the [team](#team-identity) the namespace answers as |
|
|
263
|
+
| `GET` / `PUT /__admin/limits` | Read or change the namespace's [sandbox limits](#sandbox-limits) |
|
|
264
|
+
| `GET` / `PUT /__admin/identity` | Read or set `{ "mode": "strict" \| "adopt-users" }` ([identity](#fixtures-and-identity)) |
|
|
265
|
+
| `POST /__admin/users`, `POST /__admin/users/bulk` | Insert a user (or `{ "users": [...] }`, all or none) with a chosen `user_id`; 409 on a duplicate id or `client_user_id` |
|
|
266
|
+
| `DELETE /__admin/users/{id}` | Remove a user outright, leaving no deletion tombstone |
|
|
267
|
+
| `POST /__admin/orders` | Insert an order, optionally already in a `status`, through `create_order`'s record builder |
|
|
268
|
+
| `POST /__admin/import` | `{ "users": [...], "orders": [...] }` in one call |
|
|
269
|
+
| `GET` / `DELETE /__admin/requests` | The namespace's [request journal](#is-this-the-mock) (`?operationId=`, `?status=`, `?since=`, `?limit=`, `?all=1`) / clear it |
|
|
245
270
|
| `GET /__admin/corpus`, `PUT /__admin/geo` | The loaded corpus; switch [geo mode](#geo-corpus-or-synthetic) |
|
|
246
271
|
| `GET /__admin/webhooks` | [Deliveries](#webhooks) with every attempt |
|
|
247
272
|
| `POST /__admin/webhooks/{id}/replay`, `POST /__admin/webhooks/flush` | Redeliver one message; run pending retries now |
|
|
@@ -327,8 +352,150 @@ console.log(junction.instance().labAccounts().length) // 3
|
|
|
327
352
|
```
|
|
328
353
|
|
|
329
354
|
Rejections keep Junction's body shape, `400 {"detail": "Lab account is not associated with lab labcorp"}`.
|
|
330
|
-
`
|
|
331
|
-
|
|
355
|
+
`delegated_flow` is stored and listed, but see [not modelled](#what-is-and-is-not-modelled).
|
|
356
|
+
|
|
357
|
+
Availability reads that take a `lab_account_id` (`GET /v3/order/area/info`) check it against the
|
|
358
|
+
same live list, so an id `create_order` accepts is never a 404 there.
|
|
359
|
+
|
|
360
|
+
### Changing accounts at runtime
|
|
361
|
+
|
|
362
|
+
Each namespace has its own layout, kept across its own reset. Change one account at a time, or
|
|
363
|
+
replace them all:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
curl -X POST localhost:8787/__admin/lab-accounts/presets/bioreference_ny_nj_delegated
|
|
367
|
+
curl -X PATCH localhost:8787/__admin/lab-accounts/$ID -d '{"status": "suspended"}'
|
|
368
|
+
curl -X POST localhost:8787/__admin/lab-accounts -d '{"id": "…", "lab": "quest", "states": ["AZ"]}'
|
|
369
|
+
curl -X DELETE localhost:8787/__admin/lab-accounts/$ID
|
|
370
|
+
curl -X PUT localhost:8787/__admin/lab-accounts -d '{"presets": ["quest_platform"], "accounts": []}'
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
A bad field is a 400 in the admin shape naming it, e.g. `lab account x: states: ZZ is not a US
|
|
374
|
+
state code`. `--lab-accounts` and the `labAccounts` option take the same layout, so a stack can
|
|
375
|
+
declare it at boot with no admin call:
|
|
376
|
+
|
|
377
|
+
```json
|
|
378
|
+
{ "presets": ["bioreference_ny_nj_delegated", { "name": "quest_platform", "id": "…" }], "accounts": [] }
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Lab-account presets
|
|
382
|
+
|
|
383
|
+
Named after what they model; ids are `deterministicUuid("junction:lab-account:<name>")` unless
|
|
384
|
+
given. Where a preset copies an account recorded from a real BioReference-linked sandbox team
|
|
385
|
+
(2026-09-21), its billing states are the recording's. `PLATFORM_ACCOUNT_STATES` is every state
|
|
386
|
+
but NJ, NY and RI (47).
|
|
387
|
+
|
|
388
|
+
| Preset | `lab` | `delegated_flow` | `allowed_billing` | `team_id_allowlist` | Source |
|
|
389
|
+
| --- | --- | --- | --- | --- | --- |
|
|
390
|
+
| `bioreference_ny_nj_delegated` | `bioreference` | `order_delegated` | `client_bill`: NY, NJ | the team | requested shape, **not** a recorded account |
|
|
391
|
+
| `bioreference_delegated_multi_state` | `bioreference` | `order_delegated` | `client_bill`: every state but NY, NJ (48) | the team | recorded |
|
|
392
|
+
| `bioreference_customer_multi_state` | `bioreference` | `not_delegated` | `client_bill`: `PLATFORM_ACCOUNT_STATES` | `[]` | recorded ("Junction BioReference Account") |
|
|
393
|
+
| `bioreference_patient_bill_passthrough` | `bioreference` | `not_delegated` | `patient_bill_passthrough`: NJ, NY | the team | recorded |
|
|
394
|
+
| `quest_platform` / `labcorp_platform` | `quest` / `labcorp` | `not_delegated` | `client_bill`: `PLATFORM_ACCOUNT_STATES` | `[]` | recorded ("Junction Quest/Labcorp Account") |
|
|
395
|
+
| `suspended_bioreference` / `suspended_quest` / `suspended_labcorp` | as above | `not_delegated` | as the platform accounts | `[]` | the platform shapes with `status: "suspended"` |
|
|
396
|
+
|
|
397
|
+
Every preset is `status: "active"` unless named `suspended_*`. `GET /__admin/lab-accounts/presets`
|
|
398
|
+
returns the full records.
|
|
399
|
+
|
|
400
|
+
### Team identity
|
|
401
|
+
|
|
402
|
+
The mock answers as one team: `team_id` on users, orders and webhooks, and the team that
|
|
403
|
+
`team_id_allowlist` is checked against. It is, in order: `teamId` / `--team-id`, the team a
|
|
404
|
+
version-2 corpus recorded (`corpus pull` reads it from a listed user, else from the accounts'
|
|
405
|
+
allowlists), else the fixed `MOCK_TEAM_ID`. `GET /__admin/team` reports it.
|
|
406
|
+
|
|
407
|
+
A version-2 corpus keeps each account's allowlist **verbatim**, so an account the real team is not
|
|
408
|
+
linked to answers `400 {"detail": "Lab account is not linked to your team"}` exactly as it does
|
|
409
|
+
live. A version-1 corpus (no `teamId`) still loads and behaves as 0.2.0 did: every recorded account
|
|
410
|
+
is also linked to the mock team.
|
|
411
|
+
|
|
412
|
+
**Open question — empty allowlists.** Junction's own Quest, Labcorp and BioReference accounts carry
|
|
413
|
+
`team_id_allowlist: []`, and a real team's listing returns them, so the mock lists them and treats
|
|
414
|
+
them as linked. Whether *ordering* through one — by explicit id, or when it is one of several
|
|
415
|
+
active accounts for a lab with the id omitted — behaves the same live is **not verified**.
|
|
416
|
+
`verify --orders` checks it whenever the corpus has such an account; until a run records the
|
|
417
|
+
answer, a suite that depends on it should run against the sandbox too.
|
|
418
|
+
|
|
419
|
+
## Sandbox limits
|
|
420
|
+
|
|
421
|
+
The mock exists to escape the sandbox's restrictions, so it enforces none of them unless a test
|
|
422
|
+
asks — this is a tested guarantee (500 users in a loop all succeed), not an accident.
|
|
423
|
+
|
|
424
|
+
| Sandbox limit | Modelled | Default | Turn it on |
|
|
425
|
+
| --- | --- | --- | --- |
|
|
426
|
+
| 50 live users per team | yes, byte-exact body; deleted users free their slot | off (unlimited) | `limits: { maxUsers: 50 }`, `--max-users 50`, `PUT /__admin/limits {"maxUsers": 50}` |
|
|
427
|
+
| `POST /v3/order/{id}/test` only in sandbox | as a switch: refuses a key that is not `sk_us_…`/`sk_eu_…` (400, body not recorded) | off | `simulateRequiresSandbox: true` |
|
|
428
|
+
| Rate limits | as a per-namespace budget: 429 `{"detail":"Too Many Requests"}`, `retry-after: 1` (shape not recorded) | off | `rateLimitPerSecond: n` |
|
|
429
|
+
|
|
430
|
+
`PUT /__admin/limits` merges (`null` lifts a numeric limit) and is per namespace;
|
|
431
|
+
`GET /__admin/limits` returns the effective values. For a one-off failure instead of a standing
|
|
432
|
+
limit, the `sandbox_user_quota` [fault preset](#faults-and-error-shapes) is still there.
|
|
433
|
+
|
|
434
|
+
## Fixtures and identity
|
|
435
|
+
|
|
436
|
+
A suite often needs state the public API cannot build: database fixtures that already carry real
|
|
437
|
+
Junction `user_id`s, or an order already in some status. Insert them directly:
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
curl -X POST localhost:8787/__admin/users -d '{"user_id": "3f0c…", "client_user_id": "patient-1"}'
|
|
441
|
+
curl -X POST localhost:8787/__admin/orders -d '{"user_id": "3f0c…", "lab_test_id": "…", "status": "completed"}'
|
|
442
|
+
curl -X POST localhost:8787/__admin/import -d @test/junction-fixtures.json
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
- Users take `user_id?`, `client_user_id`, `created_on?`, `fallback_time_zone?`,
|
|
446
|
+
`fallback_birth_date?`, `ingestion_start?`, `ingestion_end?`. A duplicate `user_id` or
|
|
447
|
+
`client_user_id` is a 409; `POST /__admin/users/bulk` inserts all or none.
|
|
448
|
+
- Orders take `order_id?`, `user_id`, `lab_test_id`, `lab_account_id?`, `status?` (any
|
|
449
|
+
[transition target](#order-control)), `collection_method?`, `patient_details?`,
|
|
450
|
+
`patient_address?`, `billing_type?`, `created_at?` and `result_fixture?` (as
|
|
451
|
+
`PUT /__admin/results/{id}` takes it). Request validation is skipped; references are not — the
|
|
452
|
+
user and lab test must exist (404 otherwise).
|
|
453
|
+
- Inserted records go through the same builders as `create_user` / `create_order`, so every read
|
|
454
|
+
path (`GET /v2/user/{id}`, resolve by `client_user_id`, `GET /v3/order/{id}`,
|
|
455
|
+
`GET /v3/orders?user_id=`) sees them as API-created. Default timestamps read the mock clock.
|
|
456
|
+
- **No webhook fires** unless the body has `"emitWebhooks": true`: fixtures should not trigger
|
|
457
|
+
backend side effects.
|
|
458
|
+
- `fixtures` / `--fixtures <file>` loads `{ "users": [...], "orders": [...] }` into every namespace
|
|
459
|
+
at creation and again on each reset.
|
|
460
|
+
|
|
461
|
+
For flow suites that do not care about Junction identity, `identity: "adopt-users"` (or
|
|
462
|
+
`--identity adopt-users`, or `PUT /__admin/identity {"mode": "adopt-users"}` per namespace) creates
|
|
463
|
+
an unknown, well-formed `user_id` on first use — in `create_order` and `GET /v3/orders?user_id=` —
|
|
464
|
+
and the request proceeds. The journal marks the request `adopted: true`. **Orders are never
|
|
465
|
+
adopted**: an unknown order id always 404s, because inventing an order hides real bugs. The default,
|
|
466
|
+
`strict`, is Junction's own behaviour.
|
|
467
|
+
|
|
468
|
+
## Is this the mock?
|
|
469
|
+
|
|
470
|
+
A mock 404 and a sandbox 404 are byte-identical, which is how a backend half-pointed at the sandbox
|
|
471
|
+
goes unnoticed. Three things make it visible:
|
|
472
|
+
|
|
473
|
+
- **`x-mockingbird` on every response.** Assert on it in a test helper:
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
import { expect } from "vitest"
|
|
477
|
+
|
|
478
|
+
/** Wrap the fetch your Junction client uses; fails any call that did not reach the mock. */
|
|
479
|
+
export const mockOnlyFetch =
|
|
480
|
+
(inner: typeof fetch = fetch): typeof fetch =>
|
|
481
|
+
async (input, init) => {
|
|
482
|
+
const response = await inner(input, init)
|
|
483
|
+
expect(response.headers.get("x-mockingbird"), `${String(input)} did not reach the Junction mock`).toMatch(
|
|
484
|
+
/^junction@/,
|
|
485
|
+
)
|
|
486
|
+
return response
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
- **The request journal.** Each namespace keeps its last 1000 requests (`--journal-size`,
|
|
491
|
+
`journalSize`): operation id, method, path, status, duration, fault id, the user / order /
|
|
492
|
+
lab-account ids it touched, and when (mock clock). `GET /__admin/requests?operationId=create_order_v3_order_post`
|
|
493
|
+
proves an order was placed here; an empty answer proves it was not. Never bodies.
|
|
494
|
+
- **Miss headers on 404s.** A missing user or order keeps Junction's body byte for byte and adds
|
|
495
|
+
`x-mockingbird-miss: order <id>` (or `user <id>`, `user client:<client_user_id>`) and
|
|
496
|
+
`x-mockingbird-known: users=<n> orders=<n>`.
|
|
497
|
+
|
|
498
|
+
`--log-requests` prints the same fields as one JSON line per request.
|
|
332
499
|
|
|
333
500
|
## Order control
|
|
334
501
|
|
|
@@ -370,12 +537,16 @@ delayed simulations (`?delay=<seconds>`) due and moves appointment windows, with
|
|
|
370
537
|
## Faults and error shapes
|
|
371
538
|
|
|
372
539
|
Error bodies the mock reproduces byte for byte, checked against the sandbox by `verify` and the
|
|
373
|
-
parity suite:
|
|
540
|
+
parity suite (`ORDER_NOT_FOUND` holds the unknown-order text per operation id):
|
|
374
541
|
|
|
375
542
|
| Case | Status | Body |
|
|
376
543
|
| --- | --- | --- |
|
|
377
544
|
| `GET /v2/user/{unknown id}` | 404 | `{"detail":"Not found"}` |
|
|
378
545
|
| `GET /v2/user/resolve/{unknown client_user_id}` | 404 | `{"detail":"User not found"}` |
|
|
546
|
+
| Unknown order: get order, requisition PDF | 404 | `{"detail":"This order doesn't exist"}` |
|
|
547
|
+
| Unknown order: cancel, simulate | 404 | `{"detail":"Order doesn't exist"}` |
|
|
548
|
+
| Unknown order: result, result metadata, result PDF | 404 | `{"detail":"Order not found"}` |
|
|
549
|
+
| Unknown order: every phlebotomy / PSC appointment route | 404 | `{"detail":"This order doesn't exist."}` (trailing period included) |
|
|
379
550
|
| Missing `x-vital-api-key` | 401 | `{"detail":"Missing x-vital-api-key"}` |
|
|
380
551
|
| Request validation | 422 | `{"detail":[{"type":…,"loc":[…],"msg":…,"input":…}]}` (Pydantic shape) |
|
|
381
552
|
| Business rule | 400 | `{"detail":"<message>"}` or `{"detail":{"error_type":"INVALID_REQUEST","error_message":"…"}}` |
|
|
@@ -452,10 +623,12 @@ npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --orders --sa
|
|
|
452
623
|
|
|
453
624
|
- **Drift**: re-fetches each recorded observation (or `--sample n` of them) and reports any that no
|
|
454
625
|
longer match production — the signal to re-pull.
|
|
455
|
-
- **Scenario**: creates a user, reads it back, provokes the documented 404s
|
|
626
|
+
- **Scenario**: creates a user, reads it back, provokes the documented 404s (including every
|
|
627
|
+
order-scoped operation against a random unknown order), reads the catalog (paged and bare) and a
|
|
456
628
|
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
|
|
458
|
-
|
|
629
|
+
cancels an order for the corpus's first lab test, and — when the corpus has an account with an
|
|
630
|
+
empty `team_id_allowlist` — orders through it with and without its id. Status codes must match;
|
|
631
|
+
bodies must match in shape, and byte for byte for the documented error bodies.
|
|
459
632
|
|
|
460
633
|
It exits 1 on any divergence. The scenario always deletes its user (and cancels its order), because
|
|
461
634
|
it runs against the same capped sandbox the mock exists to relieve. `verifyAgainstReal` is the same
|
|
@@ -482,7 +655,12 @@ A mock that is silent about its gaps is how a green suite starts lying. Specific
|
|
|
482
655
|
- `delegated_flow`-specific ordering. Accounts carry the field and orders follow the documented
|
|
483
656
|
selection and billing rules above, but any additional behaviour Junction applies to delegated
|
|
484
657
|
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
|
|
658
|
+
- Rate limits and outages, except when injected as faults or switched on as
|
|
659
|
+
[limits](#sandbox-limits).
|
|
660
|
+
- Whether an account with an empty `team_id_allowlist` can be ordered through (see
|
|
661
|
+
[Team identity](#team-identity)); the mock assumes it can.
|
|
662
|
+
- Adopted users (`adopt-users`) have `client_user_id` equal to their `user_id`; nothing in
|
|
663
|
+
Junction corresponds to adoption.
|
|
486
664
|
- Real 429 / 5xx bodies: the presets are plausible, not recorded.
|
|
487
665
|
- Phlebotomy availability breadth: the sandbox serves it only for `85004`; the mock generates slots
|
|
488
666
|
for any covered ZIP.
|
|
@@ -496,7 +674,7 @@ Main entry (`@crvouga/mockingbird-service-junction`, runtime-neutral):
|
|
|
496
674
|
|
|
497
675
|
| Export | Description |
|
|
498
676
|
| --- | --- |
|
|
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`. |
|
|
677
|
+
| `createRuntime` | `(options?: JunctionRuntimeOptions) => JunctionRuntime` — the served mock (health, admin, namespaces, clock, faults, metrics, journal, webhooks) as one `fetch`. Options: `corpus`, `geo`, `labAccounts`, `teamId`, `limits`, `identity`, `fixtures`, `journalSize`, `webhooks`, `onWebhook`, `sqlite`, `clock`, `seed`, `adminKey`, `onLog`. |
|
|
500
678
|
| `JunctionAPI` | Class. `new JunctionAPI(options?)`: one namespace's mock, implementing `fetch(request: Request): Promise<Response>`. |
|
|
501
679
|
| `JUNCTION_NAMESPACE` | `"junction"` — the default namespace's storage key when sharing a `sqlite` client. |
|
|
502
680
|
| `document` | The vendored Junction OpenAPI document (Mockingbird subset) that drives routing. |
|
|
@@ -507,7 +685,21 @@ Main entry (`@crvouga/mockingbird-service-junction`, runtime-neutral):
|
|
|
507
685
|
| `fingerprintCorpus` | `(corpus) => Promise<string>` — SHA-256 of a corpus's content. |
|
|
508
686
|
| `corpusLabel` | `(corpus) => string` — the short label `/health` and logs show, e.g. `v1-2026-09-18-HqFDuEAuhkwD`. |
|
|
509
687
|
| `parseSealedCorpus` | `(value: unknown) => SealedCorpus`; throws on a non-object or unsupported `version`. |
|
|
510
|
-
| `SEALED_CORPUS_VERSION` | Current corpus format version (`
|
|
688
|
+
| `SEALED_CORPUS_VERSION` | Current corpus format version (`2`: adds `teamId`). |
|
|
689
|
+
| `SUPPORTED_SEALED_CORPUS_VERSIONS` | Versions `parseSealedCorpus` loads (`[1, 2]`). |
|
|
690
|
+
| `MOCK_TEAM_ID` | The team id used when neither `teamId` nor the corpus gives one. |
|
|
691
|
+
| `OTHER_TEAM_ID` | A team that is not the mock's, for fixtures modelling another team's account. |
|
|
692
|
+
| `isLinkedToTeam` | `(account, teamId) => boolean` — the allowlist rule (`[]` counts as linked). |
|
|
693
|
+
| `LAB_ACCOUNT_PRESETS` | The [lab-account presets](#lab-account-presets), without ids. |
|
|
694
|
+
| `presetAccountId` | `(name) => string` — a preset's default id. |
|
|
695
|
+
| `PLATFORM_ACCOUNT_STATES` / `DELEGATED_ACCOUNT_STATES` | The recorded 47- and 48-state client-bill lists. |
|
|
696
|
+
| `LabAccountConflict` | Thrown by `addLabAccount` for an id that exists. |
|
|
697
|
+
| `DEFAULT_LIMITS` | Every [sandbox limit](#sandbox-limits), off. |
|
|
698
|
+
| `sandboxUserQuotaBody` | `(max) => body` — the sandbox's user-cap 400 body. |
|
|
699
|
+
| `IDENTITY_MODES` | `["strict", "adopt-users"]`. |
|
|
700
|
+
| `FixtureError` | Thrown by the fixture inserts; `status` is 400, 404 or 409. |
|
|
701
|
+
| `ORDER_NOT_FOUND` | Junction's unknown-order `detail`, by operation id. |
|
|
702
|
+
| `MISS_HEADER` / `KNOWN_HEADER` | `"x-mockingbird-miss"` / `"x-mockingbird-known"`. |
|
|
511
703
|
| `verifyAgainstReal` | `(options: VerifyOptions) => Promise<VerifyReport>` — the `verify` command as a function. |
|
|
512
704
|
| `DEFAULT_JUNCTION_BASE_URL` | `"https://api.sandbox.tryvital.io"`. |
|
|
513
705
|
| `isSandboxKey` / `SANDBOX_KEY_PREFIXES` | Whether a key is a sandbox team key (`sk_us_` / `sk_eu_`). |
|
|
@@ -539,8 +731,8 @@ Main entry (`@crvouga/mockingbird-service-junction`, runtime-neutral):
|
|
|
539
731
|
`JunctionRuntime` (from `createRuntime`) members: `fetch`, `instance(namespace?)`, `namespaces()`,
|
|
540
732
|
`reset(namespace? | "*")`, `snapshot(namespace?)`, `restore(snapshot, namespace?)`, `clock`
|
|
541
733
|
(`now`, `set`, `advance`, `freeze`, `unfreeze`, `reset`, `state`), `faults` (`add`, `list`,
|
|
542
|
-
`remove`, `clear`), `metrics` (`report`, `reset`), `
|
|
543
|
-
`idle`, `clear`), `sqlite`, `rng`.
|
|
734
|
+
`remove`, `clear`), `metrics` (`report`, `reset`), `journal` (`list`, `clear`, `size`), `webhooks`
|
|
735
|
+
(`deliveries`, `replay`, `flush`, `idle`, `clear`), `sqlite`, `rng`.
|
|
544
736
|
|
|
545
737
|
`JunctionAPI` members:
|
|
546
738
|
|
|
@@ -551,7 +743,12 @@ Main entry (`@crvouga/mockingbird-service-junction`, runtime-neutral):
|
|
|
551
743
|
| `installCorpus(corpus)` | Install a `SealedCorpus`: observations (shared, read-only), catalog, labs, lab accounts, ZIP coverage. |
|
|
552
744
|
| `corpusInfo()` | `CorpusInfo \| undefined` — label, recording date, source and counts. |
|
|
553
745
|
| `geoMode` | `"corpus" \| "synthetic"`, settable. |
|
|
554
|
-
| `configureLabAccounts(
|
|
746
|
+
| `configureLabAccounts(layout?)` / `labAccounts()` | Replace (a list, or `{ presets, accounts }`; `undefined` restores the default) / read the team's lab accounts. |
|
|
747
|
+
| `addLabAccount(input)` / `addLabAccountPreset(name, id?)` / `patchLabAccount(id, patch)` / `removeLabAccount(id)` | Change one account; kept across `reset()`. |
|
|
748
|
+
| `teamId` | The team this namespace answers as. |
|
|
749
|
+
| `limits` / `configureLimits(input)` | Read / change the sandbox limits; kept across `reset()`. |
|
|
750
|
+
| `identity` | `"strict" \| "adopt-users"`, settable. |
|
|
751
|
+
| `insertUsers(users)` / `insertOrders(orders, { emitWebhooks? })` / `importFixtures(fixtures, options?)` / `hardDeleteUser(id)` | The [fixture backdoor](#fixtures-and-identity). |
|
|
555
752
|
| `order(id)` / `orders()` | Read orders. |
|
|
556
753
|
| `transitionOrder(id, status, { now, flags? })` | Move an order to a full status and publish its webhook. |
|
|
557
754
|
| `installResultFixture(orderId, fixture)` / `resultFixture(orderId)` | Serve an exact result payload / PDF for an order. |
|
|
@@ -571,7 +768,11 @@ type JunctionAPIOptions = {
|
|
|
571
768
|
now?: () => number // clock in ms; default Date.now
|
|
572
769
|
corpus?: SealedCorpus // installed now and on every reset()
|
|
573
770
|
geo?: "corpus" | "synthetic" // default "corpus" with a corpus, else "synthetic"
|
|
574
|
-
labAccounts?: LabAccountInput[]
|
|
771
|
+
labAccounts?: LabAccountInput[] | { presets?: (string | { name; id? })[]; accounts?: LabAccountInput[] }
|
|
772
|
+
teamId?: string // default: the corpus's recorded team, else MOCK_TEAM_ID
|
|
773
|
+
limits?: { maxUsers?: number | null; simulateRequiresSandbox?: boolean; rateLimitPerSecond?: number | null }
|
|
774
|
+
identity?: "strict" | "adopt-users" // default "strict"
|
|
775
|
+
fixtures?: { users?: UserFixture[]; orders?: OrderFixture[] } // loaded now and on every reset()
|
|
575
776
|
onWebhook?: WebhookPublisher // called with every recorded event
|
|
576
777
|
webhook?: JunctionWebhookOptions
|
|
577
778
|
}
|
|
@@ -586,7 +787,7 @@ type JunctionWebhookEvent = {
|
|
|
586
787
|
event_type: "labtest.order.created" | "labtest.order.updated" | "labtest.appointment.updated"
|
|
587
788
|
data: Record<string, unknown>; team_id: string; user_id: string; client_user_id: string
|
|
588
789
|
}
|
|
589
|
-
type SealedCorpus = { version: 1; recordedAt; source; fingerprint?; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
|
|
790
|
+
type SealedCorpus = { version: 1 | 2; recordedAt; source; teamId?; fingerprint?; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
|
|
590
791
|
type SeedSource = { fetch: (request: Request) => Promise<Response>; baseUrl: string; headers: Record<string, string> }
|
|
591
792
|
type OperationId / SupportedOperationId // string unions of operationIds / supportedOperationIds
|
|
592
793
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseSealedCorpus
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ISDNA2TU.js";
|
|
4
4
|
|
|
5
5
|
// corpus/sandbox-sealed.json
|
|
6
6
|
var sandbox_sealed_default = {
|
|
@@ -145755,4 +145755,4 @@ var defaultCorpus = parseSealedCorpus(sandbox_sealed_default);
|
|
|
145755
145755
|
export {
|
|
145756
145756
|
defaultCorpus
|
|
145757
145757
|
};
|
|
145758
|
-
//# sourceMappingURL=chunk-
|
|
145758
|
+
//# sourceMappingURL=chunk-C2UGIPPM.js.map
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
// src/sealed-corpus.ts
|
|
2
|
-
var SEALED_CORPUS_VERSION =
|
|
2
|
+
var SEALED_CORPUS_VERSION = 2;
|
|
3
|
+
var SUPPORTED_SEALED_CORPUS_VERSIONS = [1, 2];
|
|
3
4
|
var parseSealedCorpus = (value) => {
|
|
4
5
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
5
6
|
throw new Error("sealed corpus must be an object");
|
|
6
7
|
}
|
|
7
8
|
const record = value;
|
|
8
|
-
if (record.version
|
|
9
|
+
if (!SUPPORTED_SEALED_CORPUS_VERSIONS.includes(record.version)) {
|
|
9
10
|
throw new Error(`unsupported sealed corpus version: ${String(record.version)}`);
|
|
10
11
|
}
|
|
12
|
+
if (record.teamId !== void 0 && typeof record.teamId !== "string") {
|
|
13
|
+
throw new Error("sealed corpus teamId must be a string");
|
|
14
|
+
}
|
|
11
15
|
return record;
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
export {
|
|
15
19
|
SEALED_CORPUS_VERSION,
|
|
20
|
+
SUPPORTED_SEALED_CORPUS_VERSIONS,
|
|
16
21
|
parseSealedCorpus
|
|
17
22
|
};
|
|
18
|
-
//# sourceMappingURL=chunk-
|
|
23
|
+
//# sourceMappingURL=chunk-ISDNA2TU.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/sealed-corpus.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ExpectedResult, LabTestRecord } from \"./catalog.js\"\nimport type { GetCacheEntry } from \"./state.js\"\n\n/**\n * Version of the sealed-corpus on-disk format. Bumped whenever the shape changes so a\n * stale recording is rejected instead of half-applied.\n *\n * - 1: observations, catalog and lab accounts.\n * - 2: adds `teamId`, the recording team, so lab-account allowlists load verbatim.\n */\nexport const SEALED_CORPUS_VERSION = 2\n\n/** Versions `parseSealedCorpus` still loads. A version-1 corpus behaves as it did in 0.2.0. */\nexport const SUPPORTED_SEALED_CORPUS_VERSIONS: readonly number[] = [1, 2]\n\n/**\n * Exact sandbox recording of the provider-owned reads a drop-in consumer depends on:\n * parameter-stable GET observations (area/PSC inventory), the lab catalog, team labs, and\n * lab accounts. Availability is intentionally absent \u2014 its slot dates and single-use\n * `booking_key`s can never match a live cache key, so it stays with the generator.\n */\nexport type SealedCorpus = {\n version: 1 | typeof SEALED_CORPUS_VERSION\n recordedAt: string\n source: string\n /**\n * The recording team's id (version 2+). When present it is the mock's default team,\n * and each lab account's `team_id_allowlist` is kept exactly as recorded.\n */\n teamId?: string\n /** SHA-256 of the recording's content, written by `corpus pull`. Identifies it in logs. */\n fingerprint?: string\n observations: Record<string, GetCacheEntry>\n catalog: {\n labTests: LabTestRecord[]\n labs: Record<string, unknown>[]\n expectedResults: Record<string, ExpectedResult[]>\n }\n labAccounts: Record<string, unknown>[]\n}\n\n/** Throws on a non-object or an unrecognized `version`. */\nexport const parseSealedCorpus = (value: unknown): SealedCorpus => {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new Error(\"sealed corpus must be an object\")\n }\n const record = value as Record<string, unknown>\n if (!SUPPORTED_SEALED_CORPUS_VERSIONS.includes(record.version as number)) {\n throw new Error(`unsupported sealed corpus version: ${String(record.version)}`)\n }\n if (record.teamId !== undefined && typeof record.teamId !== \"string\") {\n throw new Error(\"sealed corpus teamId must be a string\")\n }\n return record as unknown as SealedCorpus\n}\n"],
|
|
5
|
+
"mappings": ";AAUO,IAAM,wBAAwB;AAG9B,IAAM,mCAAsD,CAAC,GAAG,CAAC;AA6BjE,IAAM,oBAAoB,CAAC,UAAiC;AACjE,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,UAAM,IAAI,MAAM,iCAAiC;AAAA,EACnD;AACA,QAAM,SAAS;AACf,MAAI,CAAC,iCAAiC,SAAS,OAAO,OAAiB,GAAG;AACxE,UAAM,IAAI,MAAM,sCAAsC,OAAO,OAAO,OAAO,CAAC,EAAE;AAAA,EAChF;AACA,MAAI,OAAO,WAAW,UAAa,OAAO,OAAO,WAAW,UAAU;AACpE,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
IDENTITY_MODES,
|
|
2
3
|
createRuntime,
|
|
3
4
|
pullCorpus
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-NFWUEPW4.js";
|
|
5
6
|
import {
|
|
6
7
|
defaultCorpus
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-C2UGIPPM.js";
|
|
8
9
|
import {
|
|
9
10
|
parseSealedCorpus
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ISDNA2TU.js";
|
|
11
12
|
|
|
12
13
|
// src/server.ts
|
|
13
14
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
@@ -156,6 +157,10 @@ var COMMON_SERVE_OPTIONS = {
|
|
|
156
157
|
description: "Request log format",
|
|
157
158
|
default: "pretty"
|
|
158
159
|
},
|
|
160
|
+
"log-requests": {
|
|
161
|
+
type: "boolean",
|
|
162
|
+
description: "One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json"
|
|
163
|
+
},
|
|
159
164
|
config: {
|
|
160
165
|
type: "string",
|
|
161
166
|
value: "<file>",
|
|
@@ -171,7 +176,8 @@ var formatLog = (format) => {
|
|
|
171
176
|
const op = entry.operationId ?? (entry.unmatched ? "UNMATCHED" : "-");
|
|
172
177
|
const ns = entry.namespace === "default" ? "" : ` [${entry.namespace}]`;
|
|
173
178
|
const fault = entry.faultId ? ` fault=${entry.faultId}` : "";
|
|
174
|
-
|
|
179
|
+
const adopted = entry.adopted ? " adopted" : "";
|
|
180
|
+
console.log(`${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}`);
|
|
175
181
|
};
|
|
176
182
|
};
|
|
177
183
|
var asString = (value) => typeof value === "string" ? value : void 0;
|
|
@@ -215,7 +221,7 @@ var serveCommand = (target) => ({
|
|
|
215
221
|
summary: `Serve the ${target.name} mock over HTTP`,
|
|
216
222
|
options: { ...COMMON_SERVE_OPTIONS, ...target.options },
|
|
217
223
|
async run(values) {
|
|
218
|
-
const log = asString(values.log) ?? "pretty";
|
|
224
|
+
const log = values["log-requests"] === true ? "json" : asString(values.log) ?? "pretty";
|
|
219
225
|
if (!["pretty", "json", "off"].includes(log)) {
|
|
220
226
|
console.error(`--log must be pretty, json or off (got ${log})`);
|
|
221
227
|
return 2;
|
|
@@ -283,6 +289,21 @@ var createServer2 = async (options = {}) => {
|
|
|
283
289
|
return { ...listening, runtime };
|
|
284
290
|
};
|
|
285
291
|
var asString2 = (value) => typeof value === "string" && value !== "" ? value : void 0;
|
|
292
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
293
|
+
var count = (value, flag) => {
|
|
294
|
+
if (value === void 0) return void 0;
|
|
295
|
+
if (!/^\d+$/.test(value)) throw new Error(`${flag} must be a non-negative integer (got ${value})`);
|
|
296
|
+
return Number.parseInt(value, 10);
|
|
297
|
+
};
|
|
298
|
+
var readJson = async (path) => {
|
|
299
|
+
const raw = await readFile2(path, "utf8").catch(() => void 0);
|
|
300
|
+
if (raw === void 0) throw new Error(`junction mock: file not found: ${path}`);
|
|
301
|
+
try {
|
|
302
|
+
return JSON.parse(raw);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
throw new Error(`junction mock: ${path} is not JSON (${error.message})`);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
286
307
|
var retryDelays = (value) => {
|
|
287
308
|
if (value === void 0) return void 0;
|
|
288
309
|
const delays = value.split(",").map((part) => Number.parseInt(part.trim(), 10));
|
|
@@ -309,7 +330,34 @@ var serveTarget = {
|
|
|
309
330
|
"lab-accounts": {
|
|
310
331
|
type: "string",
|
|
311
332
|
value: "<file>",
|
|
312
|
-
description:
|
|
333
|
+
description: 'Lab accounts to route orders through: a JSON array, or { "presets": [...], "accounts": [...] }'
|
|
334
|
+
},
|
|
335
|
+
"team-id": {
|
|
336
|
+
type: "string",
|
|
337
|
+
value: "<uuid>",
|
|
338
|
+
description: "Team the mock answers as (default: the corpus's recorded team)"
|
|
339
|
+
},
|
|
340
|
+
"max-users": {
|
|
341
|
+
type: "string",
|
|
342
|
+
value: "<n>",
|
|
343
|
+
description: "Enforce the sandbox's live-user cap (default: unlimited)"
|
|
344
|
+
},
|
|
345
|
+
identity: {
|
|
346
|
+
type: "string",
|
|
347
|
+
value: "<strict|adopt-users>",
|
|
348
|
+
description: "Unknown user ids: 404 (strict) or create on first use",
|
|
349
|
+
default: "strict"
|
|
350
|
+
},
|
|
351
|
+
fixtures: {
|
|
352
|
+
type: "string",
|
|
353
|
+
value: "<file>",
|
|
354
|
+
description: 'Users and orders every namespace starts with: { "users": [...], "orders": [...] }'
|
|
355
|
+
},
|
|
356
|
+
"journal-size": {
|
|
357
|
+
type: "string",
|
|
358
|
+
value: "<n>",
|
|
359
|
+
description: "Requests each namespace's journal keeps (GET /__admin/requests)",
|
|
360
|
+
default: "1000"
|
|
313
361
|
},
|
|
314
362
|
"seed-url": {
|
|
315
363
|
type: "string",
|
|
@@ -356,7 +404,19 @@ var serveTarget = {
|
|
|
356
404
|
throw new Error(`--geo must be corpus or synthetic (got ${geo})`);
|
|
357
405
|
}
|
|
358
406
|
const accountsFile = asString2(values["lab-accounts"]);
|
|
359
|
-
const labAccounts = accountsFile === void 0 ? void 0 :
|
|
407
|
+
const labAccounts = accountsFile === void 0 ? void 0 : await readJson(accountsFile);
|
|
408
|
+
const fixturesFile = asString2(values.fixtures);
|
|
409
|
+
const fixtures = fixturesFile === void 0 ? void 0 : await readJson(fixturesFile);
|
|
410
|
+
const teamId = asString2(values["team-id"]);
|
|
411
|
+
if (teamId !== void 0 && !UUID.test(teamId)) {
|
|
412
|
+
throw new Error(`--team-id must be a UUID (got ${teamId})`);
|
|
413
|
+
}
|
|
414
|
+
const maxUsers = count(asString2(values["max-users"]), "--max-users");
|
|
415
|
+
const journalSize = count(asString2(values["journal-size"]), "--journal-size");
|
|
416
|
+
const identity = asString2(values.identity);
|
|
417
|
+
if (identity !== void 0 && !IDENTITY_MODES.includes(identity)) {
|
|
418
|
+
throw new Error(`--identity must be one of ${IDENTITY_MODES.join(", ")} (got ${identity})`);
|
|
419
|
+
}
|
|
360
420
|
const webhookUrl = asString2(values["webhook-url"]);
|
|
361
421
|
const webhookSecret = asString2(values["webhook-secret"]) ?? process.env.MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET;
|
|
362
422
|
if (webhookUrl !== void 0 && webhookSecret === void 0) {
|
|
@@ -368,6 +428,11 @@ var serveTarget = {
|
|
|
368
428
|
...corpus ? { corpus } : {},
|
|
369
429
|
...geo ? { geo } : {},
|
|
370
430
|
...labAccounts ? { labAccounts } : {},
|
|
431
|
+
...fixtures ? { fixtures } : {},
|
|
432
|
+
...teamId !== void 0 ? { teamId } : {},
|
|
433
|
+
...maxUsers !== void 0 ? { limits: { maxUsers } } : {},
|
|
434
|
+
...identity !== void 0 ? { identity } : {},
|
|
435
|
+
...journalSize !== void 0 ? { journalSize } : {},
|
|
371
436
|
...webhookUrl !== void 0 && webhookSecret !== void 0 ? {
|
|
372
437
|
webhooks: {
|
|
373
438
|
url: webhookUrl,
|
|
@@ -388,7 +453,10 @@ var serveTarget = {
|
|
|
388
453
|
return [
|
|
389
454
|
info ? `corpus ${info.label}: ${info.observations} observations, ${info.zips} ZIPs, ${info.labTests} lab tests, ${info.labAccounts} lab accounts (recorded ${info.recordedAt} from ${info.source})` : "corpus none: synthetic catalog and coverage",
|
|
390
455
|
`geo ${api.geoMode}${api.geoMode === "corpus" ? " (unknown ZIPs answer 424 MOCKINGBIRD_UNKNOWN_ZIP)" : ""}`,
|
|
456
|
+
`team ${api.teamId}`,
|
|
391
457
|
`lab accounts: ${api.labAccounts().length}`,
|
|
458
|
+
`limits: ${Object.entries(api.limits).filter(([, value]) => value !== null && value !== false).map(([key, value]) => `${key}=${String(value)}`).join(", ") || "none (sandbox caps off)"}`,
|
|
459
|
+
`identity ${api.identity}`,
|
|
392
460
|
`webhooks: ${junction.webhooks ? "signed delivery on" : "off"}`,
|
|
393
461
|
"auth: any x-vital-api-key value (e.g. sk_us_mockingbird)"
|
|
394
462
|
];
|
|
@@ -403,4 +471,4 @@ export {
|
|
|
403
471
|
createServer2 as createServer,
|
|
404
472
|
serveTarget
|
|
405
473
|
};
|
|
406
|
-
//# sourceMappingURL=chunk-
|
|
474
|
+
//# sourceMappingURL=chunk-KLBGX23P.js.map
|