@crvouga/mockingbird-service-slack 3.2.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,1194 +0,0 @@
1
- import {
2
- bindSocketTransport,
3
- claimSocketTicket,
4
- createClock,
5
- createRuntime,
6
- markSocketClosed,
7
- openSocketCount,
8
- parseDuration,
9
- resolveAdminPrefix,
10
- unbindSocketTransport
11
- } from "./chunk-B4MV67Z4.js";
12
-
13
- // ../../adapters/node/dist/cli.js
14
- import { readFile as readFile2 } from "node:fs/promises";
15
- import { createRequire } from "node:module";
16
- import { resolve } from "node:path";
17
- import { pathToFileURL } from "node:url";
18
- import { parseArgs } from "node:util";
19
-
20
- // ../../adapters/node/dist/fleet.js
21
- import { randomUUID } from "node:crypto";
22
- import { chmod, readFile, rename, rm, writeFile } from "node:fs/promises";
23
-
24
- // ../../adapters/node/dist/serve.js
25
- import { createServer } from "node:http";
26
- var serve = async (api, options = {}) => {
27
- const server = createServer(async (req, res) => {
28
- const chunks = [];
29
- for await (const chunk of req) {
30
- chunks.push(Buffer.from(chunk));
31
- }
32
- const body = Buffer.concat(chunks);
33
- const address = server.address();
34
- const port = typeof address === "object" && address !== null ? address.port : void 0;
35
- const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`;
36
- const raw = req.url ?? "/";
37
- const url = new URL(raw.replace(/^\/+/, "/"), base);
38
- const method = req.method ?? "GET";
39
- const init = { method, headers: req.headers };
40
- if (method !== "GET" && method !== "HEAD" && body.length > 0) {
41
- init.body = body;
42
- }
43
- const aborted = new AbortController();
44
- res.once("close", () => {
45
- if (!res.writableFinished)
46
- aborted.abort();
47
- });
48
- init.signal = aborted.signal;
49
- const request = new Request(url, init);
50
- let response;
51
- try {
52
- response = await api.fetch(request);
53
- } catch (error) {
54
- if (error.code === "MOCKINGBIRD_DROP") {
55
- req.socket.destroy();
56
- return;
57
- }
58
- res.writeHead(500, { "content-type": "application/json" });
59
- res.end(JSON.stringify({
60
- error: {
61
- type: "mockingbird_internal",
62
- message: error instanceof Error ? error.message : String(error)
63
- }
64
- }));
65
- return;
66
- }
67
- const headers = Object.fromEntries(response.headers);
68
- const cookies = response.headers.getSetCookie();
69
- if (cookies.length > 0)
70
- headers["set-cookie"] = cookies;
71
- if (!response.body) {
72
- res.writeHead(response.status, headers);
73
- res.end();
74
- return;
75
- }
76
- res.writeHead(response.status, headers);
77
- res.flushHeaders();
78
- const reader = response.body.getReader();
79
- try {
80
- for (; ; ) {
81
- const { done, value } = await reader.read();
82
- if (done)
83
- break;
84
- if (!res.write(value))
85
- await new Promise((resolve2) => res.once("drain", resolve2));
86
- }
87
- res.end();
88
- } catch {
89
- res.destroy();
90
- } finally {
91
- reader.releaseLock();
92
- }
93
- });
94
- await new Promise((resolve2, reject) => {
95
- server.once("error", reject);
96
- server.listen(options.port ?? 0, options.host, resolve2);
97
- });
98
- return server;
99
- };
100
-
101
- // ../../adapters/node/dist/listen.js
102
- var listen = async (api, options = {}) => {
103
- const host = options.host ?? "127.0.0.1";
104
- const server = await serve(api, { port: options.port ?? 0, host });
105
- const address = server.address();
106
- const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
107
- const shown = host.includes(":") ? `[${host}]` : host;
108
- return {
109
- url: `http://${shown}:${port}`,
110
- port,
111
- host,
112
- server,
113
- close: () => new Promise((resolve2, reject) => {
114
- server.close((error) => {
115
- if (error && error.code !== "ERR_SERVER_NOT_RUNNING")
116
- reject(error);
117
- else
118
- resolve2();
119
- });
120
- server.closeAllConnections?.();
121
- })
122
- };
123
- };
124
-
125
- // ../../adapters/node/dist/fleet.js
126
- var EMPTY = {
127
- activeRequests: 0,
128
- activeConnections: 0,
129
- blockedCommands: 0,
130
- pendingJobs: 0,
131
- pendingWebhooks: 0,
132
- unmatchedRequests: 0
133
- };
134
- var NS = /^[A-Za-z0-9_.-]{1,64}$/;
135
- var json = (status, value) => Response.json(value, { status });
136
- var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
137
- var pending = (d) => d.activeRequests + d.blockedCommands + d.pendingJobs + d.pendingWebhooks;
138
- var publicUrl = (raw) => {
139
- const url = new URL(raw);
140
- url.password = "";
141
- return url.href;
142
- };
143
- var atomicWrite = async (path, value) => {
144
- const temp = `${path}.${randomUUID()}.tmp`;
145
- try {
146
- await writeFile(temp, `${JSON.stringify(value, null, 2)}
147
- `, { mode: 384, flag: "wx" });
148
- await chmod(temp, 384);
149
- await rename(temp, path);
150
- } finally {
151
- await rm(temp, { force: true });
152
- }
153
- };
154
- var removeOwned = async (path, id) => {
155
- if (!path)
156
- return;
157
- try {
158
- const value = JSON.parse(await readFile(path, "utf8"));
159
- if (isObject(value) && value.id === id)
160
- await rm(path, { force: true });
161
- } catch (error) {
162
- if (error.code !== "ENOENT")
163
- throw error;
164
- }
165
- };
166
- var httpChild = async (target, entry, options) => {
167
- const runtime = await target.create(entry.options ?? {}, {
168
- adminPrefix: resolveAdminPrefix(entry.adminPrefix),
169
- adminKey: entry.adminKey,
170
- seed: entry.seed,
171
- onLog: options.onLog,
172
- ...entry.fixtures !== void 0 ? { fixtures: entry.fixtures } : {},
173
- ...entry.baseUrl !== void 0 ? { baseUrl: entry.baseUrl } : {}
174
- });
175
- const adminPrefix = resolveAdminPrefix(entry.adminPrefix);
176
- runtime.isolateNamespaces();
177
- const locked = /* @__PURE__ */ new Set();
178
- const active = /* @__PURE__ */ new Map();
179
- const api = {
180
- async fetch(request) {
181
- const namespace = runtime.namespaceOf(request);
182
- if (locked.has(namespace))
183
- return json(409, { error: "fleet control in progress", namespace });
184
- active.set(namespace, (active.get(namespace) ?? 0) + 1);
185
- try {
186
- return await runtime.fetch(request);
187
- } finally {
188
- active.set(namespace, (active.get(namespace) ?? 1) - 1);
189
- }
190
- }
191
- };
192
- let server;
193
- try {
194
- server = await listen(api, {
195
- port: entry.port ?? target.defaultPort,
196
- host: entry.host ?? "127.0.0.1"
197
- });
198
- target.listening?.(server);
199
- } catch (error) {
200
- runtime.stop?.();
201
- await server?.close();
202
- runtime.webhooks?.clear();
203
- await runtime.webhooks?.idle();
204
- throw error;
205
- }
206
- return {
207
- protocol: "http",
208
- url: server.url,
209
- healthUrl: `${server.url}${adminPrefix}/health`,
210
- adminUrl: `${server.url}${adminPrefix}`,
211
- namespaces: { header: "x-mockingbird-namespace", path: `${adminPrefix}/ns/{name}` },
212
- ready: async () => {
213
- try {
214
- return (await fetch(`${server.url}${adminPrefix}/health`, { signal: AbortSignal.timeout(2e3) })).ok;
215
- } catch {
216
- return false;
217
- }
218
- },
219
- async close() {
220
- runtime.stop?.();
221
- runtime.webhooks?.clear();
222
- await server.close();
223
- await runtime.webhooks?.idle();
224
- },
225
- lock(namespace) {
226
- if (locked.has(namespace))
227
- throw new Error("namespace already locked");
228
- locked.add(namespace);
229
- return () => locked.delete(namespace);
230
- },
231
- diagnostics(namespace) {
232
- return {
233
- ...EMPTY,
234
- activeRequests: active.get(namespace) ?? 0,
235
- pendingWebhooks: runtime.webhooks?.pending(namespace) ?? 0,
236
- unmatchedRequests: runtime.metrics.report(namespace).unmatched.reduce((n, r) => n + r.count, 0)
237
- };
238
- },
239
- async checkpoint(namespace) {
240
- return runtime.fleetSnapshot(namespace);
241
- },
242
- async restore(namespace, checkpoint) {
243
- runtime.fleetRestore(checkpoint, namespace);
244
- },
245
- reset: (namespace) => runtime.reset(namespace),
246
- clock: (namespace) => runtime.namespaceClock(namespace),
247
- async settle(namespace) {
248
- const instance = runtime.instance(namespace);
249
- instance.tick?.();
250
- await runtime.webhooks?.idle();
251
- }
252
- };
253
- };
254
- async function startFleet(config, options) {
255
- const id = randomUUID();
256
- const startedAt2 = (/* @__PURE__ */ new Date()).toISOString();
257
- const children = /* @__PURE__ */ new Map();
258
- const clocks = /* @__PURE__ */ new Map();
259
- const checkpoints = /* @__PURE__ */ new Map();
260
- let supervisor;
261
- let stopped = false;
262
- let controlling = false;
263
- const close = async () => {
264
- if (stopped)
265
- return;
266
- stopped = true;
267
- await Promise.allSettled([...children.values()].map((child) => child.close()));
268
- await supervisor?.close();
269
- await removeOwned(options.readyFile, id);
270
- await removeOwned(options.connectionsFile, id);
271
- };
272
- if (options.readyFile)
273
- await rm(options.readyFile, { force: true });
274
- if (options.connectionsFile)
275
- await rm(options.connectionsFile, { force: true });
276
- const clock = (namespace) => {
277
- let value = clocks.get(namespace);
278
- if (!value) {
279
- value = createClock();
280
- clocks.set(namespace, value);
281
- }
282
- return value;
283
- };
284
- const select = (body) => {
285
- if (body.services === void 0)
286
- return children;
287
- if (!Array.isArray(body.services) || body.services.length === 0)
288
- throw new Error("services must be a nonempty list");
289
- const selected = /* @__PURE__ */ new Map();
290
- for (const name of body.services) {
291
- if (typeof name !== "string" || !children.has(name))
292
- throw new Error("unknown service in selection");
293
- selected.set(name, children.get(name));
294
- }
295
- return selected;
296
- };
297
- const diagnostics = (namespace) => Object.fromEntries([...children].map(([name, child]) => [name, child.diagnostics(namespace)]));
298
- const transaction = async (namespace, selected, run) => {
299
- if (controlling)
300
- return json(409, { error: "another fleet operation is in progress" });
301
- controlling = true;
302
- const release = [];
303
- const before = /* @__PURE__ */ new Map();
304
- const results = {};
305
- let failure;
306
- try {
307
- for (const [name, child] of selected) {
308
- failure = name;
309
- release.push(child.lock(namespace));
310
- }
311
- const busy = Object.fromEntries([...selected].filter(([, child]) => {
312
- const state = child.diagnostics(namespace);
313
- return state.activeRequests + state.blockedCommands + state.pendingWebhooks + (state.activeJobs ?? 0) > 0;
314
- }).map(([name, child]) => [name, child.diagnostics(namespace)]));
315
- if (Object.keys(busy).length)
316
- return json(409, { error: "namespace is not quiescent", services: busy });
317
- for (const [name, child] of selected) {
318
- failure = name;
319
- before.set(name, await child.checkpoint(namespace));
320
- }
321
- for (const [name, child] of selected) {
322
- failure = name;
323
- results[name] = { status: "ok", result: await run(name, child) };
324
- }
325
- return json(200, { status: "ok", namespace, services: results });
326
- } catch {
327
- const rollback = {};
328
- for (const [name, point] of before) {
329
- try {
330
- await selected.get(name)?.restore(namespace, point);
331
- rollback[name] = "restored";
332
- } catch {
333
- rollback[name] = "failed";
334
- }
335
- }
336
- return json(500, {
337
- status: "failed",
338
- namespace,
339
- failedService: failure,
340
- services: results,
341
- rollback,
342
- rolledBack: before.size > 0 && Object.values(rollback).every((value) => value === "restored")
343
- });
344
- } finally {
345
- for (const unlock of release.reverse())
346
- unlock();
347
- controlling = false;
348
- }
349
- };
350
- const api = {
351
- async fetch(request) {
352
- const url = new URL(request.url);
353
- const adminPrefix = resolveAdminPrefix(config.adminPrefix);
354
- const path = url.pathname.startsWith(`${adminPrefix}/`) ? url.pathname.slice(adminPrefix.length) : "";
355
- if (path === "/health" && request.method === "GET") {
356
- const states = await Promise.all([...children].map(async ([name, child]) => {
357
- let protocolReady = false;
358
- try {
359
- protocolReady = await child.ready();
360
- } catch {
361
- }
362
- return [
363
- name,
364
- {
365
- processReady: !stopped,
366
- protocolReady,
367
- status: protocolReady ? "ready" : "failed",
368
- url: publicUrl(child.url)
369
- }
370
- ];
371
- }));
372
- const ready = !stopped && states.every(([, state]) => state.protocolReady);
373
- return json(ready ? 200 : 503, {
374
- status: ready ? "ready" : "failed",
375
- id,
376
- services: Object.fromEntries(states)
377
- });
378
- }
379
- if (!path.startsWith("/fleet/"))
380
- return json(404, { error: "not found" });
381
- const adminKey = config.adminKey ?? process.env.MOCKINGBIRD_ADMIN_KEY;
382
- if (adminKey !== void 0 && request.headers.get("x-mockingbird-admin-key") !== adminKey)
383
- return json(401, { error: "admin key required" });
384
- const route = /^\/fleet\/namespaces\/([^/]+)\/(reset|snapshots)(?:\/([^/]+)\/restore)?$/.exec(path);
385
- const namespace = route ? decodeURIComponent(route[1]) : url.searchParams.get("namespace") ?? request.headers.get("x-mockingbird-namespace") ?? config.namespace ?? "default";
386
- if (!NS.test(namespace))
387
- return json(400, { error: "invalid namespace" });
388
- if (request.method === "GET" && path === "/fleet/metrics")
389
- return json(200, { namespace, services: diagnostics(namespace) });
390
- if (request.method === "GET" && path === "/fleet/clock")
391
- return json(200, {
392
- namespace,
393
- ...clock(namespace).state(),
394
- services: Object.fromEntries([...children].map(([name, child]) => [name, child.clock(namespace).state()]))
395
- });
396
- if (request.method !== "POST")
397
- return json(405, { error: "method not allowed" });
398
- let body;
399
- try {
400
- const text2 = await request.text();
401
- const value = text2 ? JSON.parse(text2) : {};
402
- if (!isObject(value))
403
- throw new Error("expected object");
404
- body = value;
405
- } catch {
406
- return json(400, { error: "expected JSON object" });
407
- }
408
- let selected;
409
- try {
410
- selected = select(body);
411
- } catch (error) {
412
- return json(400, { error: error instanceof Error ? error.message : "bad selection" });
413
- }
414
- if (path === "/fleet/wait-until-idle") {
415
- const services = diagnostics(namespace);
416
- const idle = Object.values(services).every((d) => pending(d) === 0);
417
- return json(idle ? 200 : 409, { status: idle ? "idle" : "busy", namespace, services });
418
- }
419
- if (path === "/fleet/clock") {
420
- const current = clock(namespace);
421
- let target = current.now();
422
- if (body.set !== void 0) {
423
- target = typeof body.set === "number" ? body.set : typeof body.set === "string" ? Date.parse(body.set) : NaN;
424
- if (!Number.isFinite(target))
425
- return json(400, { error: "set must be epoch ms or ISO-8601" });
426
- }
427
- if (body.advance !== void 0) {
428
- const delta = parseDuration(body.advance);
429
- if (delta === void 0)
430
- return json(400, { error: "invalid advance" });
431
- target += delta;
432
- }
433
- const frozen = body.freeze === void 0 ? current.state().frozen : body.freeze;
434
- if (typeof frozen !== "boolean")
435
- return json(400, { error: "freeze must be boolean" });
436
- const response2 = await transaction(namespace, selected, async (_name, child) => {
437
- const childClock = child.clock(namespace);
438
- childClock.freeze();
439
- childClock.set(target);
440
- if (!frozen)
441
- childClock.unfreeze();
442
- await child.settle?.(namespace);
443
- return childClock.state();
444
- });
445
- if (response2.ok) {
446
- current.freeze();
447
- current.set(target);
448
- if (!frozen)
449
- current.unfreeze();
450
- }
451
- return response2;
452
- }
453
- if (!route)
454
- return json(404, { error: "not found" });
455
- if (route[2] === "reset") {
456
- if (route[3])
457
- return json(404, { error: "not found" });
458
- const response2 = await transaction(namespace, selected, async (_name, child) => child.reset(namespace));
459
- if (response2.ok && selected.size === children.size)
460
- clock(namespace).reset();
461
- return response2;
462
- }
463
- if (route[3]) {
464
- const point = checkpoints.get(route[3]);
465
- if (!point || point.namespace !== namespace)
466
- return json(404, { error: "no snapshot in this namespace" });
467
- const restored = new Map([...point.values].map(([name]) => [name, children.get(name)]));
468
- const response2 = await transaction(namespace, restored, async (name, child) => child.restore(namespace, point.values.get(name)));
469
- if (response2.ok) {
470
- const current = clock(namespace);
471
- current.freeze();
472
- current.set(point.clock.now);
473
- if (!point.clock.frozen)
474
- current.unfreeze();
475
- }
476
- return response2;
477
- }
478
- const snapshotId = randomUUID();
479
- const values = /* @__PURE__ */ new Map();
480
- const response = await transaction(namespace, selected, async (name, child) => {
481
- values.set(name, await child.checkpoint(namespace));
482
- return { checkpoint: `${snapshotId}:${name}` };
483
- });
484
- if (!response.ok)
485
- return response;
486
- checkpoints.set(snapshotId, { namespace, values, clock: clock(namespace).state() });
487
- return json(201, {
488
- ...await response.json(),
489
- id: snapshotId,
490
- label: body.label
491
- });
492
- }
493
- };
494
- try {
495
- const adminPrefix = resolveAdminPrefix(config.adminPrefix);
496
- if (!isObject(config.services) || Object.keys(config.services).length === 0)
497
- throw new Error("config must contain at least one service");
498
- for (const [name, entry] of Object.entries(config.services)) {
499
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
500
- throw new Error("invalid service name");
501
- try {
502
- const target = await options.load(name);
503
- if (entry.protocol && entry.protocol !== ("protocol" in target ? target.protocol : "http"))
504
- throw new Error("configured protocol does not match target");
505
- const adminKey = entry.adminKey ?? config.adminKey ?? process.env.MOCKINGBIRD_ADMIN_KEY;
506
- const child = "start" in target ? await target.start(entry) : await httpChild(target, {
507
- ...entry,
508
- adminPrefix: resolveAdminPrefix(entry.adminPrefix ?? adminPrefix),
509
- ...adminKey !== void 0 ? { adminKey } : {}
510
- }, options);
511
- children.set(name, child);
512
- if (!await child.ready())
513
- throw new Error("protocol readiness failed");
514
- } catch {
515
- throw new Error(`service ${name} failed to start; all started listeners were closed`);
516
- }
517
- }
518
- supervisor = await listen(api, {
519
- port: config.control?.port ?? 0,
520
- host: config.control?.host ?? "127.0.0.1"
521
- });
522
- const manifest = {
523
- version: 1,
524
- state: "ready",
525
- pid: process.pid,
526
- id,
527
- startedAt: startedAt2,
528
- adminBase: `${supervisor.url}${adminPrefix}/fleet`,
529
- healthUrl: `${supervisor.url}${adminPrefix}/health`,
530
- services: Object.fromEntries([...children].map(([name, child]) => [
531
- name,
532
- {
533
- protocol: child.protocol,
534
- url: publicUrl(child.url),
535
- healthUrl: child.healthUrl ?? `${supervisor?.url}${adminPrefix}/health`,
536
- adminUrl: child.adminUrl ?? `${supervisor?.url}${adminPrefix}/fleet`,
537
- namespaces: child.namespaces,
538
- processReady: true,
539
- protocolReady: true
540
- }
541
- ]))
542
- };
543
- if (options.connectionsFile)
544
- await atomicWrite(options.connectionsFile, {
545
- version: 1,
546
- id,
547
- pid: process.pid,
548
- services: Object.fromEntries([...children].map(([name, child]) => [
549
- name,
550
- { url: child.connection ?? child.url, namespaces: child.connections }
551
- ]))
552
- });
553
- if (options.readyFile)
554
- await atomicWrite(options.readyFile, manifest);
555
- options.onReady?.(manifest);
556
- return { manifest, children, fetch: api.fetch, close };
557
- } catch (error) {
558
- await close();
559
- throw error;
560
- }
561
- }
562
-
563
- // ../../adapters/node/dist/cli.js
564
- var optionHelp = (options) => Object.entries(options).map(([name, option]) => {
565
- const flag = `--${name}${option.type === "string" ? ` ${option.value ?? "<value>"}` : ""}`;
566
- const fallback = option.default !== void 0 ? ` (default: ${String(option.default)})` : "";
567
- return ` ${flag.padEnd(30)} ${option.description}${fallback}`;
568
- });
569
- var help = (spec) => [
570
- `${spec.bin} \u2014 ${spec.description}`,
571
- "",
572
- "Usage:",
573
- ` ${spec.bin} <command> [options]`,
574
- "",
575
- "Commands:",
576
- ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),
577
- "",
578
- `Run \`${spec.bin} <command> --help\` for a command's options.`
579
- ].join("\n");
580
- var commandHelp = (spec, name, command) => [
581
- `${spec.bin} ${name} \u2014 ${command.summary}`,
582
- "",
583
- "Usage:",
584
- ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,
585
- ...command.options ? ["", "Options:", ...optionHelp(command.options)] : []
586
- ].join("\n");
587
- var runCli = async (spec, argv) => {
588
- const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : void 0;
589
- const words = pair !== void 0 && spec.commands[pair] ? 2 : 1;
590
- const name = words === 2 ? pair : argv[0];
591
- const rest = argv.slice(words);
592
- if (name === void 0 || name === "--help" || name === "-h" || name === "help") {
593
- console.log(help(spec));
594
- return 0;
595
- }
596
- const command = spec.commands[name];
597
- if (!command) {
598
- console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}
599
-
600
- ${help(spec)}`);
601
- return 2;
602
- }
603
- if (rest.includes("--help") || rest.includes("-h")) {
604
- console.log(commandHelp(spec, name, command));
605
- return 0;
606
- }
607
- let parsed;
608
- try {
609
- parsed = parseArgs({
610
- args: rest,
611
- allowPositionals: true,
612
- strict: true,
613
- options: Object.fromEntries(Object.entries(command.options ?? {}).map(([key, option]) => [
614
- key,
615
- {
616
- type: option.type,
617
- ...option.default !== void 0 ? { default: option.default } : {}
618
- }
619
- ]))
620
- });
621
- } catch (error) {
622
- console.error(`${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}
623
-
624
- ${commandHelp(spec, name, command)}`);
625
- return 2;
626
- }
627
- return command.run(parsed.values, parsed.positionals);
628
- };
629
- var COMMON_SERVE_OPTIONS = {
630
- port: { type: "string", value: "<port>", description: "Port to listen on" },
631
- host: { type: "string", value: "<host>", description: "Interface to bind", default: "127.0.0.1" },
632
- "admin-prefix": {
633
- type: "string",
634
- value: "<path>",
635
- description: "Internal API prefix (default /__admin; env MOCKINGBIRD_ADMIN_PREFIX)"
636
- },
637
- "admin-key": {
638
- type: "string",
639
- value: "<key>",
640
- description: "Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)"
641
- },
642
- seed: { type: "string", value: "<seed>", description: "Seed for every random choice" },
643
- log: {
644
- type: "string",
645
- value: "<pretty|json|off>",
646
- description: "Request log format",
647
- default: "pretty"
648
- },
649
- "log-requests": {
650
- type: "boolean",
651
- description: "One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json"
652
- },
653
- config: {
654
- type: "string",
655
- value: "<file>",
656
- description: "Serve every service in a mockingbird.json config instead"
657
- },
658
- "ready-file": {
659
- type: "string",
660
- description: "Atomically publish fleet endpoint JSON",
661
- value: "<file>"
662
- },
663
- "connections-file": {
664
- type: "string",
665
- description: "Write private connection URLs with mode 0600",
666
- value: "<file>"
667
- },
668
- "ready-json": {
669
- type: "boolean",
670
- description: "Emit one public JSON record after fleet readiness"
671
- }
672
- };
673
- var formatLog = (format) => {
674
- if (format === "off")
675
- return void 0;
676
- if (format === "json")
677
- return (entry) => console.log(JSON.stringify(entry));
678
- return (entry) => {
679
- const op = entry.operationId ?? (entry.unmatched ? "UNMATCHED" : "-");
680
- const ns = entry.namespace === "default" ? "" : ` [${entry.namespace}]`;
681
- const fault = entry.faultId ? ` fault=${entry.faultId}` : "";
682
- const adopted = entry.adopted ? " adopted" : "";
683
- const received = entry.request ? ` received=${entry.request.bodyBytes === null ? "?" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? "-"}${entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : ""}` : "";
684
- 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})` : ""}` : "";
685
- console.log(`${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`);
686
- };
687
- };
688
- var asString = (value) => typeof value === "string" ? value : void 0;
689
- var loadTarget = async (name, own) => {
690
- if (name === own.name)
691
- return own;
692
- const specifier = `@crvouga/mockingbird-service-${name}/server`;
693
- try {
694
- let mod;
695
- try {
696
- mod = await import(specifier);
697
- } catch {
698
- const fromProject = createRequire(resolve(process.cwd(), "package.json"));
699
- mod = await import(pathToFileURL(fromProject.resolve(specifier)).href);
700
- }
701
- if (!mod.serveTarget)
702
- throw new Error(`${specifier} exports no serveTarget`);
703
- return mod.serveTarget;
704
- } catch (error) {
705
- const reason = error instanceof Error ? error.message : String(error);
706
- throw new Error(`cannot load service "${name}": ${reason}. Install @crvouga/mockingbird-service-${name}.`);
707
- }
708
- };
709
- var start = async (target, values, config) => {
710
- const adminPrefix = resolveAdminPrefix(config.adminPrefix);
711
- const runtime = await target.create(values, {
712
- adminPrefix,
713
- adminKey: config.adminKey,
714
- seed: config.seed,
715
- onLog: formatLog(config.log)
716
- });
717
- const listening = await listen(runtime, { port: config.port, host: config.host });
718
- target.listening?.(listening);
719
- console.log(`${target.name} emulator listening on ${listening.url}`);
720
- console.log(`${target.name} health: GET ${listening.url}${adminPrefix}/health`);
721
- console.log(`${target.name} admin: ${listening.url}${adminPrefix} (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
722
- console.log(`${target.name} admin ui: ${listening.url}${adminPrefix}/ui`);
723
- for (const line of target.banner?.(runtime) ?? [])
724
- console.log(`${target.name} ${line.replaceAll("/__admin", adminPrefix)}`);
725
- return listening;
726
- };
727
- var untilSignal = async (servers) => new Promise((resolve2) => {
728
- const stop = async () => {
729
- ;
730
- process.removeListener("SIGINT", stop);
731
- process.removeListener("SIGTERM", stop);
732
- await Promise.allSettled(servers.map((s) => s.close()));
733
- resolve2(0);
734
- };
735
- process.once("SIGINT", stop);
736
- process.once("SIGTERM", stop);
737
- });
738
- var serveCommand = (target) => ({
739
- summary: `Serve the ${target.name} emulator or a configured fleet`,
740
- options: { ...COMMON_SERVE_OPTIONS, ..."options" in target ? target.options : {} },
741
- async run(values) {
742
- const log = values["log-requests"] === true ? "json" : asString(values.log) ?? "pretty";
743
- if (!["pretty", "json", "off"].includes(log)) {
744
- console.error(`--log must be pretty, json or off (got ${log})`);
745
- return 2;
746
- }
747
- const configPath = asString(values.config);
748
- if (configPath !== void 0) {
749
- try {
750
- const config = JSON.parse(await readFile2(configPath, "utf8"));
751
- config.adminPrefix = resolveAdminPrefix(config.adminPrefix ?? asString(values["admin-prefix"]) ?? process.env.MOCKINGBIRD_ADMIN_PREFIX);
752
- const adminKey2 = config.adminKey ?? asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
753
- if (adminKey2 !== void 0)
754
- config.adminKey = adminKey2;
755
- const fleet = await startFleet(config, {
756
- load: (name) => loadTarget(name, target),
757
- onLog: formatLog(config.log ?? log),
758
- ...asString(values["ready-file"]) ? { readyFile: asString(values["ready-file"]) } : {},
759
- ...asString(values["connections-file"]) ? { connectionsFile: asString(values["connections-file"]) } : {},
760
- onReady: (manifest) => {
761
- if (values["ready-json"])
762
- console.log(JSON.stringify(manifest));
763
- else
764
- for (const [name, endpoint] of Object.entries(manifest.services))
765
- console.log(`${name} emulator listening on ${endpoint.url}`);
766
- }
767
- });
768
- return untilSignal([fleet]);
769
- } catch (error) {
770
- console.error(error instanceof Error ? error.message : String(error));
771
- return 1;
772
- }
773
- }
774
- if ("start" in target) {
775
- console.error("Use --config to supervise protocol services with this command");
776
- return 2;
777
- }
778
- const port = asString(values.port);
779
- const adminKey = asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
780
- const seed = asString(values.seed);
781
- let listening;
782
- try {
783
- listening = await start(target, values, {
784
- adminPrefix: resolveAdminPrefix(asString(values["admin-prefix"]) ?? process.env.MOCKINGBIRD_ADMIN_PREFIX),
785
- port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
786
- host: asString(values.host) ?? "127.0.0.1",
787
- ...adminKey !== void 0 ? { adminKey } : {},
788
- ...seed !== void 0 ? { seed } : {},
789
- log
790
- });
791
- } catch (error) {
792
- console.error(error instanceof Error ? error.message : String(error));
793
- return 1;
794
- }
795
- return untilSignal([listening]);
796
- }
797
- });
798
-
799
- // ../../adapters/node/dist/fixtures.js
800
- import { readFile as readFile3 } from "node:fs/promises";
801
- var providerServeOptions = {
802
- fixtures: {
803
- type: "string",
804
- value: "<json-file>",
805
- description: "Load synthetic provider fixtures from a JSON object"
806
- },
807
- "base-url": {
808
- type: "string",
809
- value: "<url>",
810
- description: "Public HTTP(S) base URL advertised in provider responses"
811
- }
812
- };
813
- async function providerServeValues(values, defaults = {}) {
814
- const options = {};
815
- if (values.fixtures !== void 0) {
816
- if (typeof values.fixtures !== "string")
817
- throw new Error("--fixtures requires a JSON file");
818
- let parsed;
819
- try {
820
- parsed = JSON.parse(await readFile3(values.fixtures, "utf8"));
821
- } catch {
822
- throw new Error("Cannot read --fixtures as JSON; supply a synthetic provider fixture object");
823
- }
824
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
825
- throw new Error("--fixtures must contain a JSON object");
826
- options.fixtures = parsed;
827
- } else if (defaults.fixtures !== void 0) {
828
- if (defaults.fixtures === null || typeof defaults.fixtures !== "object" || Array.isArray(defaults.fixtures))
829
- throw new Error("fleet fixtures must contain a JSON object");
830
- options.fixtures = defaults.fixtures;
831
- }
832
- const advertised = values["base-url"] ?? defaults.baseUrl;
833
- if (advertised !== void 0) {
834
- let url;
835
- try {
836
- if (typeof advertised !== "string")
837
- throw new Error();
838
- url = new URL(advertised);
839
- } catch {
840
- throw new Error("--base-url must be an absolute HTTP(S) URL");
841
- }
842
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
843
- throw new Error("--base-url must be HTTP(S) without credentials, query or fragment");
844
- options.baseUrl = url.href.replace(/\/+$/, "");
845
- }
846
- return options;
847
- }
848
-
849
- // src/socket-server.ts
850
- import { createHash } from "node:crypto";
851
- var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
852
- var SERVER_PING_MS = 2e3;
853
- var MAX_FRAME = 1e6;
854
- var HOST = "applink-mockingbird";
855
- var attached = /* @__PURE__ */ new WeakSet();
856
- var encodeFrame = (opcode, payload) => {
857
- const length = payload.length;
858
- let header;
859
- if (length < 126) {
860
- header = Buffer.alloc(2);
861
- header[1] = length;
862
- } else if (length < 65536) {
863
- header = Buffer.alloc(4);
864
- header[1] = 126;
865
- header.writeUInt16BE(length, 2);
866
- } else {
867
- header = Buffer.alloc(10);
868
- header[1] = 127;
869
- header.writeBigUInt64BE(BigInt(length), 2);
870
- }
871
- header[0] = 128 | opcode;
872
- return Buffer.concat([header, payload]);
873
- };
874
- var acceptKey = (key) => createHash("sha1").update(key + GUID).digest("base64");
875
- var rejectHttp = (socket, status, reason) => {
876
- const body = Buffer.from(reason);
877
- socket.end(
878
- `HTTP/1.1 ${status} ${reason}\r
879
- Connection: close\r
880
- Content-Type: text/plain\r
881
- Content-Length: ${body.length}\r
882
- \r
883
- ${reason}`
884
- );
885
- };
886
- var startedAt = (ms) => new Date(ms).toISOString().replace("T", " ").replace("Z", "");
887
- var live = /* @__PURE__ */ new Map();
888
- var createParser = (onFrame, onError) => {
889
- let buffer = Buffer.alloc(0);
890
- let fragments = [];
891
- let fragmentOpcode = 0;
892
- return {
893
- push(chunk) {
894
- buffer = Buffer.concat([buffer, chunk]);
895
- if (buffer.length > MAX_FRAME) {
896
- onError();
897
- return;
898
- }
899
- for (; ; ) {
900
- if (buffer.length < 2) return;
901
- const first = buffer[0] ?? 0;
902
- const second = buffer[1] ?? 0;
903
- if ((first & 112) !== 0) {
904
- onError();
905
- return;
906
- }
907
- const fin = (first & 128) !== 0;
908
- const opcode = first & 15;
909
- const masked = (second & 128) !== 0;
910
- let length = second & 127;
911
- let offset = 2;
912
- if (length === 126) {
913
- if (buffer.length < 4) return;
914
- length = buffer.readUInt16BE(2);
915
- offset = 4;
916
- } else if (length === 127) {
917
- if (buffer.length < 10) return;
918
- const wide = buffer.readBigUInt64BE(2);
919
- if (wide > BigInt(MAX_FRAME)) {
920
- onError();
921
- return;
922
- }
923
- length = Number(wide);
924
- offset = 10;
925
- }
926
- if (length > MAX_FRAME) {
927
- onError();
928
- return;
929
- }
930
- const maskLength = masked ? 4 : 0;
931
- if (buffer.length < offset + maskLength + length) return;
932
- const raw = buffer.subarray(offset + maskLength, offset + maskLength + length);
933
- let payload;
934
- if (masked) {
935
- const mask = buffer.subarray(offset, offset + 4);
936
- payload = Buffer.alloc(length);
937
- for (let i = 0; i < length; i++) payload[i] = (raw[i] ?? 0) ^ (mask[i % 4] ?? 0);
938
- } else payload = Buffer.from(raw);
939
- buffer = Buffer.from(buffer.subarray(offset + maskLength + length));
940
- if (opcode === 0) {
941
- if (fragmentOpcode === 0) {
942
- onError();
943
- return;
944
- }
945
- fragments.push(payload);
946
- if (fin) {
947
- const message = Buffer.concat(fragments);
948
- const op = fragmentOpcode;
949
- fragments = [];
950
- fragmentOpcode = 0;
951
- onFrame(op, message);
952
- }
953
- continue;
954
- }
955
- if (opcode === 1 || opcode === 2) {
956
- if (!fin) {
957
- fragmentOpcode = opcode;
958
- fragments = [payload];
959
- } else onFrame(opcode, payload);
960
- continue;
961
- }
962
- onFrame(opcode, payload);
963
- }
964
- }
965
- };
966
- };
967
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
968
- var writeFrame = (socket, opcode, payload) => {
969
- if (socket.destroyed || !socket.writable) return;
970
- socket.write(encodeFrame(opcode, payload));
971
- };
972
- var helloBody = (record, approximate) => JSON.stringify({
973
- type: "hello",
974
- num_connections: openSocketCount(record.namespace),
975
- debug_info: {
976
- host: HOST,
977
- started: startedAt(Date.now()),
978
- build_number: 1,
979
- approximate_connection_time: approximate
980
- },
981
- connection_info: { app_id: record.appId }
982
- });
983
- var disconnectBody = (reason) => JSON.stringify({ type: "disconnect", reason, debug_info: { host: HOST } });
984
- var acceptUpgrade = (req, socket, head, owned) => {
985
- const url = new URL(req.url ?? "/", "http://localhost");
986
- if (url.pathname !== "/link" && url.pathname !== "/link/") {
987
- rejectHttp(socket, 404, "not_found");
988
- return;
989
- }
990
- const ticket = url.searchParams.get("ticket");
991
- const key = req.headers["sec-websocket-key"];
992
- if (!ticket || typeof key !== "string" || key.length === 0) {
993
- rejectHttp(socket, 400, "bad_request");
994
- return;
995
- }
996
- const record = claimSocketTicket(ticket);
997
- if (!record) {
998
- rejectHttp(socket, 404, "ticket_used");
999
- return;
1000
- }
1001
- socket.setNoDelay(true);
1002
- socket.write(
1003
- `HTTP/1.1 101 Switching Protocols\r
1004
- Upgrade: websocket\r
1005
- Connection: Upgrade\r
1006
- Sec-WebSocket-Accept: ${acceptKey(key)}\r
1007
- \r
1008
- `
1009
- );
1010
- const session = {
1011
- ticket,
1012
- socket,
1013
- done: false,
1014
- ping: void 0,
1015
- lifetime: void 0,
1016
- hello: void 0
1017
- };
1018
- live.set(ticket, session);
1019
- owned.add(ticket);
1020
- const cleanup = () => {
1021
- if (session.done) return;
1022
- session.done = true;
1023
- if (session.ping) clearInterval(session.ping);
1024
- if (session.lifetime) clearTimeout(session.lifetime);
1025
- if (session.hello) clearTimeout(session.hello);
1026
- live.delete(ticket);
1027
- owned.delete(ticket);
1028
- unbindSocketTransport(ticket);
1029
- markSocketClosed(ticket);
1030
- };
1031
- const finish = (code) => {
1032
- if (session.done) return;
1033
- if (code === 1006) socket.destroy();
1034
- else {
1035
- const payload = Buffer.alloc(2);
1036
- payload.writeUInt16BE(code, 0);
1037
- writeFrame(socket, 8, payload);
1038
- socket.end();
1039
- }
1040
- cleanup();
1041
- };
1042
- bindSocketTransport(ticket, {
1043
- disconnect(reason, code) {
1044
- if (session.done) return;
1045
- writeFrame(socket, 1, Buffer.from(disconnectBody(reason)));
1046
- const timer = setTimeout(() => finish(code), 20);
1047
- timer.unref();
1048
- },
1049
- destroy() {
1050
- socket.destroy();
1051
- cleanup();
1052
- }
1053
- });
1054
- const parser = createParser(
1055
- (opcode, data) => {
1056
- if (session.done) return;
1057
- if (opcode === 8) {
1058
- finish(1e3);
1059
- return;
1060
- }
1061
- if (opcode === 9) {
1062
- writeFrame(socket, 10, data);
1063
- return;
1064
- }
1065
- if (opcode !== 1) return;
1066
- let parsed;
1067
- try {
1068
- parsed = JSON.parse(data.toString("utf8"));
1069
- } catch {
1070
- return;
1071
- }
1072
- if (isRecord(parsed) && typeof parsed.envelope_id === "string" && parsed.type === void 0)
1073
- return;
1074
- },
1075
- () => finish(1002)
1076
- );
1077
- socket.on("data", (chunk) => {
1078
- if (session.done) return;
1079
- parser.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1080
- });
1081
- socket.on("close", cleanup);
1082
- socket.on("error", cleanup);
1083
- socket.on("end", cleanup);
1084
- if (head.length > 0) parser.push(head);
1085
- const approximate = url.searchParams.get("debug_reconnects") === "true" ? 360 : record.lifetimeMs !== null && record.lifetimeMs > 0 ? Math.max(1, Math.ceil(record.lifetimeMs / 1e3)) : 3600;
1086
- const deliverHello = () => {
1087
- if (session.done) return;
1088
- const frame = Buffer.from(helloBody(record, approximate));
1089
- socket.write(encodeFrame(1, frame), () => {
1090
- if (session.done) return;
1091
- if (record.abnormalClose) {
1092
- socket.destroy();
1093
- cleanup();
1094
- return;
1095
- }
1096
- if (record.closeAfterHello) {
1097
- finish(1e3);
1098
- return;
1099
- }
1100
- session.ping = setInterval(() => {
1101
- if (session.done) return;
1102
- writeFrame(socket, 9, Buffer.from("ping"));
1103
- }, SERVER_PING_MS);
1104
- session.ping.unref();
1105
- if (record.lifetimeMs !== null && record.lifetimeMs > 0) {
1106
- session.lifetime = setTimeout(() => {
1107
- if (session.done) return;
1108
- writeFrame(socket, 1, Buffer.from(disconnectBody("warning")));
1109
- const timer = setTimeout(() => finish(1e3), 20);
1110
- timer.unref();
1111
- }, record.lifetimeMs);
1112
- session.lifetime.unref();
1113
- }
1114
- });
1115
- };
1116
- if (record.helloLatencyMs > 0) {
1117
- session.hello = setTimeout(deliverHello, record.helloLatencyMs);
1118
- session.hello.unref();
1119
- } else deliverHello();
1120
- };
1121
- var attachSocketServer = (server) => {
1122
- if (attached.has(server)) return;
1123
- attached.add(server);
1124
- const owned = /* @__PURE__ */ new Set();
1125
- server.on("upgrade", (req, socket, head) => {
1126
- acceptUpgrade(req, socket, head, owned);
1127
- });
1128
- server.on("close", () => {
1129
- for (const ticket of owned) live.get(ticket)?.socket.destroy();
1130
- owned.clear();
1131
- });
1132
- };
1133
-
1134
- // src/server.ts
1135
- var DEFAULT_PORT = 8808;
1136
- var createServer2 = async (options = {}) => {
1137
- const { port, host, ...rest } = options;
1138
- const runtime = createRuntime(rest);
1139
- const listening = await listen(runtime, {
1140
- port: port ?? 0,
1141
- ...host !== void 0 ? { host } : {}
1142
- });
1143
- attachSocketServer(listening.server);
1144
- return { ...listening, runtime };
1145
- };
1146
- var text = (value) => typeof value === "string" ? value : void 0;
1147
- var serveTarget = {
1148
- name: "slack",
1149
- defaultPort: DEFAULT_PORT,
1150
- options: {
1151
- ...providerServeOptions,
1152
- token: {
1153
- type: "string",
1154
- value: "<xoxb-\u2026>",
1155
- description: "Only accept this bot token on the Web API (default: any xoxb-/xoxp- token is accepted)"
1156
- },
1157
- "strict-channels": {
1158
- type: "boolean",
1159
- description: "Answer channel_not_found for channels not created through /__admin/channels"
1160
- }
1161
- },
1162
- create: async (values, common) => {
1163
- const token = text(values.token);
1164
- return createRuntime({
1165
- ...await providerServeValues(values, common),
1166
- settings: {
1167
- ...token ? { tokens: [token] } : {},
1168
- ...values["strict-channels"] === true ? { strictChannels: true } : {}
1169
- },
1170
- ...common.adminPrefix !== void 0 ? { adminPrefix: common.adminPrefix } : {},
1171
- ...common.adminKey !== void 0 ? { adminKey: common.adminKey } : {},
1172
- ...common.seed !== void 0 ? { seed: common.seed } : {},
1173
- ...common.onLog ? { onLog: common.onLog } : {}
1174
- });
1175
- },
1176
- listening: (listening) => {
1177
- attachSocketServer(listening.server);
1178
- },
1179
- banner: () => [
1180
- "webhooks: point SLACK_*_WEBHOOK_URL at <this>/services/T000/B000/XXXX (any path works until you POST /__admin/hooks)",
1181
- "web api: Authorization: Bearer xoxb-\u2026; @slack/web-api: new WebClient(token, {slackApiUrl: '<this>/api/'})",
1182
- "socket mode: Authorization: Bearer xapp-\u2026 on POST <this>/api/apps.connections.open; Bolt: socketMode + clientOptions.slackApiUrl '<this>/api/'",
1183
- "outbox: GET /__admin/outbox?webhook=/services/T/B/X or ?channel=C\u2026"
1184
- ]
1185
- };
1186
-
1187
- export {
1188
- runCli,
1189
- serveCommand,
1190
- DEFAULT_PORT,
1191
- createServer2 as createServer,
1192
- serveTarget
1193
- };
1194
- //# sourceMappingURL=chunk-IM5DJEPT.js.map