@crvouga/mockingbird-service-junction 0.1.2 → 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.
@@ -0,0 +1,1210 @@
1
+ import { Server } from 'node:http';
2
+ import { Hono } from 'hono';
3
+
4
+ /**
5
+ * The single source of time for a service.
6
+ *
7
+ * Every timestamp a mock writes reads from here, so a suite moves time instead of
8
+ * sleeping: appointment windows, result delays and expiries become reachable in
9
+ * milliseconds. A frozen clock also makes timestamps reproducible from a seed.
10
+ */
11
+ type ClockState$1 = {
12
+ /** Current epoch milliseconds. */
13
+ now: number;
14
+ /** True while time does not advance on its own. */
15
+ frozen: boolean;
16
+ /** Milliseconds this clock adds to its underlying source. */
17
+ offsetMs: number;
18
+ };
19
+ type Clock$1 = {
20
+ now(): number;
21
+ /** Pin the clock to an exact instant, keeping it frozen if it already was. */
22
+ set(epochMs: number): void;
23
+ /** Move the clock forward, or back with a negative delta. */
24
+ advance(deltaMs: number): void;
25
+ /** Stop time at the current instant. */
26
+ freeze(): void;
27
+ /** Resume from the current instant. */
28
+ unfreeze(): void;
29
+ /** Drop back to the underlying source, live. */
30
+ reset(): void;
31
+ state(): ClockState$1;
32
+ };
33
+
34
+ /** Bind values accepted by Mockingbird's SQLite port (matches sqlite-mem / better-sqlite3). */
35
+ type SqliteValue$2 = null | number | bigint | string | Uint8Array | boolean;
36
+ /** Mutation counters returned by {@link SqliteStatement.run}. */
37
+ type SqliteRunResult$2 = {
38
+ changes: number;
39
+ lastInsertRowid: number | bigint;
40
+ };
41
+ /**
42
+ * Prepared statement bound to a {@link SqliteClient}.
43
+ *
44
+ * Pass bind values as rest arguments on each call (no sticky `bind()`).
45
+ */
46
+ interface SqliteStatement$2 {
47
+ run(...params: SqliteValue$2[]): SqliteRunResult$2;
48
+ all<T = Record<string, unknown>>(...params: SqliteValue$2[]): T[];
49
+ get<T = Record<string, unknown>>(...params: SqliteValue$2[]): T | undefined;
50
+ }
51
+ /**
52
+ * Sync SQLite client port owned by Mockingbird.
53
+ *
54
+ * Duck-typed so `@crvouga/mockingbird-service-sqlite` `Database`, better-sqlite3, and wrapped
55
+ * `bun:sqlite` instances all work when they expose this surface.
56
+ */
57
+ interface SqliteClient$2 {
58
+ exec(sql: string): void;
59
+ prepare(sql: string): SqliteStatement$2;
60
+ transaction<T>(fn: () => T): T;
61
+ }
62
+
63
+ /**
64
+ * Seeded pseudo-random numbers, so anything a mock invents — ids, jitter, which
65
+ * request a percentage fault hits — is reproducible from a seed.
66
+ *
67
+ * mulberry32: small, fast, and stable across runtimes, which matters more here
68
+ * than statistical quality.
69
+ */
70
+ type Rng$1 = {
71
+ /** Next value in `[0, 1)`. */
72
+ next(): number;
73
+ /** Next integer in `[min, max]`. */
74
+ int(min: number, max: number): number;
75
+ /** Restart the stream from its seed. */
76
+ reset(): void;
77
+ seed: number;
78
+ };
79
+
80
+ /**
81
+ * A deliberate failure injected in front of an operation.
82
+ *
83
+ * This is how a suite reaches the vendor's failure modes without the vendor: the
84
+ * quota error that only appears when a shared sandbox is full, the 429 that only
85
+ * appears under load, the 5xx that proves a retry path works.
86
+ */
87
+ type FaultRule$1 = {
88
+ /** Stable id, so a suite can retire exactly the rule it added. */
89
+ id: string;
90
+ /** Fault only this operation. Omit to match every operation. */
91
+ operationId?: string;
92
+ /** Fault only this HTTP method, case-insensitive. Omit to match every method. */
93
+ method?: string;
94
+ /** Fault only paths starting with this prefix. Omit to match every path. */
95
+ pathPrefix?: string;
96
+ /**
97
+ * Fault only this namespace. Omit (or `"*"`) to fault every namespace — which is what
98
+ * an in-process caller usually wants, and what a parallel worker usually does not:
99
+ * rules added through `POST /__admin/faults` default to the calling namespace.
100
+ */
101
+ namespace?: string;
102
+ status: number;
103
+ /** Response body, serialized as JSON. A string is sent as-is. */
104
+ body?: unknown;
105
+ headers?: Record<string, string>;
106
+ /** Retire the rule after this many faults. Omit to keep it until removed. */
107
+ count?: number;
108
+ /** Fault this fraction of matching requests, `0`–`1`. Default `1`. */
109
+ rate?: number;
110
+ /** Hold the response back this long, to exercise timeouts. */
111
+ delayMs?: number;
112
+ };
113
+ /** What a request looks like to the fault matcher. */
114
+ type FaultCandidate$1 = {
115
+ operationId: string | undefined;
116
+ method: string;
117
+ path: string;
118
+ namespace: string;
119
+ };
120
+ type FaultRegistry$1 = {
121
+ add(rule: FaultRule$1): FaultRule$1;
122
+ list(): (FaultRule$1 & {
123
+ remaining: number | null;
124
+ hits: number;
125
+ })[];
126
+ remove(id: string): boolean;
127
+ clear(): void;
128
+ /**
129
+ * The fault this request should get, or `undefined` to let it through.
130
+ * Consumes one of the matching rule's remaining uses.
131
+ */
132
+ take(candidate: FaultCandidate$1): Promise<{
133
+ id: string;
134
+ response: Response;
135
+ } | undefined>;
136
+ };
137
+
138
+ /** One handled request, as the structured log sees it. */
139
+ type RequestLog$1 = {
140
+ service: string;
141
+ namespace: string;
142
+ operationId: string | undefined;
143
+ method: string;
144
+ path: string;
145
+ status: number;
146
+ durationMs: number;
147
+ /** True when the path matched no operation in the contract. */
148
+ unmatched: boolean;
149
+ /** Set when a fault rule produced the response. */
150
+ faultId?: string;
151
+ /** Resource ids the handler touched (`userId`, `orderId`, …), when the service reports them. */
152
+ ids?: Record<string, string>;
153
+ /** Set when the service created a resource the request referred to but that did not exist. */
154
+ adopted?: boolean;
155
+ };
156
+ type MetricsReport$1 = {
157
+ requests: number;
158
+ /** Counts keyed `<operationId> <status>`. */
159
+ byOperation: Record<string, number>;
160
+ /**
161
+ * Paths that matched no operation, most frequent first.
162
+ *
163
+ * This is the early-warning signal: a consumer calling something the mock does
164
+ * not implement shows up here as a count, before it fails a suite as a 404.
165
+ */
166
+ unmatched: {
167
+ method: string;
168
+ path: string;
169
+ count: number;
170
+ }[];
171
+ faults: number;
172
+ totalDurationMs: number;
173
+ };
174
+ type Metrics$1 = {
175
+ record(entry: RequestLog$1): void;
176
+ report(): MetricsReport$1;
177
+ reset(): void;
178
+ };
179
+
180
+ /** One journal entry: a request log stamped with when (on the mock clock) it was handled. */
181
+ type JournalEntry$1 = RequestLog$1 & {
182
+ at: string;
183
+ };
184
+ type JournalQuery$1 = {
185
+ /** Only this namespace. Omit for every namespace, oldest first across all of them. */
186
+ namespace?: string;
187
+ operationId?: string;
188
+ status?: number;
189
+ /** Only entries at or after this instant (epoch ms). */
190
+ since?: number;
191
+ /** At most this many, the most recent kept. */
192
+ limit?: number;
193
+ };
194
+ type Journal$1 = {
195
+ readonly size: number;
196
+ record(entry: JournalEntry$1): void;
197
+ list(query?: JournalQuery$1): JournalEntry$1[];
198
+ /** Forget one namespace's entries, or every namespace's. */
199
+ clear(namespace?: string): void;
200
+ };
201
+
202
+ /**
203
+ * A point-in-time copy of everything a service namespace holds.
204
+ *
205
+ * All service state lives in the two core tables keyed by namespace, so a snapshot
206
+ * is generic: any service gets per-test rollback without knowing its own schema.
207
+ * Restoring is much cheaper than rebuilding a namespace from a corpus.
208
+ */
209
+ type NamespaceSnapshot$1 = {
210
+ namespace: string;
211
+ records: {
212
+ collection: string;
213
+ id: string;
214
+ seq: number;
215
+ value: string;
216
+ }[];
217
+ sequences: {
218
+ name: string;
219
+ kind: string;
220
+ value: number;
221
+ }[];
222
+ };
223
+
224
+ /**
225
+ * Anything that can answer a Fetch `Request` with a `Response`.
226
+ *
227
+ * Every Mockingbird service implements this, and every runtime adapter consumes it.
228
+ * It is the only contract shared across the whole graph.
229
+ */
230
+ interface FetchAPI$2 {
231
+ fetch(request: Request): Promise<Response>;
232
+ }
233
+
234
+ /** What the runtime needs from a service: a Fetch handler it can reset. */
235
+ type ServiceInstance$1 = FetchAPI$2 & {
236
+ reset(): Promise<void>;
237
+ };
238
+ type ServiceRuntime$1<T extends ServiceInstance$1> = FetchAPI$2 & {
239
+ readonly name: string;
240
+ readonly sqlite: SqliteClient$2;
241
+ readonly clock: Clock$1;
242
+ readonly faults: FaultRegistry$1;
243
+ readonly metrics: Metrics$1;
244
+ readonly journal: Journal$1;
245
+ readonly rng: Rng$1;
246
+ /** The instance behind `namespace` (the default one when omitted), created on first use. */
247
+ instance(namespace?: string): T;
248
+ /** Public names of every namespace created so far. */
249
+ namespaces(): string[];
250
+ /** Reset one namespace, or every namespace with `"*"`. */
251
+ reset(namespace?: string): Promise<void>;
252
+ snapshot(namespace?: string): NamespaceSnapshot$1;
253
+ restore(snapshot: NamespaceSnapshot$1, namespace?: string): void;
254
+ };
255
+
256
+ type CliOption = {
257
+ type: "string" | "boolean";
258
+ description: string;
259
+ /** Shown in help; the value placeholder, e.g. `<port>`. */
260
+ value?: string;
261
+ default?: string | boolean;
262
+ };
263
+ type CliValues = Record<string, string | boolean | undefined>;
264
+ type CommonServeOptions = {
265
+ adminKey: string | undefined;
266
+ seed: string | undefined;
267
+ onLog: ((entry: RequestLog$1) => void) | undefined;
268
+ };
269
+ /**
270
+ * What a service contributes to `serve`: how to build its runtime from CLI flags,
271
+ * and what to say at startup. Every service's `./server` entry exports one as
272
+ * `serveTarget`, which is also how `serve --config` finds services by name.
273
+ */
274
+ type ServeTarget = {
275
+ name: string;
276
+ defaultPort: number;
277
+ /** Serve flags beyond the common ones. */
278
+ options?: Record<string, CliOption>;
279
+ create(values: CliValues, common: CommonServeOptions): Promise<ServiceRuntime$1<ServiceInstance$1>> | ServiceRuntime$1<ServiceInstance$1>;
280
+ /** Startup lines after the listen address, e.g. the loaded corpus version. */
281
+ banner?(runtime: ServiceRuntime$1<ServiceInstance$1>): string[];
282
+ };
283
+
284
+ /** A running server, with the address it actually bound. */
285
+ type Listening = {
286
+ url: string;
287
+ port: number;
288
+ host: string;
289
+ server: Server;
290
+ close(): Promise<void>;
291
+ };
292
+
293
+ /**
294
+ * The single source of time for a service.
295
+ *
296
+ * Every timestamp a mock writes reads from here, so a suite moves time instead of
297
+ * sleeping: appointment windows, result delays and expiries become reachable in
298
+ * milliseconds. A frozen clock also makes timestamps reproducible from a seed.
299
+ */
300
+ type ClockState = {
301
+ /** Current epoch milliseconds. */
302
+ now: number;
303
+ /** True while time does not advance on its own. */
304
+ frozen: boolean;
305
+ /** Milliseconds this clock adds to its underlying source. */
306
+ offsetMs: number;
307
+ };
308
+ type Clock = {
309
+ now(): number;
310
+ /** Pin the clock to an exact instant, keeping it frozen if it already was. */
311
+ set(epochMs: number): void;
312
+ /** Move the clock forward, or back with a negative delta. */
313
+ advance(deltaMs: number): void;
314
+ /** Stop time at the current instant. */
315
+ freeze(): void;
316
+ /** Resume from the current instant. */
317
+ unfreeze(): void;
318
+ /** Drop back to the underlying source, live. */
319
+ reset(): void;
320
+ state(): ClockState;
321
+ };
322
+
323
+ /** Bind values accepted by Mockingbird's SQLite port (matches sqlite-mem / better-sqlite3). */
324
+ type SqliteValue$1 = null | number | bigint | string | Uint8Array | boolean;
325
+ /** Mutation counters returned by {@link SqliteStatement.run}. */
326
+ type SqliteRunResult$1 = {
327
+ changes: number;
328
+ lastInsertRowid: number | bigint;
329
+ };
330
+ /**
331
+ * Prepared statement bound to a {@link SqliteClient}.
332
+ *
333
+ * Pass bind values as rest arguments on each call (no sticky `bind()`).
334
+ */
335
+ interface SqliteStatement$1 {
336
+ run(...params: SqliteValue$1[]): SqliteRunResult$1;
337
+ all<T = Record<string, unknown>>(...params: SqliteValue$1[]): T[];
338
+ get<T = Record<string, unknown>>(...params: SqliteValue$1[]): T | undefined;
339
+ }
340
+ /**
341
+ * Sync SQLite client port owned by Mockingbird.
342
+ *
343
+ * Duck-typed so `@crvouga/mockingbird-service-sqlite` `Database`, better-sqlite3, and wrapped
344
+ * `bun:sqlite` instances all work when they expose this surface.
345
+ */
346
+ interface SqliteClient$1 {
347
+ exec(sql: string): void;
348
+ prepare(sql: string): SqliteStatement$1;
349
+ transaction<T>(fn: () => T): T;
350
+ }
351
+
352
+ /**
353
+ * Seeded pseudo-random numbers, so anything a mock invents — ids, jitter, which
354
+ * request a percentage fault hits — is reproducible from a seed.
355
+ *
356
+ * mulberry32: small, fast, and stable across runtimes, which matters more here
357
+ * than statistical quality.
358
+ */
359
+ type Rng = {
360
+ /** Next value in `[0, 1)`. */
361
+ next(): number;
362
+ /** Next integer in `[min, max]`. */
363
+ int(min: number, max: number): number;
364
+ /** Restart the stream from its seed. */
365
+ reset(): void;
366
+ seed: number;
367
+ };
368
+
369
+ /**
370
+ * A deliberate failure injected in front of an operation.
371
+ *
372
+ * This is how a suite reaches the vendor's failure modes without the vendor: the
373
+ * quota error that only appears when a shared sandbox is full, the 429 that only
374
+ * appears under load, the 5xx that proves a retry path works.
375
+ */
376
+ type FaultRule = {
377
+ /** Stable id, so a suite can retire exactly the rule it added. */
378
+ id: string;
379
+ /** Fault only this operation. Omit to match every operation. */
380
+ operationId?: string;
381
+ /** Fault only this HTTP method, case-insensitive. Omit to match every method. */
382
+ method?: string;
383
+ /** Fault only paths starting with this prefix. Omit to match every path. */
384
+ pathPrefix?: string;
385
+ /**
386
+ * Fault only this namespace. Omit (or `"*"`) to fault every namespace — which is what
387
+ * an in-process caller usually wants, and what a parallel worker usually does not:
388
+ * rules added through `POST /__admin/faults` default to the calling namespace.
389
+ */
390
+ namespace?: string;
391
+ status: number;
392
+ /** Response body, serialized as JSON. A string is sent as-is. */
393
+ body?: unknown;
394
+ headers?: Record<string, string>;
395
+ /** Retire the rule after this many faults. Omit to keep it until removed. */
396
+ count?: number;
397
+ /** Fault this fraction of matching requests, `0`–`1`. Default `1`. */
398
+ rate?: number;
399
+ /** Hold the response back this long, to exercise timeouts. */
400
+ delayMs?: number;
401
+ };
402
+ /** What a request looks like to the fault matcher. */
403
+ type FaultCandidate = {
404
+ operationId: string | undefined;
405
+ method: string;
406
+ path: string;
407
+ namespace: string;
408
+ };
409
+ type FaultRegistry = {
410
+ add(rule: FaultRule): FaultRule;
411
+ list(): (FaultRule & {
412
+ remaining: number | null;
413
+ hits: number;
414
+ })[];
415
+ remove(id: string): boolean;
416
+ clear(): void;
417
+ /**
418
+ * The fault this request should get, or `undefined` to let it through.
419
+ * Consumes one of the matching rule's remaining uses.
420
+ */
421
+ take(candidate: FaultCandidate): Promise<{
422
+ id: string;
423
+ response: Response;
424
+ } | undefined>;
425
+ };
426
+
427
+ /** One handled request, as the structured log sees it. */
428
+ type RequestLog = {
429
+ service: string;
430
+ namespace: string;
431
+ operationId: string | undefined;
432
+ method: string;
433
+ path: string;
434
+ status: number;
435
+ durationMs: number;
436
+ /** True when the path matched no operation in the contract. */
437
+ unmatched: boolean;
438
+ /** Set when a fault rule produced the response. */
439
+ faultId?: string;
440
+ /** Resource ids the handler touched (`userId`, `orderId`, …), when the service reports them. */
441
+ ids?: Record<string, string>;
442
+ /** Set when the service created a resource the request referred to but that did not exist. */
443
+ adopted?: boolean;
444
+ };
445
+ type MetricsReport = {
446
+ requests: number;
447
+ /** Counts keyed `<operationId> <status>`. */
448
+ byOperation: Record<string, number>;
449
+ /**
450
+ * Paths that matched no operation, most frequent first.
451
+ *
452
+ * This is the early-warning signal: a consumer calling something the mock does
453
+ * not implement shows up here as a count, before it fails a suite as a 404.
454
+ */
455
+ unmatched: {
456
+ method: string;
457
+ path: string;
458
+ count: number;
459
+ }[];
460
+ faults: number;
461
+ totalDurationMs: number;
462
+ };
463
+ type Metrics = {
464
+ record(entry: RequestLog): void;
465
+ report(): MetricsReport;
466
+ reset(): void;
467
+ };
468
+
469
+ /** One journal entry: a request log stamped with when (on the mock clock) it was handled. */
470
+ type JournalEntry = RequestLog & {
471
+ at: string;
472
+ };
473
+ type JournalQuery = {
474
+ /** Only this namespace. Omit for every namespace, oldest first across all of them. */
475
+ namespace?: string;
476
+ operationId?: string;
477
+ status?: number;
478
+ /** Only entries at or after this instant (epoch ms). */
479
+ since?: number;
480
+ /** At most this many, the most recent kept. */
481
+ limit?: number;
482
+ };
483
+ type Journal = {
484
+ readonly size: number;
485
+ record(entry: JournalEntry): void;
486
+ list(query?: JournalQuery): JournalEntry[];
487
+ /** Forget one namespace's entries, or every namespace's. */
488
+ clear(namespace?: string): void;
489
+ };
490
+
491
+ /**
492
+ * A point-in-time copy of everything a service namespace holds.
493
+ *
494
+ * All service state lives in the two core tables keyed by namespace, so a snapshot
495
+ * is generic: any service gets per-test rollback without knowing its own schema.
496
+ * Restoring is much cheaper than rebuilding a namespace from a corpus.
497
+ */
498
+ type NamespaceSnapshot = {
499
+ namespace: string;
500
+ records: {
501
+ collection: string;
502
+ id: string;
503
+ seq: number;
504
+ value: string;
505
+ }[];
506
+ sequences: {
507
+ name: string;
508
+ kind: string;
509
+ value: number;
510
+ }[];
511
+ };
512
+
513
+ /**
514
+ * Anything that can answer a Fetch `Request` with a `Response`.
515
+ *
516
+ * Every Mockingbird service implements this, and every runtime adapter consumes it.
517
+ * It is the only contract shared across the whole graph.
518
+ */
519
+ interface FetchAPI$1 {
520
+ fetch(request: Request): Promise<Response>;
521
+ }
522
+
523
+ /** What the runtime needs from a service: a Fetch handler it can reset. */
524
+ type ServiceInstance = FetchAPI$1 & {
525
+ reset(): Promise<void>;
526
+ };
527
+ type ServiceRuntime<T extends ServiceInstance> = FetchAPI$1 & {
528
+ readonly name: string;
529
+ readonly sqlite: SqliteClient$1;
530
+ readonly clock: Clock;
531
+ readonly faults: FaultRegistry;
532
+ readonly metrics: Metrics;
533
+ readonly journal: Journal;
534
+ readonly rng: Rng;
535
+ /** The instance behind `namespace` (the default one when omitted), created on first use. */
536
+ instance(namespace?: string): T;
537
+ /** Public names of every namespace created so far. */
538
+ namespaces(): string[];
539
+ /** Reset one namespace, or every namespace with `"*"`. */
540
+ reset(namespace?: string): Promise<void>;
541
+ snapshot(namespace?: string): NamespaceSnapshot;
542
+ restore(snapshot: NamespaceSnapshot, namespace?: string): void;
543
+ };
544
+
545
+ /** Options every provider constructor accepts. */
546
+ type APIOptions = {
547
+ /** Sync SQLite client. Defaults to `@crvouga/mockingbird-service-sqlite`. */
548
+ sqlite?: SqliteClient$1;
549
+ /** Clock used for `created`-style fields. Default `Date.now`. */
550
+ now?: () => number;
551
+ /**
552
+ * Storage namespace for this instance's records. Instances sharing one SQLite
553
+ * client stay isolated when their namespaces differ. Defaults to the service name.
554
+ */
555
+ namespace?: string;
556
+ };
557
+
558
+ /** Bind values accepted by Mockingbird's SQLite port (matches sqlite-mem / better-sqlite3). */
559
+ type SqliteValue = null | number | bigint | string | Uint8Array | boolean;
560
+ /** Mutation counters returned by {@link SqliteStatement.run}. */
561
+ type SqliteRunResult = {
562
+ changes: number;
563
+ lastInsertRowid: number | bigint;
564
+ };
565
+ /**
566
+ * Prepared statement bound to a {@link SqliteClient}.
567
+ *
568
+ * Pass bind values as rest arguments on each call (no sticky `bind()`).
569
+ */
570
+ interface SqliteStatement {
571
+ run(...params: SqliteValue[]): SqliteRunResult;
572
+ all<T = Record<string, unknown>>(...params: SqliteValue[]): T[];
573
+ get<T = Record<string, unknown>>(...params: SqliteValue[]): T | undefined;
574
+ }
575
+ /**
576
+ * Sync SQLite client port owned by Mockingbird.
577
+ *
578
+ * Duck-typed so `@crvouga/mockingbird-service-sqlite` `Database`, better-sqlite3, and wrapped
579
+ * `bun:sqlite` instances all work when they expose this surface.
580
+ */
581
+ interface SqliteClient {
582
+ exec(sql: string): void;
583
+ prepare(sql: string): SqliteStatement;
584
+ transaction<T>(fn: () => T): T;
585
+ }
586
+
587
+ /** Lab-test catalog mirrored from the Junction sandbox (`GET /v3/lab_test`). */
588
+ type CatalogMarker = {
589
+ id: number;
590
+ name: string;
591
+ slug: string;
592
+ description: string | null;
593
+ lab_id: number | null;
594
+ provider_id: string | null;
595
+ type: string | null;
596
+ unit: string | null;
597
+ price: string | null;
598
+ aoe: unknown;
599
+ a_la_carte_enabled: boolean;
600
+ common_tat_days: number | null;
601
+ worst_case_tat_days: number | null;
602
+ is_orderable: boolean | null;
603
+ };
604
+ type ExpectedResultLoinc = {
605
+ id: number;
606
+ name: string;
607
+ slug: string;
608
+ code: string;
609
+ unit: string | null;
610
+ };
611
+ type ExpectedResult = {
612
+ id: number;
613
+ name: string;
614
+ slug: string;
615
+ lab_id: number | null;
616
+ provider_id: string | null;
617
+ required: boolean;
618
+ loinc: ExpectedResultLoinc | null;
619
+ };
620
+ type LabTestRecord = {
621
+ id: string;
622
+ slug: string;
623
+ name: string;
624
+ sample_type: string;
625
+ method: string;
626
+ price: number;
627
+ is_active: boolean;
628
+ status: string;
629
+ fasting: boolean;
630
+ lab: Record<string, unknown>;
631
+ markers: CatalogMarker[] | null;
632
+ is_delegated: boolean;
633
+ auto_generated: boolean;
634
+ has_collection_instructions: boolean | null;
635
+ common_tat_days: number | null;
636
+ worst_case_tat_days: number | null;
637
+ };
638
+
639
+ type JunctionLimits = {
640
+ /**
641
+ * Live users a team may hold; creating one more answers the sandbox's
642
+ * "maximum of 50 Sandbox users" 400. Deleted users free their slot. `null`: unlimited.
643
+ */
644
+ maxUsers: number | null;
645
+ /** Refuse `POST /v3/order/{id}/test` unless the API key is a sandbox key (`sk_us_…`/`sk_eu_…`). */
646
+ simulateRequiresSandbox: boolean;
647
+ /** Vendor requests per second per namespace before a 429. `null`: unlimited. */
648
+ rateLimitPerSecond: number | null;
649
+ };
650
+ type JunctionLimitsInput = {
651
+ [K in keyof JunctionLimits]?: JunctionLimits[K] | undefined;
652
+ };
653
+
654
+ type GetCacheEntry = {
655
+ status: number;
656
+ headers: Record<string, string>;
657
+ body: unknown;
658
+ };
659
+ type JunctionWebhookEvent = {
660
+ event_type: "labtest.order.created" | "labtest.order.updated" | "labtest.appointment.updated";
661
+ data: Record<string, unknown>;
662
+ team_id: string;
663
+ user_id: string;
664
+ client_user_id: string;
665
+ };
666
+ type WebhookDeliveryAttempt = {
667
+ message_id: string;
668
+ event: JunctionWebhookEvent;
669
+ attempt: number;
670
+ scheduled_at: string;
671
+ timeout_ms: number;
672
+ acknowledged: boolean;
673
+ };
674
+ type UserRecord = {
675
+ user_id: string;
676
+ team_id: string;
677
+ client_user_id: string;
678
+ created_on: string;
679
+ connected_sources: unknown[];
680
+ fallback_time_zone: {
681
+ id: string;
682
+ source_slug: string;
683
+ updated_at: string;
684
+ } | null;
685
+ fallback_birth_date: {
686
+ value: string;
687
+ source_slug: string;
688
+ updated_at: string;
689
+ } | null;
690
+ ingestion_start: string | null;
691
+ ingestion_end: string | null;
692
+ };
693
+ type OrderEventRecord = {
694
+ id: number;
695
+ created_at: string;
696
+ status: string;
697
+ status_detail: null;
698
+ };
699
+ type OrderTransactionEmbed = {
700
+ id: string;
701
+ status: "active" | "completed" | "cancelled";
702
+ orders: Array<{
703
+ id: string;
704
+ low_level_status: string;
705
+ low_level_status_created_at: string;
706
+ origin: string;
707
+ parent_id: null;
708
+ created_at: string;
709
+ updated_at: string;
710
+ }>;
711
+ };
712
+ type OrderRecord = {
713
+ id: string;
714
+ user_id: string;
715
+ team_id: string;
716
+ patient_details: Record<string, unknown>;
717
+ patient_address: Record<string, unknown>;
718
+ lab_test: LabTestRecord;
719
+ details: Record<string, unknown>;
720
+ sample_id: string | null;
721
+ notes: null;
722
+ clinical_notes: string | null;
723
+ passthrough: string | null;
724
+ /** Echoed from the request's opaque provider account id; absent when the order had none. */
725
+ lab_account_id?: string;
726
+ created_at: string;
727
+ updated_at: string;
728
+ events: OrderEventRecord[];
729
+ status: string;
730
+ last_event: OrderEventRecord;
731
+ physician: {
732
+ first_name: string;
733
+ last_name: string;
734
+ npi: string;
735
+ };
736
+ health_insurance_id: null;
737
+ requisition_form_url: string | null;
738
+ shipping_details: null;
739
+ has_abn: boolean;
740
+ billing_type: string;
741
+ priority: boolean;
742
+ activate_by: null;
743
+ icd_codes: string[] | null;
744
+ interpretation: string | null;
745
+ has_missing_results: boolean | null;
746
+ result_types: string[] | null;
747
+ expected_result_by_date: string | null;
748
+ worst_case_result_by_date: string | null;
749
+ origin: string;
750
+ order_transaction: OrderTransactionEmbed;
751
+ };
752
+ /**
753
+ * A lab account as the ordering rules read it. Structurally `LabAccountRecord` from
754
+ * lab-accounts.ts, restated here because that module imports this one.
755
+ */
756
+ type StoredLabAccount = {
757
+ id: string;
758
+ lab: string;
759
+ org_id: string | null;
760
+ status: "active" | "pending" | "suspended" | "ready_to_launch";
761
+ delegated_flow: "order_delegated" | "result_delegated" | "fully_delegated" | "not_delegated";
762
+ provider_account_id: string;
763
+ account_name: string | null;
764
+ default_clinical_notes: string | null;
765
+ business_units: string[] | null;
766
+ allowed_billing: Record<string, readonly string[]>;
767
+ team_id_allowlist: readonly string[];
768
+ };
769
+ /**
770
+ * A chosen result for one order, installed through the admin API. Replaces the
771
+ * generated biomarker lines, so a suite can assert on an exact payload — an abnormal
772
+ * panel, a critical value, a missing marker — instead of whatever the generator made.
773
+ */
774
+ type ResultFixture = {
775
+ /** The fixture's name, for logs and `GET /__admin/results`. */
776
+ name: string;
777
+ results?: unknown[];
778
+ missing_results?: unknown[] | null;
779
+ interpretation?: string;
780
+ /** Served from `GET /v3/order/{id}/result/pdf` instead of the generated PDF. */
781
+ pdf_base64?: string;
782
+ };
783
+ /**
784
+ * How serviceability reads answer a ZIP the loaded corpus has no record for.
785
+ *
786
+ * - `synthetic`: invent plausible coverage. Right for property tests that walk random ZIPs.
787
+ * - `corpus`: refuse with a documented Mockingbird error. Right for a mock standing in for
788
+ * the vendor, where invented coverage would silently disagree with production.
789
+ */
790
+ type GeoMode = "synthetic" | "corpus";
791
+ type WebhookPublisher = (event: JunctionWebhookEvent) => void;
792
+ type JunctionWebhookOptions = {
793
+ seed?: number;
794
+ jitterRatio?: number;
795
+ timeoutMs?: number;
796
+ };
797
+
798
+ /**
799
+ * The test backdoor: users and orders put straight into a namespace, with ids a suite
800
+ * chooses — the Junction `user_id`s a database fixture already carries, or an order
801
+ * already in some status — plus the lenient identity mode that adopts unknown users.
802
+ *
803
+ * Everything inserted here goes through the records `create_user` and `create_order`
804
+ * build, so no read path can tell a fixture from an API-created resource.
805
+ */
806
+
807
+ /** How requests referring to a user the namespace does not hold are answered. */
808
+ type IdentityMode = "strict" | "adopt-users";
809
+ type UserFixture = {
810
+ /** Default: a fresh id, as `create_user` would mint. */
811
+ user_id?: string;
812
+ client_user_id: string;
813
+ /** ISO-8601. Default: the mock clock's now. */
814
+ created_on?: string;
815
+ /** An IANA zone, or the full `{ id, source_slug, updated_at }` object. */
816
+ fallback_time_zone?: string | UserRecord["fallback_time_zone"];
817
+ /** `YYYY-MM-DD`, or the full `{ value, source_slug, updated_at }` object. */
818
+ fallback_birth_date?: string | UserRecord["fallback_birth_date"];
819
+ ingestion_start?: string | null;
820
+ ingestion_end?: string | null;
821
+ };
822
+ type OrderFixture = {
823
+ /** Default: a fresh id, as `create_order` would mint. */
824
+ order_id?: string;
825
+ user_id: string;
826
+ lab_test_id: string;
827
+ lab_account_id?: string;
828
+ /** A transition target (`completed`, `at_lab`, or a full status). Default: just ordered. */
829
+ status?: string;
830
+ /** Default: the lab test's own method. */
831
+ collection_method?: string;
832
+ patient_details?: Record<string, unknown>;
833
+ patient_address?: Record<string, unknown>;
834
+ billing_type?: string;
835
+ icd_codes?: string[] | null;
836
+ passthrough?: string | null;
837
+ clinical_notes?: string | null;
838
+ /** ISO-8601. Default: the mock clock's now. */
839
+ created_at?: string;
840
+ /** Results served for the order, as `PUT /__admin/results/{id}` takes them. */
841
+ result_fixture?: Omit<ResultFixture, "name"> & {
842
+ name?: string;
843
+ };
844
+ };
845
+ type JunctionFixtures = {
846
+ users?: readonly UserFixture[];
847
+ orders?: readonly OrderFixture[];
848
+ };
849
+ type InsertOptions = {
850
+ /** Publish the webhooks the equivalent API calls would. Default `false`. */
851
+ emitWebhooks?: boolean;
852
+ };
853
+
854
+ /**
855
+ * Anything that can answer a Fetch `Request` with a `Response`.
856
+ *
857
+ * Every Mockingbird service implements this, and every runtime adapter consumes it.
858
+ * It is the only contract shared across the whole graph.
859
+ */
860
+ interface FetchAPI {
861
+ fetch(request: Request): Promise<Response>;
862
+ }
863
+
864
+ type LabAccountStatus = "active" | "pending" | "suspended" | "ready_to_launch";
865
+ type LabAccountDelegatedFlow = "order_delegated" | "result_delegated" | "fully_delegated" | "not_delegated";
866
+ /**
867
+ * A lab account as a consumer configures it. Mirrors `ClientFacingLabAccount`, with
868
+ * every field but `id` and `lab` optional, plus a `states` shorthand for the common
869
+ * case of an account that client-bills in a fixed set of states.
870
+ */
871
+ type LabAccountInput = {
872
+ id: string;
873
+ /** Lab slug, e.g. `"quest"`, `"labcorp"`, `"bioreference"`. */
874
+ lab: string;
875
+ status?: LabAccountStatus;
876
+ delegated_flow?: LabAccountDelegatedFlow;
877
+ account_name?: string | null;
878
+ provider_account_id?: string;
879
+ org_id?: string | null;
880
+ business_units?: string[] | null;
881
+ default_clinical_notes?: string | null;
882
+ /** Billing type → states it is allowed in. Takes precedence over `states`. */
883
+ allowed_billing?: Record<string, readonly string[]>;
884
+ /** Shorthand for `allowed_billing: { client_bill: states }`. Default: every state. */
885
+ states?: readonly string[];
886
+ /** Teams the account is linked to. Default: the configured team, so it is selectable. */
887
+ team_id_allowlist?: readonly string[];
888
+ };
889
+
890
+ /**
891
+ * Named lab accounts a suite can add in one call, modelled on a real team's recorded
892
+ * listing (a BioReference-linked team, recorded 2026-09-21 from the Junction sandbox).
893
+ *
894
+ * Where a preset copies a recorded account, its states and billing types are the
895
+ * recording's. `bioreference_ny_nj_delegated` is not a recorded account: it is the
896
+ * NY/NJ client-bill shape a consumer asked for, kept because suites depend on it.
897
+ */
898
+
899
+ /**
900
+ * A lab-account layout as a file or option declares it: a bare list of accounts, or
901
+ * presets (by name, or `{ name, id }`) plus accounts, added in that order.
902
+ */
903
+ type LabAccountLayout = readonly LabAccountInput[] | {
904
+ presets?: readonly (string | {
905
+ name: string;
906
+ id?: string;
907
+ })[];
908
+ accounts?: readonly LabAccountInput[];
909
+ };
910
+
911
+ /**
912
+ * Version of the sealed-corpus on-disk format. Bumped whenever the shape changes so a
913
+ * stale recording is rejected instead of half-applied.
914
+ *
915
+ * - 1: observations, catalog and lab accounts.
916
+ * - 2: adds `teamId`, the recording team, so lab-account allowlists load verbatim.
917
+ */
918
+ declare const SEALED_CORPUS_VERSION = 2;
919
+ /**
920
+ * Exact sandbox recording of the provider-owned reads a drop-in consumer depends on:
921
+ * parameter-stable GET observations (area/PSC inventory), the lab catalog, team labs, and
922
+ * lab accounts. Availability is intentionally absent — its slot dates and single-use
923
+ * `booking_key`s can never match a live cache key, so it stays with the generator.
924
+ */
925
+ type SealedCorpus = {
926
+ version: 1 | typeof SEALED_CORPUS_VERSION;
927
+ recordedAt: string;
928
+ source: string;
929
+ /**
930
+ * The recording team's id (version 2+). When present it is the mock's default team,
931
+ * and each lab account's `team_id_allowlist` is kept exactly as recorded.
932
+ */
933
+ teamId?: string;
934
+ /** SHA-256 of the recording's content, written by `corpus pull`. Identifies it in logs. */
935
+ fingerprint?: string;
936
+ observations: Record<string, GetCacheEntry>;
937
+ catalog: {
938
+ labTests: LabTestRecord[];
939
+ labs: Record<string, unknown>[];
940
+ expectedResults: Record<string, ExpectedResult[]>;
941
+ };
942
+ labAccounts: Record<string, unknown>[];
943
+ };
944
+
945
+ type SeedSource = {
946
+ fetch: (request: Request) => Promise<Response>;
947
+ baseUrl: string;
948
+ headers: Record<string, string>;
949
+ };
950
+ type SeedObservations = {
951
+ getCache?: ReadonlyMap<string, GetCacheEntry>;
952
+ };
953
+ type SeedReport = {
954
+ users: number;
955
+ orders: number;
956
+ appointments: number;
957
+ catalogTests: number;
958
+ labs: number;
959
+ cacheEntries: number;
960
+ };
961
+
962
+ /**
963
+ * Signed webhook delivery, the way Junction delivers through Svix.
964
+ *
965
+ * Signing lives here, next to the events, so a consumer never hand-rolls it. The
966
+ * signature is standard Svix — `v1,` + base64 HMAC-SHA256, keyed by the base64 part of
967
+ * `whsec_…`, over `"<svix-id>.<svix-timestamp>.<body>"` — so the official `svix`
968
+ * verifier, and a receiver written for real Junction webhooks, accept it unchanged.
969
+ */
970
+ type WebhookEndpoint = {
971
+ url: string;
972
+ /** Svix signing secret, `whsec_<base64>`. A bare base64 secret is accepted too. */
973
+ secret: string;
974
+ /**
975
+ * Delay before each attempt, in ms; the first entry delays the first attempt. Default:
976
+ * Svix's schedule (immediately, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h).
977
+ */
978
+ retryDelaysMs?: readonly number[];
979
+ /** Abort an attempt that takes longer than this. Default 15 s, like Svix. */
980
+ timeoutMs?: number;
981
+ /** Added to every delivery, e.g. a routing scope for a shared receiver. */
982
+ headers?: Record<string, string>;
983
+ fetch?: (request: Request) => Promise<Response>;
984
+ };
985
+ type WebhookAttempt = {
986
+ attempt: number;
987
+ /** Wall-clock time of the attempt, ISO-8601. */
988
+ at: string;
989
+ /** Receiver's status code, or `null` when the request never completed. */
990
+ status: number | null;
991
+ error: string | null;
992
+ durationMs: number;
993
+ };
994
+ type WebhookDelivery = {
995
+ messageId: string;
996
+ namespace: string;
997
+ event: JunctionWebhookEvent;
998
+ state: "pending" | "delivered" | "failed";
999
+ attempts: WebhookAttempt[];
1000
+ };
1001
+ type WebhookDispatcher = {
1002
+ /** Queue an event for delivery. Returns immediately; delivery runs in the background. */
1003
+ publish(event: JunctionWebhookEvent, namespace: string): WebhookDelivery;
1004
+ deliveries(namespace?: string): WebhookDelivery[];
1005
+ /** Deliver a message again now, as a new attempt, whatever its state. */
1006
+ replay(messageId: string): Promise<WebhookDelivery | undefined>;
1007
+ /** Run every pending retry now instead of waiting out its delay. */
1008
+ flush(): Promise<void>;
1009
+ /** Resolve once no delivery is in flight. */
1010
+ idle(): Promise<void>;
1011
+ /** Cancel pending retries and forget every delivery. */
1012
+ clear(namespace?: string): void;
1013
+ };
1014
+
1015
+ /**
1016
+ * Stateful mock of the Junction (Vital) API user and lab-testing surfaces.
1017
+ *
1018
+ * Docs: https://docs.junction.com/
1019
+ * Auth: `x-vital-api-key` — https://docs.junction.com/api-details/junction-api
1020
+ */
1021
+ type JunctionAPIOptions = APIOptions & {
1022
+ onWebhook?: WebhookPublisher;
1023
+ webhook?: JunctionWebhookOptions;
1024
+ /** Install a recorded corpus at construction; it is re-applied on every `reset()`. */
1025
+ corpus?: SealedCorpus;
1026
+ /**
1027
+ * How serviceability reads answer a ZIP the corpus does not cover. Default: `corpus`
1028
+ * when a corpus is given (refuse, loudly), otherwise `synthetic` (invent coverage).
1029
+ */
1030
+ geo?: GeoMode;
1031
+ /**
1032
+ * The team's lab accounts, replacing the built-in fixtures (and any from the corpus):
1033
+ * a list of accounts, or `{ presets: [...], accounts: [...] }`. `create_order` routes,
1034
+ * accepts and rejects `lab_account_id` against these.
1035
+ */
1036
+ labAccounts?: LabAccountLayout;
1037
+ /**
1038
+ * The team the mock answers as: `team_id` on users and orders, and the team lab-account
1039
+ * allowlists are checked against. Default: the corpus's recorded team, else a fixed id.
1040
+ */
1041
+ teamId?: string;
1042
+ /** Sandbox-only restrictions to enforce. Every one is off unless set here. */
1043
+ limits?: JunctionLimitsInput;
1044
+ /** `adopt-users` creates an unknown (well-formed) `user_id` on first use. Default `strict`. */
1045
+ identity?: IdentityMode;
1046
+ /** Users and orders to load into every namespace, re-applied on every `reset()`. */
1047
+ fixtures?: JunctionFixtures;
1048
+ };
1049
+ /** What a loaded corpus covers, for `/health` and startup logs. */
1050
+ type CorpusInfo = {
1051
+ label: string;
1052
+ recordedAt: string;
1053
+ source: string;
1054
+ observations: number;
1055
+ labTests: number;
1056
+ labAccounts: number;
1057
+ zips: number;
1058
+ /** The recording team, when the corpus recorded one (version 2+). */
1059
+ teamId?: string;
1060
+ };
1061
+ declare class JunctionAPI implements FetchAPI {
1062
+ readonly app: Hono;
1063
+ readonly sqlite: SqliteClient;
1064
+ private readonly service;
1065
+ private readonly state;
1066
+ private readonly now;
1067
+ private readonly explicitTeamId;
1068
+ private readonly fixtures;
1069
+ private readonly rateWindow;
1070
+ private corpus;
1071
+ private labAccountConfig;
1072
+ constructor(options?: JunctionAPIOptions);
1073
+ fetch(request: Request): Promise<Response>;
1074
+ reset(): Promise<void>;
1075
+ /**
1076
+ * Install a sealed sandbox recording: seed the observation cache and replace the catalog,
1077
+ * labs, and lab accounts with the recording's exact values. Re-applied on `reset()`.
1078
+ */
1079
+ installCorpus(corpus: SealedCorpus): void;
1080
+ /**
1081
+ * Which lab accounts the ordering rules see, in order of precedence: the configured
1082
+ * ones, then a corpus's real accounts, then the built-in fixtures (which a corpus
1083
+ * recorded from a team with no accounts leaves in place).
1084
+ */
1085
+ private seedLabAccounts;
1086
+ /** The team this namespace answers as. */
1087
+ get teamId(): string;
1088
+ /**
1089
+ * Replace the team's lab accounts (a list, or `{ presets, accounts }`); `undefined`
1090
+ * restores the default. Kept across `reset()`, per namespace.
1091
+ */
1092
+ configureLabAccounts(accounts: LabAccountLayout | undefined): StoredLabAccount[];
1093
+ labAccounts(): StoredLabAccount[];
1094
+ /** Apply one change to the effective account list and keep it as this namespace's config. */
1095
+ private editLabAccounts;
1096
+ /** Add one account. Throws `LabAccountConflict` when the id is taken. Kept across resets. */
1097
+ addLabAccount(input: LabAccountInput | StoredLabAccount): StoredLabAccount;
1098
+ /** Add a named preset (see `LAB_ACCOUNT_PRESETS`); `undefined` for an unknown name. */
1099
+ addLabAccountPreset(name: string, id?: string): StoredLabAccount | undefined;
1100
+ /** Merge fields into an account; `undefined` when there is none with that id. */
1101
+ patchLabAccount(id: string, patch: Partial<LabAccountInput>): StoredLabAccount | undefined;
1102
+ removeLabAccount(id: string): boolean;
1103
+ /** The sandbox limits this namespace enforces. Every one is off unless configured. */
1104
+ get limits(): JunctionLimits;
1105
+ /** Change some limits (`null` lifts a numeric one); kept across `reset()`. */
1106
+ configureLimits(input: JunctionLimitsInput): JunctionLimits;
1107
+ get identity(): IdentityMode;
1108
+ set identity(mode: IdentityMode);
1109
+ /** Insert users with chosen ids, all or none. Throws `FixtureError` (400/404/409). */
1110
+ insertUsers(users: readonly UserFixture[]): UserRecord[];
1111
+ /** Remove a user outright, leaving no deletion tombstone. */
1112
+ hardDeleteUser(userId: string): boolean;
1113
+ /** Insert orders, optionally already in a status. No webhook fires unless asked. */
1114
+ insertOrders(orders: readonly OrderFixture[], options?: InsertOptions): OrderRecord[];
1115
+ /** Load users, then orders, in one call. */
1116
+ importFixtures(fixtures: JunctionFixtures, options?: InsertOptions): {
1117
+ users: UserRecord[];
1118
+ orders: OrderRecord[];
1119
+ };
1120
+ corpusInfo(): CorpusInfo | undefined;
1121
+ get geoMode(): GeoMode;
1122
+ set geoMode(mode: GeoMode);
1123
+ order(id: string): OrderRecord | undefined;
1124
+ orders(): OrderRecord[];
1125
+ /**
1126
+ * Put an order straight into a status (a full `<status>.<method>.<event>` value) and
1127
+ * publish the webhook a real transition would. `flags` takes the same keys as
1128
+ * `simulate_order`'s body: `interpretation`, `result_types`, `has_missing_results`.
1129
+ */
1130
+ transitionOrder(id: string, status: string, options: {
1131
+ now: () => number;
1132
+ flags?: Record<string, unknown> | null;
1133
+ }): OrderRecord | undefined;
1134
+ /** Serve `fixture` as this order's results, replacing the generated ones. */
1135
+ installResultFixture(orderId: string, fixture: ResultFixture): boolean;
1136
+ resultFixture(orderId: string): ResultFixture | undefined;
1137
+ seedFrom(source: SeedSource, observations?: SeedObservations): Promise<SeedReport>;
1138
+ ensureLabTests(source: SeedSource, ids: readonly string[]): Promise<number>;
1139
+ ensureOrders(source: SeedSource, ids: readonly string[]): Promise<number>;
1140
+ markUserDeleted(userId: string): void;
1141
+ webhookEvents(): JunctionWebhookEvent[];
1142
+ webhookDeliveryAttempts(): WebhookDeliveryAttempt[];
1143
+ }
1144
+
1145
+ type JunctionRuntimeOptions = {
1146
+ /**
1147
+ * Recorded vendor data every namespace starts from. The served mock loads the corpus
1148
+ * shipped at `@crvouga/mockingbird-service-junction/corpus` unless told otherwise.
1149
+ */
1150
+ corpus?: SealedCorpus;
1151
+ /** Default: `corpus` when a corpus is loaded, otherwise `synthetic`. */
1152
+ geo?: GeoMode;
1153
+ /** The team's lab accounts, or `{ presets, accounts }`; see {@link JunctionAPIOptions.labAccounts}. */
1154
+ labAccounts?: LabAccountLayout;
1155
+ /** The team the mock answers as. Default: the corpus's recorded team, else a fixed id. */
1156
+ teamId?: string;
1157
+ /** Sandbox-only restrictions, every one off by default; per namespace via `PUT /__admin/limits`. */
1158
+ limits?: JunctionLimitsInput;
1159
+ /** `adopt-users` creates unknown user ids on first use. Default `strict`. */
1160
+ identity?: IdentityMode;
1161
+ /** Users and orders every namespace starts with, re-applied on each reset. */
1162
+ fixtures?: JunctionFixtures;
1163
+ /** Requests each namespace's journal keeps (`GET /__admin/requests`). Default 1000. */
1164
+ journalSize?: number;
1165
+ /** Deliver signed webhooks here, with Svix's retry schedule. */
1166
+ webhooks?: WebhookEndpoint;
1167
+ /** Called in-process for every webhook event, delivered or not. */
1168
+ onWebhook?: (event: Parameters<WebhookPublisher>[0], namespace: string) => void;
1169
+ sqlite?: SqliteClient;
1170
+ clock?: Clock;
1171
+ /** Seeds fault rates and webhook retry jitter. */
1172
+ seed?: number | string;
1173
+ /** Require `x-mockingbird-admin-key` on `/__admin/*`. */
1174
+ adminKey?: string;
1175
+ onLog?: (entry: RequestLog) => void;
1176
+ };
1177
+ type JunctionRuntime = ServiceRuntime<JunctionAPI> & {
1178
+ /** Signed delivery, when `webhooks` was configured. */
1179
+ readonly webhooks: WebhookDispatcher | undefined;
1180
+ };
1181
+
1182
+ /** Port `mockingbird-junction serve` listens on when none is given. */
1183
+ declare const DEFAULT_PORT = 8787;
1184
+ /** A corpus to serve: the shipped one, none (synthetic), a file path, or a loaded corpus. */
1185
+ type CorpusSource = "default" | "none" | (string & {}) | SealedCorpus;
1186
+ type JunctionServerOptions = Omit<JunctionRuntimeOptions, "corpus"> & {
1187
+ /** Default `0`: the OS picks a free port (read it from `url` / `port`). */
1188
+ port?: number;
1189
+ /** Default `127.0.0.1`. */
1190
+ host?: string;
1191
+ /** Default `"default"`: the corpus shipped with this package. */
1192
+ corpus?: CorpusSource;
1193
+ };
1194
+ type JunctionServer = Listening & {
1195
+ runtime: JunctionRuntime;
1196
+ };
1197
+ /** Resolve a {@link CorpusSource} to a corpus, or `undefined` for synthetic data. */
1198
+ declare const loadCorpus: (source: CorpusSource) => Promise<SealedCorpus | undefined>;
1199
+ /**
1200
+ * Serve the Junction mock over `node:http`. Resolves once it is listening.
1201
+ *
1202
+ * const server = await createServer()
1203
+ * // point the Vital SDK at server.url; await server.close() when done
1204
+ */
1205
+ declare const createServer: (options?: JunctionServerOptions) => Promise<JunctionServer>;
1206
+ /** How `serve` (and `serve --config`) builds the Junction mock from flags. */
1207
+ declare const serveTarget: ServeTarget;
1208
+
1209
+ export { DEFAULT_PORT, createServer, loadCorpus, serveTarget };
1210
+ export type { CorpusSource, JunctionServer, JunctionServerOptions };