@crvouga/mockingbird-service-rxvortex 3.3.0 → 3.4.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.
@@ -1,1022 +0,0 @@
1
- import {
2
- createClock,
3
- createRuntime,
4
- normalizeBootControls,
5
- parseCatalog,
6
- parseDuration,
7
- parseFaultHeader,
8
- resolveAdminPrefix,
9
- restoreClock
10
- } from "./chunk-U2SZUONL.js";
11
-
12
- // src/server.ts
13
- import { readFileSync } from "node:fs";
14
-
15
- // ../../adapters/node/dist/cli.js
16
- import { readFile as readFile2 } from "node:fs/promises";
17
- import { createRequire } from "node:module";
18
- import { resolve } from "node:path";
19
- import { pathToFileURL } from "node:url";
20
- import { parseArgs } from "node:util";
21
-
22
- // ../../adapters/node/dist/fleet.js
23
- import { randomUUID } from "node:crypto";
24
- import { chmod, readFile, rename, rm, writeFile } from "node:fs/promises";
25
-
26
- // ../../adapters/node/dist/serve.js
27
- import { createServer } from "node:http";
28
- var serve = async (api, options = {}) => {
29
- const server = createServer(async (req, res) => {
30
- const chunks = [];
31
- for await (const chunk of req) {
32
- chunks.push(Buffer.from(chunk));
33
- }
34
- const body = Buffer.concat(chunks);
35
- const address = server.address();
36
- const port = typeof address === "object" && address !== null ? address.port : void 0;
37
- const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`;
38
- const raw = req.url ?? "/";
39
- const url = new URL(raw.replace(/^\/+/, "/"), base);
40
- const method = req.method ?? "GET";
41
- const init = { method, headers: req.headers };
42
- if (method !== "GET" && method !== "HEAD" && body.length > 0) {
43
- init.body = body;
44
- }
45
- const aborted = new AbortController();
46
- res.once("close", () => {
47
- if (!res.writableFinished)
48
- aborted.abort();
49
- });
50
- init.signal = aborted.signal;
51
- const request = new Request(url, init);
52
- let response;
53
- try {
54
- response = await api.fetch(request);
55
- } catch (error) {
56
- if (error.code === "MOCKINGBIRD_DROP") {
57
- req.socket.destroy();
58
- return;
59
- }
60
- if (aborted.signal.aborted)
61
- return;
62
- res.writeHead(500, { "content-type": "application/json" });
63
- res.end(JSON.stringify({
64
- error: {
65
- type: "mockingbird_internal",
66
- message: error instanceof Error ? error.message : String(error)
67
- }
68
- }));
69
- return;
70
- }
71
- const headers = Object.fromEntries(response.headers);
72
- const cookies = response.headers.getSetCookie();
73
- if (cookies.length > 0)
74
- headers["set-cookie"] = cookies;
75
- if (!response.body) {
76
- res.writeHead(response.status, headers);
77
- res.end();
78
- return;
79
- }
80
- res.writeHead(response.status, headers);
81
- res.flushHeaders();
82
- const reader = response.body.getReader();
83
- try {
84
- for (; ; ) {
85
- const { done, value } = await reader.read();
86
- if (done)
87
- break;
88
- if (!res.write(value))
89
- await new Promise((resolve2) => res.once("drain", resolve2));
90
- }
91
- res.end();
92
- } catch {
93
- await new Promise((resolve2) => setImmediate(resolve2));
94
- res.destroy();
95
- } finally {
96
- reader.releaseLock();
97
- }
98
- });
99
- await new Promise((resolve2, reject) => {
100
- server.once("error", reject);
101
- server.listen(options.port ?? 0, options.host, resolve2);
102
- });
103
- return server;
104
- };
105
-
106
- // ../../adapters/node/dist/listen.js
107
- var listen = async (api, options = {}) => {
108
- const host = options.host ?? "127.0.0.1";
109
- const server = await serve(api, { port: options.port ?? 0, host });
110
- const address = server.address();
111
- const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
112
- const shown = host.includes(":") ? `[${host}]` : host;
113
- return {
114
- url: `http://${shown}:${port}`,
115
- port,
116
- host,
117
- server,
118
- close: () => new Promise((resolve2, reject) => {
119
- server.close((error) => {
120
- if (error && error.code !== "ERR_SERVER_NOT_RUNNING")
121
- reject(error);
122
- else
123
- resolve2();
124
- });
125
- server.closeAllConnections?.();
126
- })
127
- };
128
- };
129
-
130
- // ../../adapters/node/dist/fleet.js
131
- var EMPTY = {
132
- activeRequests: 0,
133
- activeConnections: 0,
134
- blockedCommands: 0,
135
- pendingJobs: 0,
136
- pendingWebhooks: 0,
137
- unmatchedRequests: 0
138
- };
139
- var NS = /^[A-Za-z0-9_.-]{1,64}$/;
140
- var json = (status, value) => Response.json(value, { status });
141
- var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
142
- var pending = (d) => d.activeRequests + d.blockedCommands + d.pendingJobs + d.pendingWebhooks;
143
- var publicUrl = (raw) => {
144
- const url = new URL(raw);
145
- url.password = "";
146
- return url.href;
147
- };
148
- var atomicWrite = async (path, value) => {
149
- const temp = `${path}.${randomUUID()}.tmp`;
150
- try {
151
- await writeFile(temp, `${JSON.stringify(value, null, 2)}
152
- `, { mode: 384, flag: "wx" });
153
- await chmod(temp, 384);
154
- await rename(temp, path);
155
- } finally {
156
- await rm(temp, { force: true });
157
- }
158
- };
159
- var removeOwned = async (path, id) => {
160
- if (!path)
161
- return;
162
- try {
163
- const value = JSON.parse(await readFile(path, "utf8"));
164
- if (isObject(value) && value.id === id)
165
- await rm(path, { force: true });
166
- } catch (error) {
167
- if (error.code !== "ENOENT")
168
- throw error;
169
- }
170
- };
171
- var httpChild = async (target, entry, options) => {
172
- const runtime = await target.create(entry.options ?? {}, {
173
- adminPrefix: resolveAdminPrefix(entry.adminPrefix),
174
- adminKey: entry.adminKey,
175
- seed: entry.seed,
176
- onLog: options.onLog,
177
- ...entry.adminControls !== void 0 ? { adminControls: entry.adminControls } : {},
178
- ...entry.fixtures !== void 0 ? { fixtures: entry.fixtures } : {},
179
- ...entry.baseUrl !== void 0 ? { baseUrl: entry.baseUrl } : {}
180
- });
181
- const adminPrefix = resolveAdminPrefix(entry.adminPrefix);
182
- const locked = /* @__PURE__ */ new Set();
183
- const active = /* @__PURE__ */ new Map();
184
- const api = {
185
- async fetch(request) {
186
- const namespace = runtime.namespaceOf(request);
187
- if (locked.has(namespace))
188
- return json(409, { error: "fleet control in progress", namespace });
189
- active.set(namespace, (active.get(namespace) ?? 0) + 1);
190
- try {
191
- return await runtime.fetch(request);
192
- } finally {
193
- active.set(namespace, (active.get(namespace) ?? 1) - 1);
194
- }
195
- }
196
- };
197
- let server;
198
- try {
199
- server = await listen(api, {
200
- port: entry.port ?? target.defaultPort,
201
- host: entry.host ?? "127.0.0.1"
202
- });
203
- target.listening?.(server);
204
- } catch (error) {
205
- runtime.stop?.();
206
- await server?.close();
207
- runtime.webhooks?.clear();
208
- await runtime.webhooks?.idle();
209
- throw error;
210
- }
211
- return {
212
- protocol: "http",
213
- url: server.url,
214
- healthUrl: `${server.url}${adminPrefix}/health`,
215
- adminUrl: `${server.url}${adminPrefix}`,
216
- namespaces: { header: "x-mockingbird-namespace", path: `${adminPrefix}/ns/{name}` },
217
- ready: async () => {
218
- try {
219
- return (await fetch(`${server.url}${adminPrefix}/health`, { signal: AbortSignal.timeout(2e3) })).ok;
220
- } catch {
221
- return false;
222
- }
223
- },
224
- async close() {
225
- runtime.stop?.();
226
- runtime.webhooks?.clear();
227
- await server.close();
228
- await runtime.webhooks?.idle();
229
- },
230
- lock(namespace) {
231
- if (locked.has(namespace))
232
- throw new Error("namespace already locked");
233
- locked.add(namespace);
234
- return () => locked.delete(namespace);
235
- },
236
- diagnostics(namespace) {
237
- return {
238
- ...EMPTY,
239
- activeRequests: active.get(namespace) ?? 0,
240
- pendingWebhooks: runtime.webhooks?.pending(namespace) ?? 0,
241
- unmatchedRequests: runtime.metrics.report(namespace).unmatched.reduce((n, r) => n + r.count, 0)
242
- };
243
- },
244
- async checkpoint(namespace) {
245
- return runtime.fleetSnapshot(namespace);
246
- },
247
- async restore(namespace, checkpoint) {
248
- runtime.fleetRestore(checkpoint, namespace);
249
- },
250
- reset: (namespace) => runtime.reset(namespace),
251
- clock: (namespace) => runtime.namespaceClock(namespace),
252
- async settle(namespace) {
253
- const instance = runtime.instance(namespace);
254
- instance.tick?.();
255
- await runtime.webhooks?.idle();
256
- }
257
- };
258
- };
259
- async function startFleet(config, options) {
260
- const id = randomUUID();
261
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
262
- const children = /* @__PURE__ */ new Map();
263
- const clocks = /* @__PURE__ */ new Map();
264
- const checkpoints = /* @__PURE__ */ new Map();
265
- let supervisor;
266
- let stopped = false;
267
- let controlling = false;
268
- const close = async () => {
269
- if (stopped)
270
- return;
271
- stopped = true;
272
- await Promise.allSettled([...children.values()].map((child) => child.close()));
273
- await supervisor?.close();
274
- await removeOwned(options.readyFile, id);
275
- await removeOwned(options.connectionsFile, id);
276
- };
277
- if (options.readyFile)
278
- await rm(options.readyFile, { force: true });
279
- if (options.connectionsFile)
280
- await rm(options.connectionsFile, { force: true });
281
- const clock = (namespace) => {
282
- let value = clocks.get(namespace);
283
- if (!value) {
284
- value = createClock();
285
- clocks.set(namespace, value);
286
- }
287
- return value;
288
- };
289
- const select = (body) => {
290
- if (body.services === void 0)
291
- return children;
292
- if (!Array.isArray(body.services) || body.services.length === 0)
293
- throw new Error("services must be a nonempty list");
294
- const selected = /* @__PURE__ */ new Map();
295
- for (const name of body.services) {
296
- if (typeof name !== "string" || !children.has(name))
297
- throw new Error("unknown service in selection");
298
- selected.set(name, children.get(name));
299
- }
300
- return selected;
301
- };
302
- const diagnostics = (namespace) => Object.fromEntries([...children].map(([name, child]) => [name, child.diagnostics(namespace)]));
303
- const transaction = async (namespace, selected, run) => {
304
- if (controlling)
305
- return json(409, { error: "another fleet operation is in progress" });
306
- controlling = true;
307
- const release = [];
308
- const before = /* @__PURE__ */ new Map();
309
- const results = {};
310
- let failure;
311
- try {
312
- for (const [name, child] of selected) {
313
- failure = name;
314
- release.push(child.lock(namespace));
315
- }
316
- const busy = Object.fromEntries([...selected].filter(([, child]) => {
317
- const state = child.diagnostics(namespace);
318
- return state.activeRequests + state.blockedCommands + state.pendingWebhooks + (state.activeJobs ?? 0) > 0;
319
- }).map(([name, child]) => [name, child.diagnostics(namespace)]));
320
- if (Object.keys(busy).length)
321
- return json(409, { error: "namespace is not quiescent", services: busy });
322
- for (const [name, child] of selected) {
323
- failure = name;
324
- before.set(name, await child.checkpoint(namespace));
325
- }
326
- for (const [name, child] of selected) {
327
- failure = name;
328
- results[name] = { status: "ok", result: await run(name, child) };
329
- }
330
- return json(200, { status: "ok", namespace, services: results });
331
- } catch {
332
- const rollback = {};
333
- for (const [name, point] of before) {
334
- try {
335
- await selected.get(name)?.restore(namespace, point);
336
- rollback[name] = "restored";
337
- } catch {
338
- rollback[name] = "failed";
339
- }
340
- }
341
- return json(500, {
342
- status: "failed",
343
- namespace,
344
- failedService: failure,
345
- services: results,
346
- rollback,
347
- rolledBack: before.size > 0 && Object.values(rollback).every((value) => value === "restored")
348
- });
349
- } finally {
350
- for (const unlock of release.reverse())
351
- unlock();
352
- controlling = false;
353
- }
354
- };
355
- const api = {
356
- async fetch(request) {
357
- const url = new URL(request.url);
358
- const adminPrefix = resolveAdminPrefix(config.adminPrefix);
359
- const path = url.pathname.startsWith(`${adminPrefix}/`) ? url.pathname.slice(adminPrefix.length) : "";
360
- if (path === "/health" && request.method === "GET") {
361
- const states = await Promise.all([...children].map(async ([name, child]) => {
362
- let protocolReady = false;
363
- try {
364
- protocolReady = await child.ready();
365
- } catch {
366
- }
367
- return [
368
- name,
369
- {
370
- processReady: !stopped,
371
- protocolReady,
372
- status: protocolReady ? "ready" : "failed",
373
- url: publicUrl(child.url)
374
- }
375
- ];
376
- }));
377
- const ready = !stopped && states.every(([, state]) => state.protocolReady);
378
- return json(ready ? 200 : 503, {
379
- status: ready ? "ready" : "failed",
380
- id,
381
- services: Object.fromEntries(states)
382
- });
383
- }
384
- if (!path.startsWith("/fleet/"))
385
- return json(404, { error: "not found" });
386
- const adminKey = config.adminKey ?? process.env.MOCKINGBIRD_ADMIN_KEY;
387
- if (adminKey !== void 0 && request.headers.get("x-mockingbird-admin-key") !== adminKey)
388
- return json(401, { error: "admin key required" });
389
- const route = /^\/fleet\/namespaces\/([^/]+)\/(reset|snapshots)(?:\/([^/]+)\/restore)?$/.exec(path);
390
- const namespace = route ? decodeURIComponent(route[1]) : url.searchParams.get("namespace") ?? request.headers.get("x-mockingbird-namespace") ?? config.namespace ?? "default";
391
- if (!NS.test(namespace))
392
- return json(400, { error: "invalid namespace" });
393
- if (request.method === "GET" && path === "/fleet/metrics")
394
- return json(200, { namespace, services: diagnostics(namespace) });
395
- if (request.method === "GET" && path === "/fleet/clock")
396
- return json(200, {
397
- namespace,
398
- ...clock(namespace).state(),
399
- services: Object.fromEntries([...children].map(([name, child]) => [name, child.clock(namespace).state()]))
400
- });
401
- if (request.method !== "POST")
402
- return json(405, { error: "method not allowed" });
403
- let body;
404
- try {
405
- const text2 = await request.text();
406
- const value = text2 ? JSON.parse(text2) : {};
407
- if (!isObject(value))
408
- throw new Error("expected object");
409
- body = value;
410
- } catch {
411
- return json(400, { error: "expected JSON object" });
412
- }
413
- let selected;
414
- try {
415
- selected = select(body);
416
- } catch (error) {
417
- return json(400, { error: error instanceof Error ? error.message : "bad selection" });
418
- }
419
- if (path === "/fleet/wait-until-idle") {
420
- const services = diagnostics(namespace);
421
- const idle = Object.values(services).every((d) => pending(d) === 0);
422
- return json(idle ? 200 : 409, { status: idle ? "idle" : "busy", namespace, services });
423
- }
424
- if (path === "/fleet/clock") {
425
- const current = clock(namespace);
426
- let target = current.now();
427
- if (body.set !== void 0) {
428
- target = typeof body.set === "number" ? body.set : typeof body.set === "string" ? Date.parse(body.set) : NaN;
429
- if (!Number.isFinite(target))
430
- return json(400, { error: "set must be epoch ms or ISO-8601" });
431
- }
432
- if (body.advance !== void 0) {
433
- const delta = parseDuration(body.advance);
434
- if (delta === void 0)
435
- return json(400, { error: "invalid advance" });
436
- target += delta;
437
- }
438
- const frozen = body.freeze === void 0 ? current.state().frozen : body.freeze;
439
- if (typeof frozen !== "boolean")
440
- return json(400, { error: "freeze must be boolean" });
441
- const response2 = await transaction(namespace, selected, async (_name, child) => {
442
- const childClock = child.clock(namespace);
443
- restoreClock(childClock, { now: target, frozen });
444
- await child.settle?.(namespace);
445
- return childClock.state();
446
- });
447
- if (response2.ok)
448
- restoreClock(current, { now: target, frozen });
449
- return response2;
450
- }
451
- if (!route)
452
- return json(404, { error: "not found" });
453
- if (route[2] === "reset") {
454
- if (route[3])
455
- return json(404, { error: "not found" });
456
- const response2 = await transaction(namespace, selected, async (_name, child) => child.reset(namespace));
457
- if (response2.ok && selected.size === children.size)
458
- clock(namespace).reset();
459
- return response2;
460
- }
461
- if (route[3]) {
462
- const point = checkpoints.get(route[3]);
463
- if (!point || point.namespace !== namespace)
464
- return json(404, { error: "no snapshot in this namespace" });
465
- const restored = new Map([...point.values].map(([name]) => [name, children.get(name)]));
466
- const response2 = await transaction(namespace, restored, async (name, child) => child.restore(namespace, point.values.get(name)));
467
- if (response2.ok)
468
- restoreClock(clock(namespace), point.clock);
469
- return response2;
470
- }
471
- const snapshotId = randomUUID();
472
- const values = /* @__PURE__ */ new Map();
473
- const response = await transaction(namespace, selected, async (name, child) => {
474
- values.set(name, await child.checkpoint(namespace));
475
- return { checkpoint: `${snapshotId}:${name}` };
476
- });
477
- if (!response.ok)
478
- return response;
479
- checkpoints.set(snapshotId, { namespace, values, clock: clock(namespace).state() });
480
- return json(201, {
481
- ...await response.json(),
482
- id: snapshotId,
483
- label: body.label
484
- });
485
- }
486
- };
487
- try {
488
- const adminPrefix = resolveAdminPrefix(config.adminPrefix);
489
- if (!isObject(config.services) || Object.keys(config.services).length === 0)
490
- throw new Error("config must contain at least one service");
491
- for (const [name, entry] of Object.entries(config.services)) {
492
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
493
- throw new Error("invalid service name");
494
- if (config.adminControls !== void 0 || entry.adminControls !== void 0)
495
- normalizeBootControls({ ...config.adminControls, ...entry.adminControls }, `services.${name}.adminControls`);
496
- try {
497
- const target = await options.load(name);
498
- if (entry.protocol && entry.protocol !== ("protocol" in target ? target.protocol : "http"))
499
- throw new Error("configured protocol does not match target");
500
- const adminKey = entry.adminKey ?? config.adminKey ?? process.env.MOCKINGBIRD_ADMIN_KEY;
501
- const child = "start" in target ? await target.start(entry) : await httpChild(target, {
502
- ...entry,
503
- adminPrefix: resolveAdminPrefix(entry.adminPrefix ?? adminPrefix),
504
- ...adminKey !== void 0 ? { adminKey } : {},
505
- // Fleet-wide controls, with the service's own keys replacing them.
506
- ...config.adminControls !== void 0 || entry.adminControls !== void 0 ? { adminControls: { ...config.adminControls, ...entry.adminControls } } : {}
507
- }, options);
508
- children.set(name, child);
509
- if (!await child.ready())
510
- throw new Error("protocol readiness failed");
511
- } catch {
512
- throw new Error(`service ${name} failed to start; all started listeners were closed`);
513
- }
514
- }
515
- supervisor = await listen(api, {
516
- port: config.control?.port ?? 0,
517
- host: config.control?.host ?? "127.0.0.1"
518
- });
519
- const manifest = {
520
- version: 1,
521
- state: "ready",
522
- pid: process.pid,
523
- id,
524
- startedAt,
525
- adminBase: `${supervisor.url}${adminPrefix}/fleet`,
526
- healthUrl: `${supervisor.url}${adminPrefix}/health`,
527
- services: Object.fromEntries([...children].map(([name, child]) => [
528
- name,
529
- {
530
- protocol: child.protocol,
531
- url: publicUrl(child.url),
532
- healthUrl: child.healthUrl ?? `${supervisor?.url}${adminPrefix}/health`,
533
- adminUrl: child.adminUrl ?? `${supervisor?.url}${adminPrefix}/fleet`,
534
- namespaces: child.namespaces,
535
- processReady: true,
536
- protocolReady: true
537
- }
538
- ]))
539
- };
540
- if (options.connectionsFile)
541
- await atomicWrite(options.connectionsFile, {
542
- version: 1,
543
- id,
544
- pid: process.pid,
545
- services: Object.fromEntries([...children].map(([name, child]) => [
546
- name,
547
- { url: child.connection ?? child.url, namespaces: child.connections }
548
- ]))
549
- });
550
- if (options.readyFile)
551
- await atomicWrite(options.readyFile, manifest);
552
- options.onReady?.(manifest);
553
- return { manifest, children, fetch: api.fetch, close };
554
- } catch (error) {
555
- await close();
556
- throw error;
557
- }
558
- }
559
-
560
- // ../../adapters/node/dist/cli.js
561
- var optionHelp = (options) => Object.entries(options).map(([name, option]) => {
562
- const flag = `--${name}${option.type === "string" ? ` ${option.value ?? "<value>"}` : ""}`;
563
- const fallback = option.default !== void 0 ? ` (default: ${String(option.default)})` : "";
564
- return ` ${flag.padEnd(30)} ${option.description}${fallback}`;
565
- });
566
- var help = (spec) => [
567
- `${spec.bin} \u2014 ${spec.description}`,
568
- "",
569
- "Usage:",
570
- ` ${spec.bin} <command> [options]`,
571
- "",
572
- "Commands:",
573
- ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),
574
- "",
575
- `Run \`${spec.bin} <command> --help\` for a command's options.`
576
- ].join("\n");
577
- var commandHelp = (spec, name, command) => [
578
- `${spec.bin} ${name} \u2014 ${command.summary}`,
579
- "",
580
- "Usage:",
581
- ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,
582
- ...command.options ? ["", "Options:", ...optionHelp(command.options)] : []
583
- ].join("\n");
584
- var runCli = async (spec, argv) => {
585
- const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : void 0;
586
- const words = pair !== void 0 && spec.commands[pair] ? 2 : 1;
587
- const name = words === 2 ? pair : argv[0];
588
- const rest = argv.slice(words);
589
- if (name === void 0 || name === "--help" || name === "-h" || name === "help") {
590
- console.log(help(spec));
591
- return 0;
592
- }
593
- const command = spec.commands[name];
594
- if (!command) {
595
- console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}
596
-
597
- ${help(spec)}`);
598
- return 2;
599
- }
600
- if (rest.includes("--help") || rest.includes("-h")) {
601
- console.log(commandHelp(spec, name, command));
602
- return 0;
603
- }
604
- let parsed;
605
- try {
606
- parsed = parseArgs({
607
- args: rest,
608
- allowPositionals: true,
609
- strict: true,
610
- options: Object.fromEntries(Object.entries(command.options ?? {}).map(([key, option]) => [
611
- key,
612
- {
613
- type: option.type,
614
- ...option.default !== void 0 ? { default: option.default } : {}
615
- }
616
- ]))
617
- });
618
- } catch (error) {
619
- console.error(`${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}
620
-
621
- ${commandHelp(spec, name, command)}`);
622
- return 2;
623
- }
624
- return command.run(parsed.values, parsed.positionals);
625
- };
626
- var COMMON_SERVE_OPTIONS = {
627
- port: { type: "string", value: "<port>", description: "Port to listen on" },
628
- host: { type: "string", value: "<host>", description: "Interface to bind", default: "127.0.0.1" },
629
- "admin-prefix": {
630
- type: "string",
631
- value: "<path>",
632
- description: "Internal API prefix (default /__admin; env MOCKINGBIRD_ADMIN_PREFIX)"
633
- },
634
- "admin-key": {
635
- type: "string",
636
- value: "<key>",
637
- description: "Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)"
638
- },
639
- seed: {
640
- type: "string",
641
- value: "<seed>",
642
- description: "Seed for every random choice (env MOCKINGBIRD_SEED)"
643
- },
644
- "admin-controls": {
645
- type: "string",
646
- value: "<json|file>",
647
- description: "Admin controls to boot with, as JSON or a JSON file: clock, latency, faults, snapshot, namespaces (env MOCKINGBIRD_ADMIN_CONTROLS)"
648
- },
649
- clock: {
650
- type: "string",
651
- value: "<instant>",
652
- description: "Start the clock at this instant (epoch ms or ISO-8601)"
653
- },
654
- freeze: { type: "boolean", description: "Freeze the clock: it moves only when told to" },
655
- "auto-advance": {
656
- type: "string",
657
- value: "<duration>",
658
- description: "Step the clock this much before every request (e.g. 1s)"
659
- },
660
- latency: {
661
- type: "string",
662
- value: "<duration>",
663
- description: "Baseline response time for every request (e.g. 250ms)"
664
- },
665
- jitter: {
666
- type: "string",
667
- value: "<duration>",
668
- description: "Seeded extra latency between 0 and this"
669
- },
670
- "virtual-time": {
671
- type: "boolean",
672
- description: "Spend latency, delays and timeouts on the mock clock instead of really waiting"
673
- },
674
- fault: {
675
- type: "string",
676
- value: "<fault>",
677
- description: 'A fault to boot with, as in x-mockingbird-fault: 503, rate_limited, "status=503; delay=2s"'
678
- },
679
- snapshot: {
680
- type: "string",
681
- value: "<file>",
682
- description: "Start from a snapshot document (GET /__admin/snapshots/:id)"
683
- },
684
- log: {
685
- type: "string",
686
- value: "<pretty|json|off>",
687
- description: "Request log format",
688
- default: "pretty"
689
- },
690
- "log-requests": {
691
- type: "boolean",
692
- description: "One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json"
693
- },
694
- config: {
695
- type: "string",
696
- value: "<file>",
697
- description: "Serve every service in a mockingbird.json config instead"
698
- },
699
- "ready-file": {
700
- type: "string",
701
- description: "Atomically publish fleet endpoint JSON",
702
- value: "<file>"
703
- },
704
- "connections-file": {
705
- type: "string",
706
- description: "Write private connection URLs with mode 0600",
707
- value: "<file>"
708
- },
709
- "ready-json": {
710
- type: "boolean",
711
- description: "Emit one public JSON record after fleet readiness"
712
- }
713
- };
714
- var formatLog = (format) => {
715
- if (format === "off")
716
- return void 0;
717
- if (format === "json")
718
- return (entry) => console.log(JSON.stringify(entry));
719
- return (entry) => {
720
- const op = entry.operationId ?? (entry.unmatched ? "UNMATCHED" : "-");
721
- const ns = entry.namespace === "default" ? "" : ` [${entry.namespace}]`;
722
- const fault = entry.faultId ? ` fault=${entry.faultId}` : "";
723
- const adopted = entry.adopted ? " adopted" : "";
724
- const received = entry.request ? ` received=${entry.request.bodyBytes === null ? "?" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? "-"}${entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : ""}` : "";
725
- const issues = entry.issues?.length ? ` issues=${entry.issues.slice(0, 3).map((issue) => `${issue.path || "$"}: ${issue.message}`).join("; ")}${entry.issues.length > 3 ? ` (+${entry.issues.length - 3})` : ""}` : "";
726
- console.log(`${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`);
727
- };
728
- };
729
- var asString = (value) => typeof value === "string" ? value : void 0;
730
- var readJsonFile = async (path, label) => {
731
- try {
732
- return JSON.parse(await readFile2(path, "utf8"));
733
- } catch (error) {
734
- throw new Error(`${label}: cannot read ${path}: ${error instanceof Error ? error.message : error}`);
735
- }
736
- };
737
- var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
738
- var resolveServeControls = async (values, env = process.env, base) => {
739
- const source = asString(values["admin-controls"]) ?? env.MOCKINGBIRD_ADMIN_CONTROLS;
740
- let document = isObject2(base) ? { ...base } : {};
741
- if (source !== void 0 && source.trim() !== "") {
742
- let parsed;
743
- if (source.trim().startsWith("{")) {
744
- try {
745
- parsed = JSON.parse(source);
746
- } catch {
747
- throw new Error("--admin-controls: not valid JSON");
748
- }
749
- } else
750
- parsed = await readJsonFile(source, "--admin-controls");
751
- if (!isObject2(parsed))
752
- throw new Error("--admin-controls: expected a JSON object");
753
- document = { ...document, ...parsed };
754
- }
755
- const clock = isObject2(document.clock) ? { ...document.clock } : {};
756
- const now = asString(values.clock);
757
- if (now !== void 0)
758
- clock.now = now;
759
- if (values.freeze === true)
760
- clock.frozen = true;
761
- const autoAdvance = asString(values["auto-advance"]);
762
- if (autoAdvance !== void 0)
763
- clock.autoAdvance = autoAdvance;
764
- if (Object.keys(clock).length > 0)
765
- document.clock = clock;
766
- const delay = asString(values.latency);
767
- const jitter = asString(values.jitter);
768
- if (delay !== void 0 || jitter !== void 0) {
769
- const latency = isObject2(document.latency) ? { ...document.latency } : document.latency !== void 0 ? { delay: document.latency } : {};
770
- if (delay !== void 0)
771
- latency.delay = delay;
772
- if (jitter !== void 0)
773
- latency.jitter = jitter;
774
- document.latency = latency;
775
- }
776
- if (values["virtual-time"] === true)
777
- document.virtualTime = true;
778
- const fault = asString(values.fault);
779
- if (fault !== void 0)
780
- document.faults = [
781
- ...Array.isArray(document.faults) ? document.faults : [],
782
- parseFaultHeader(fault, "--fault")
783
- ];
784
- const snapshot = asString(values.snapshot);
785
- if (snapshot !== void 0)
786
- document.snapshot = await readJsonFile(snapshot, "--snapshot");
787
- if (Object.keys(document).length === 0)
788
- return void 0;
789
- normalizeBootControls(document, "adminControls");
790
- return document;
791
- };
792
- var loadTarget = async (name, own) => {
793
- if (name === own.name)
794
- return own;
795
- const specifier = `@crvouga/mockingbird-service-${name}/server`;
796
- try {
797
- let mod;
798
- try {
799
- mod = await import(specifier);
800
- } catch {
801
- const fromProject = createRequire(resolve(process.cwd(), "package.json"));
802
- mod = await import(pathToFileURL(fromProject.resolve(specifier)).href);
803
- }
804
- if (!mod.serveTarget)
805
- throw new Error(`${specifier} exports no serveTarget`);
806
- return mod.serveTarget;
807
- } catch (error) {
808
- const reason = error instanceof Error ? error.message : String(error);
809
- throw new Error(`cannot load service "${name}": ${reason}. Install @crvouga/mockingbird-service-${name}.`);
810
- }
811
- };
812
- var start = async (target, values, config) => {
813
- const adminPrefix = resolveAdminPrefix(config.adminPrefix);
814
- const runtime = await target.create(values, {
815
- adminPrefix,
816
- adminKey: config.adminKey,
817
- seed: config.seed,
818
- onLog: formatLog(config.log),
819
- ...config.adminControls !== void 0 ? { adminControls: config.adminControls } : {}
820
- });
821
- const listening = await listen(runtime, { port: config.port, host: config.host });
822
- target.listening?.(listening);
823
- console.log(`${target.name} emulator listening on ${listening.url}`);
824
- console.log(`${target.name} health: GET ${listening.url}${adminPrefix}/health`);
825
- console.log(`${target.name} admin: ${listening.url}${adminPrefix} (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
826
- console.log(`${target.name} admin ui: ${listening.url}${adminPrefix}/ui`);
827
- for (const line of target.banner?.(runtime) ?? [])
828
- console.log(`${target.name} ${line.replaceAll("/__admin", adminPrefix)}`);
829
- return listening;
830
- };
831
- var untilSignal = async (servers) => new Promise((resolve2) => {
832
- const stop = async () => {
833
- ;
834
- process.removeListener("SIGINT", stop);
835
- process.removeListener("SIGTERM", stop);
836
- await Promise.allSettled(servers.map((s) => s.close()));
837
- resolve2(0);
838
- };
839
- process.once("SIGINT", stop);
840
- process.once("SIGTERM", stop);
841
- });
842
- var serveCommand = (target) => ({
843
- summary: `Serve the ${target.name} emulator or a configured fleet`,
844
- options: { ...COMMON_SERVE_OPTIONS, ..."options" in target ? target.options : {} },
845
- async run(values) {
846
- const log = values["log-requests"] === true ? "json" : asString(values.log) ?? "pretty";
847
- if (!["pretty", "json", "off"].includes(log)) {
848
- console.error(`--log must be pretty, json or off (got ${log})`);
849
- return 2;
850
- }
851
- const configPath = asString(values.config);
852
- if (configPath !== void 0) {
853
- try {
854
- const config = JSON.parse(await readFile2(configPath, "utf8"));
855
- config.adminPrefix = resolveAdminPrefix(config.adminPrefix ?? asString(values["admin-prefix"]) ?? process.env.MOCKINGBIRD_ADMIN_PREFIX);
856
- const adminKey2 = config.adminKey ?? asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
857
- if (adminKey2 !== void 0)
858
- config.adminKey = adminKey2;
859
- const adminControls = await resolveServeControls(values, process.env, config.adminControls);
860
- if (adminControls !== void 0)
861
- config.adminControls = adminControls;
862
- const fleet = await startFleet(config, {
863
- load: (name) => loadTarget(name, target),
864
- onLog: formatLog(config.log ?? log),
865
- ...asString(values["ready-file"]) ? { readyFile: asString(values["ready-file"]) } : {},
866
- ...asString(values["connections-file"]) ? { connectionsFile: asString(values["connections-file"]) } : {},
867
- onReady: (manifest) => {
868
- if (values["ready-json"])
869
- console.log(JSON.stringify(manifest));
870
- else
871
- for (const [name, endpoint] of Object.entries(manifest.services))
872
- console.log(`${name} emulator listening on ${endpoint.url}`);
873
- }
874
- });
875
- return untilSignal([fleet]);
876
- } catch (error) {
877
- console.error(error instanceof Error ? error.message : String(error));
878
- return 1;
879
- }
880
- }
881
- if ("start" in target) {
882
- console.error("Use --config to supervise protocol services with this command");
883
- return 2;
884
- }
885
- const port = asString(values.port);
886
- const adminKey = asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
887
- const seed = asString(values.seed) ?? process.env.MOCKINGBIRD_SEED;
888
- let listening;
889
- try {
890
- const adminControls = await resolveServeControls(values);
891
- listening = await start(target, values, {
892
- ...adminControls !== void 0 ? { adminControls } : {},
893
- adminPrefix: resolveAdminPrefix(asString(values["admin-prefix"]) ?? process.env.MOCKINGBIRD_ADMIN_PREFIX),
894
- port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
895
- host: asString(values.host) ?? "127.0.0.1",
896
- ...adminKey !== void 0 ? { adminKey } : {},
897
- ...seed !== void 0 ? { seed } : {},
898
- log
899
- });
900
- } catch (error) {
901
- console.error(error instanceof Error ? error.message : String(error));
902
- return 1;
903
- }
904
- return untilSignal([listening]);
905
- }
906
- });
907
-
908
- // src/server.ts
909
- var DEFAULT_PORT = 8791;
910
- var createServer2 = async (options = {}) => {
911
- const { port, host, ...rest } = options;
912
- const runtime = createRuntime({ tickMs: 100, ...rest });
913
- const listening = await listen(runtime, {
914
- port: port ?? 0,
915
- ...host !== void 0 ? { host } : {}
916
- });
917
- return {
918
- ...listening,
919
- runtime,
920
- close: async () => {
921
- runtime.stop();
922
- await listening.close();
923
- }
924
- };
925
- };
926
- var text = (value) => typeof value === "string" ? value : void 0;
927
- var loadCatalogFile = (path) => {
928
- let raw;
929
- try {
930
- raw = readFileSync(path, "utf8");
931
- } catch {
932
- throw new Error(`rxvortex catalog not found: ${path}`);
933
- }
934
- try {
935
- return parseCatalog(JSON.parse(raw));
936
- } catch (error) {
937
- throw new Error(`rxvortex catalog ${path}: ${error.message}`);
938
- }
939
- };
940
- var serveTarget = {
941
- name: "rxvortex",
942
- defaultPort: DEFAULT_PORT,
943
- options: {
944
- "webhook-url": {
945
- type: "string",
946
- value: "<url>",
947
- description: "Deliver status webhooks here (e.g. http://127.0.0.1:3000/prescriptions/webhooks/rxvortex)"
948
- },
949
- "webhook-secret": {
950
- type: "string",
951
- value: "<secret>",
952
- description: "Sent as x-rxvortex-webhook-secret (the app's RXVORTEX_WEBHOOK_SECRET)"
953
- },
954
- "api-token": {
955
- type: "string",
956
- value: "<token>",
957
- description: "A static bearer token to accept (the app's RXVORTEX_API_TOKEN)"
958
- },
959
- catalog: {
960
- type: "string",
961
- value: "<file.json>",
962
- description: "Preset catalog every namespace starts with and resets to: a JSON array, or {data: [...]} as GET /api/v1/preset-catalog-items returns"
963
- },
964
- "unknown-presets": {
965
- type: "string",
966
- value: "<reject|accept>",
967
- description: "An unknown preset_catalog_id: 422 like the sandbox (reject, default) or add it (accept)"
968
- },
969
- "auto-advance": {
970
- type: "string",
971
- value: "<ms:Status,Status,\u2026>",
972
- description: 'Walk every new order along a path, e.g. "2000:Fill,Shipping,Delivered"'
973
- }
974
- },
975
- create: (values, common) => {
976
- const url = text(values["webhook-url"]);
977
- const secret = text(values["webhook-secret"]);
978
- const token = text(values["api-token"]);
979
- const auto = text(values["auto-advance"]);
980
- const catalogPath = text(values.catalog);
981
- const unknownPresets = text(values["unknown-presets"]);
982
- if (unknownPresets !== void 0 && unknownPresets !== "reject" && unknownPresets !== "accept")
983
- throw new Error('--unknown-presets must be "reject" or "accept"');
984
- const plan = auto ? /^(\d+):(.+)$/.exec(auto) : null;
985
- if (auto && !plan)
986
- throw new Error('--auto-advance must look like "2000:Fill,Shipping,Delivered"');
987
- return createRuntime({
988
- tickMs: 100,
989
- ...url ? { webhooks: { url, ...secret ? { secret } : {} } } : {},
990
- ...catalogPath ? { catalog: loadCatalogFile(catalogPath) } : {},
991
- settings: {
992
- ...unknownPresets ? { unknownPresets } : {},
993
- ...token ? { staticTokens: [token] } : {},
994
- ...plan ? {
995
- autoAdvance: {
996
- afterMs: Number(plan[1]),
997
- path: plan[2].split(",").map((s) => s.trim())
998
- }
999
- } : {}
1000
- },
1001
- ...common.adminPrefix !== void 0 ? { adminPrefix: common.adminPrefix } : {},
1002
- ...common.adminKey !== void 0 ? { adminKey: common.adminKey } : {},
1003
- ...common.adminControls !== void 0 ? { adminControls: common.adminControls } : {},
1004
- ...common.seed !== void 0 ? { seed: common.seed } : {},
1005
- ...common.onLog ? { onLog: common.onLog } : {}
1006
- });
1007
- },
1008
- banner: () => [
1009
- "auth: POST /api/v1/generate-access-token {client_id, client_secret}, then Authorization: Bearer <token>",
1010
- "namespaces: x-mockingbird-namespace, /__admin/ns/<name>/\u2026, or PUT /__admin/credentials {<client_id>: <ns>}"
1011
- ]
1012
- };
1013
-
1014
- export {
1015
- runCli,
1016
- serveCommand,
1017
- DEFAULT_PORT,
1018
- createServer2 as createServer,
1019
- loadCatalogFile,
1020
- serveTarget
1021
- };
1022
- //# sourceMappingURL=chunk-DAE44LUH.js.map