@crvouga/mockingbird-service-slack 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +31 -0
- package/dist/{chunk-73BP3FZK.js → chunk-56E5SPJ7.js} +791 -262
- package/dist/chunk-56E5SPJ7.js.map +7 -0
- package/dist/{chunk-LOAZPGDT.js → chunk-PVVAIER7.js} +518 -31
- package/dist/chunk-PVVAIER7.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +35 -5
- package/dist/index.js +1 -1
- package/dist/server.d.ts +64 -4
- package/dist/server.js +2 -2
- package/openapi.yaml +1945 -0
- package/package.json +29 -4
- package/dist/chunk-73BP3FZK.js.map +0 -7
- package/dist/chunk-LOAZPGDT.js.map +0 -7
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
2
|
bindSocketTransport,
|
|
3
3
|
claimSocketTicket,
|
|
4
|
+
createClock,
|
|
4
5
|
createRuntime,
|
|
5
6
|
markSocketClosed,
|
|
6
7
|
openSocketCount,
|
|
8
|
+
parseDuration,
|
|
9
|
+
resolveAdminPrefix,
|
|
7
10
|
unbindSocketTransport
|
|
8
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-56E5SPJ7.js";
|
|
9
12
|
|
|
10
13
|
// ../../adapters/node/dist/cli.js
|
|
11
|
-
import { readFile } from "node:fs/promises";
|
|
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";
|
|
12
18
|
import { parseArgs } from "node:util";
|
|
13
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
|
+
|
|
14
24
|
// ../../adapters/node/dist/serve.js
|
|
15
25
|
import { createServer } from "node:http";
|
|
16
26
|
var serve = async (api, options = {}) => {
|
|
@@ -72,7 +82,7 @@ var serve = async (api, options = {}) => {
|
|
|
72
82
|
if (done)
|
|
73
83
|
break;
|
|
74
84
|
if (!res.write(value))
|
|
75
|
-
await new Promise((
|
|
85
|
+
await new Promise((resolve2) => res.once("drain", resolve2));
|
|
76
86
|
}
|
|
77
87
|
res.end();
|
|
78
88
|
} catch {
|
|
@@ -81,9 +91,9 @@ var serve = async (api, options = {}) => {
|
|
|
81
91
|
reader.releaseLock();
|
|
82
92
|
}
|
|
83
93
|
});
|
|
84
|
-
await new Promise((
|
|
94
|
+
await new Promise((resolve2, reject) => {
|
|
85
95
|
server.once("error", reject);
|
|
86
|
-
server.listen(options.port ?? 0, options.host,
|
|
96
|
+
server.listen(options.port ?? 0, options.host, resolve2);
|
|
87
97
|
});
|
|
88
98
|
return server;
|
|
89
99
|
};
|
|
@@ -100,18 +110,454 @@ var listen = async (api, options = {}) => {
|
|
|
100
110
|
port,
|
|
101
111
|
host,
|
|
102
112
|
server,
|
|
103
|
-
close: () => new Promise((
|
|
113
|
+
close: () => new Promise((resolve2, reject) => {
|
|
104
114
|
server.close((error) => {
|
|
105
115
|
if (error && error.code !== "ERR_SERVER_NOT_RUNNING")
|
|
106
116
|
reject(error);
|
|
107
117
|
else
|
|
108
|
-
|
|
118
|
+
resolve2();
|
|
109
119
|
});
|
|
110
120
|
server.closeAllConnections?.();
|
|
111
121
|
})
|
|
112
122
|
};
|
|
113
123
|
};
|
|
114
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
|
+
});
|
|
173
|
+
const adminPrefix = resolveAdminPrefix(entry.adminPrefix);
|
|
174
|
+
runtime.isolateNamespaces();
|
|
175
|
+
const locked = /* @__PURE__ */ new Set();
|
|
176
|
+
const active = /* @__PURE__ */ new Map();
|
|
177
|
+
const api = {
|
|
178
|
+
async fetch(request) {
|
|
179
|
+
const namespace = runtime.namespaceOf(request);
|
|
180
|
+
if (locked.has(namespace))
|
|
181
|
+
return json(409, { error: "fleet control in progress", namespace });
|
|
182
|
+
active.set(namespace, (active.get(namespace) ?? 0) + 1);
|
|
183
|
+
try {
|
|
184
|
+
return await runtime.fetch(request);
|
|
185
|
+
} finally {
|
|
186
|
+
active.set(namespace, (active.get(namespace) ?? 1) - 1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
let server;
|
|
191
|
+
try {
|
|
192
|
+
server = await listen(api, {
|
|
193
|
+
port: entry.port ?? target.defaultPort,
|
|
194
|
+
host: entry.host ?? "127.0.0.1"
|
|
195
|
+
});
|
|
196
|
+
target.listening?.(server);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
runtime.stop?.();
|
|
199
|
+
await server?.close();
|
|
200
|
+
runtime.webhooks?.clear();
|
|
201
|
+
await runtime.webhooks?.idle();
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
protocol: "http",
|
|
206
|
+
url: server.url,
|
|
207
|
+
healthUrl: `${server.url}${adminPrefix}/health`,
|
|
208
|
+
adminUrl: `${server.url}${adminPrefix}`,
|
|
209
|
+
namespaces: { header: "x-mockingbird-namespace", path: `${adminPrefix}/ns/{name}` },
|
|
210
|
+
ready: async () => {
|
|
211
|
+
try {
|
|
212
|
+
return (await fetch(`${server.url}${adminPrefix}/health`, { signal: AbortSignal.timeout(2e3) })).ok;
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
async close() {
|
|
218
|
+
runtime.stop?.();
|
|
219
|
+
runtime.webhooks?.clear();
|
|
220
|
+
await server.close();
|
|
221
|
+
await runtime.webhooks?.idle();
|
|
222
|
+
},
|
|
223
|
+
lock(namespace) {
|
|
224
|
+
if (locked.has(namespace))
|
|
225
|
+
throw new Error("namespace already locked");
|
|
226
|
+
locked.add(namespace);
|
|
227
|
+
return () => locked.delete(namespace);
|
|
228
|
+
},
|
|
229
|
+
diagnostics(namespace) {
|
|
230
|
+
return {
|
|
231
|
+
...EMPTY,
|
|
232
|
+
activeRequests: active.get(namespace) ?? 0,
|
|
233
|
+
pendingWebhooks: runtime.webhooks?.pending(namespace) ?? 0,
|
|
234
|
+
unmatchedRequests: runtime.metrics.report(namespace).unmatched.reduce((n, r) => n + r.count, 0)
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
async checkpoint(namespace) {
|
|
238
|
+
return runtime.fleetSnapshot(namespace);
|
|
239
|
+
},
|
|
240
|
+
async restore(namespace, checkpoint) {
|
|
241
|
+
runtime.fleetRestore(checkpoint, namespace);
|
|
242
|
+
},
|
|
243
|
+
reset: (namespace) => runtime.reset(namespace),
|
|
244
|
+
clock: (namespace) => runtime.namespaceClock(namespace),
|
|
245
|
+
async settle(namespace) {
|
|
246
|
+
const instance = runtime.instance(namespace);
|
|
247
|
+
instance.tick?.();
|
|
248
|
+
await runtime.webhooks?.idle();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
async function startFleet(config, options) {
|
|
253
|
+
const id = randomUUID();
|
|
254
|
+
const startedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
255
|
+
const children = /* @__PURE__ */ new Map();
|
|
256
|
+
const clocks = /* @__PURE__ */ new Map();
|
|
257
|
+
const checkpoints = /* @__PURE__ */ new Map();
|
|
258
|
+
let supervisor;
|
|
259
|
+
let stopped = false;
|
|
260
|
+
let controlling = false;
|
|
261
|
+
const close = async () => {
|
|
262
|
+
if (stopped)
|
|
263
|
+
return;
|
|
264
|
+
stopped = true;
|
|
265
|
+
await Promise.allSettled([...children.values()].map((child) => child.close()));
|
|
266
|
+
await supervisor?.close();
|
|
267
|
+
await removeOwned(options.readyFile, id);
|
|
268
|
+
await removeOwned(options.connectionsFile, id);
|
|
269
|
+
};
|
|
270
|
+
if (options.readyFile)
|
|
271
|
+
await rm(options.readyFile, { force: true });
|
|
272
|
+
if (options.connectionsFile)
|
|
273
|
+
await rm(options.connectionsFile, { force: true });
|
|
274
|
+
const clock = (namespace) => {
|
|
275
|
+
let value = clocks.get(namespace);
|
|
276
|
+
if (!value) {
|
|
277
|
+
value = createClock();
|
|
278
|
+
clocks.set(namespace, value);
|
|
279
|
+
}
|
|
280
|
+
return value;
|
|
281
|
+
};
|
|
282
|
+
const select = (body) => {
|
|
283
|
+
if (body.services === void 0)
|
|
284
|
+
return children;
|
|
285
|
+
if (!Array.isArray(body.services) || body.services.length === 0)
|
|
286
|
+
throw new Error("services must be a nonempty list");
|
|
287
|
+
const selected = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const name of body.services) {
|
|
289
|
+
if (typeof name !== "string" || !children.has(name))
|
|
290
|
+
throw new Error("unknown service in selection");
|
|
291
|
+
selected.set(name, children.get(name));
|
|
292
|
+
}
|
|
293
|
+
return selected;
|
|
294
|
+
};
|
|
295
|
+
const diagnostics = (namespace) => Object.fromEntries([...children].map(([name, child]) => [name, child.diagnostics(namespace)]));
|
|
296
|
+
const transaction = async (namespace, selected, run) => {
|
|
297
|
+
if (controlling)
|
|
298
|
+
return json(409, { error: "another fleet operation is in progress" });
|
|
299
|
+
controlling = true;
|
|
300
|
+
const release = [];
|
|
301
|
+
const before = /* @__PURE__ */ new Map();
|
|
302
|
+
const results = {};
|
|
303
|
+
let failure;
|
|
304
|
+
try {
|
|
305
|
+
for (const [name, child] of selected) {
|
|
306
|
+
failure = name;
|
|
307
|
+
release.push(child.lock(namespace));
|
|
308
|
+
}
|
|
309
|
+
const busy = Object.fromEntries([...selected].filter(([, child]) => {
|
|
310
|
+
const state = child.diagnostics(namespace);
|
|
311
|
+
return state.activeRequests + state.blockedCommands + state.pendingWebhooks + (state.activeJobs ?? 0) > 0;
|
|
312
|
+
}).map(([name, child]) => [name, child.diagnostics(namespace)]));
|
|
313
|
+
if (Object.keys(busy).length)
|
|
314
|
+
return json(409, { error: "namespace is not quiescent", services: busy });
|
|
315
|
+
for (const [name, child] of selected) {
|
|
316
|
+
failure = name;
|
|
317
|
+
before.set(name, await child.checkpoint(namespace));
|
|
318
|
+
}
|
|
319
|
+
for (const [name, child] of selected) {
|
|
320
|
+
failure = name;
|
|
321
|
+
results[name] = { status: "ok", result: await run(name, child) };
|
|
322
|
+
}
|
|
323
|
+
return json(200, { status: "ok", namespace, services: results });
|
|
324
|
+
} catch {
|
|
325
|
+
const rollback = {};
|
|
326
|
+
for (const [name, point] of before) {
|
|
327
|
+
try {
|
|
328
|
+
await selected.get(name)?.restore(namespace, point);
|
|
329
|
+
rollback[name] = "restored";
|
|
330
|
+
} catch {
|
|
331
|
+
rollback[name] = "failed";
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return json(500, {
|
|
335
|
+
status: "failed",
|
|
336
|
+
namespace,
|
|
337
|
+
failedService: failure,
|
|
338
|
+
services: results,
|
|
339
|
+
rollback,
|
|
340
|
+
rolledBack: before.size > 0 && Object.values(rollback).every((value) => value === "restored")
|
|
341
|
+
});
|
|
342
|
+
} finally {
|
|
343
|
+
for (const unlock of release.reverse())
|
|
344
|
+
unlock();
|
|
345
|
+
controlling = false;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
const api = {
|
|
349
|
+
async fetch(request) {
|
|
350
|
+
const url = new URL(request.url);
|
|
351
|
+
const adminPrefix = resolveAdminPrefix(config.adminPrefix);
|
|
352
|
+
const path = url.pathname.startsWith(`${adminPrefix}/`) ? url.pathname.slice(adminPrefix.length) : "";
|
|
353
|
+
if (path === "/health" && request.method === "GET") {
|
|
354
|
+
const states = await Promise.all([...children].map(async ([name, child]) => {
|
|
355
|
+
let protocolReady = false;
|
|
356
|
+
try {
|
|
357
|
+
protocolReady = await child.ready();
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
return [
|
|
361
|
+
name,
|
|
362
|
+
{
|
|
363
|
+
processReady: !stopped,
|
|
364
|
+
protocolReady,
|
|
365
|
+
status: protocolReady ? "ready" : "failed",
|
|
366
|
+
url: publicUrl(child.url)
|
|
367
|
+
}
|
|
368
|
+
];
|
|
369
|
+
}));
|
|
370
|
+
const ready = !stopped && states.every(([, state]) => state.protocolReady);
|
|
371
|
+
return json(ready ? 200 : 503, {
|
|
372
|
+
status: ready ? "ready" : "failed",
|
|
373
|
+
id,
|
|
374
|
+
services: Object.fromEntries(states)
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (!path.startsWith("/fleet/"))
|
|
378
|
+
return json(404, { error: "not found" });
|
|
379
|
+
const adminKey = config.adminKey ?? process.env.MOCKINGBIRD_ADMIN_KEY;
|
|
380
|
+
if (adminKey !== void 0 && request.headers.get("x-mockingbird-admin-key") !== adminKey)
|
|
381
|
+
return json(401, { error: "admin key required" });
|
|
382
|
+
const route = /^\/fleet\/namespaces\/([^/]+)\/(reset|snapshots)(?:\/([^/]+)\/restore)?$/.exec(path);
|
|
383
|
+
const namespace = route ? decodeURIComponent(route[1]) : url.searchParams.get("namespace") ?? request.headers.get("x-mockingbird-namespace") ?? config.namespace ?? "default";
|
|
384
|
+
if (!NS.test(namespace))
|
|
385
|
+
return json(400, { error: "invalid namespace" });
|
|
386
|
+
if (request.method === "GET" && path === "/fleet/metrics")
|
|
387
|
+
return json(200, { namespace, services: diagnostics(namespace) });
|
|
388
|
+
if (request.method === "GET" && path === "/fleet/clock")
|
|
389
|
+
return json(200, {
|
|
390
|
+
namespace,
|
|
391
|
+
...clock(namespace).state(),
|
|
392
|
+
services: Object.fromEntries([...children].map(([name, child]) => [name, child.clock(namespace).state()]))
|
|
393
|
+
});
|
|
394
|
+
if (request.method !== "POST")
|
|
395
|
+
return json(405, { error: "method not allowed" });
|
|
396
|
+
let body;
|
|
397
|
+
try {
|
|
398
|
+
const text2 = await request.text();
|
|
399
|
+
const value = text2 ? JSON.parse(text2) : {};
|
|
400
|
+
if (!isObject(value))
|
|
401
|
+
throw new Error("expected object");
|
|
402
|
+
body = value;
|
|
403
|
+
} catch {
|
|
404
|
+
return json(400, { error: "expected JSON object" });
|
|
405
|
+
}
|
|
406
|
+
let selected;
|
|
407
|
+
try {
|
|
408
|
+
selected = select(body);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
return json(400, { error: error instanceof Error ? error.message : "bad selection" });
|
|
411
|
+
}
|
|
412
|
+
if (path === "/fleet/wait-until-idle") {
|
|
413
|
+
const services = diagnostics(namespace);
|
|
414
|
+
const idle = Object.values(services).every((d) => pending(d) === 0);
|
|
415
|
+
return json(idle ? 200 : 409, { status: idle ? "idle" : "busy", namespace, services });
|
|
416
|
+
}
|
|
417
|
+
if (path === "/fleet/clock") {
|
|
418
|
+
const current = clock(namespace);
|
|
419
|
+
let target = current.now();
|
|
420
|
+
if (body.set !== void 0) {
|
|
421
|
+
target = typeof body.set === "number" ? body.set : typeof body.set === "string" ? Date.parse(body.set) : NaN;
|
|
422
|
+
if (!Number.isFinite(target))
|
|
423
|
+
return json(400, { error: "set must be epoch ms or ISO-8601" });
|
|
424
|
+
}
|
|
425
|
+
if (body.advance !== void 0) {
|
|
426
|
+
const delta = parseDuration(body.advance);
|
|
427
|
+
if (delta === void 0)
|
|
428
|
+
return json(400, { error: "invalid advance" });
|
|
429
|
+
target += delta;
|
|
430
|
+
}
|
|
431
|
+
const frozen = body.freeze === void 0 ? current.state().frozen : body.freeze;
|
|
432
|
+
if (typeof frozen !== "boolean")
|
|
433
|
+
return json(400, { error: "freeze must be boolean" });
|
|
434
|
+
const response2 = await transaction(namespace, selected, async (_name, child) => {
|
|
435
|
+
const childClock = child.clock(namespace);
|
|
436
|
+
childClock.freeze();
|
|
437
|
+
childClock.set(target);
|
|
438
|
+
if (!frozen)
|
|
439
|
+
childClock.unfreeze();
|
|
440
|
+
await child.settle?.(namespace);
|
|
441
|
+
return childClock.state();
|
|
442
|
+
});
|
|
443
|
+
if (response2.ok) {
|
|
444
|
+
current.freeze();
|
|
445
|
+
current.set(target);
|
|
446
|
+
if (!frozen)
|
|
447
|
+
current.unfreeze();
|
|
448
|
+
}
|
|
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
|
+
const current = clock(namespace);
|
|
469
|
+
current.freeze();
|
|
470
|
+
current.set(point.clock.now);
|
|
471
|
+
if (!point.clock.frozen)
|
|
472
|
+
current.unfreeze();
|
|
473
|
+
}
|
|
474
|
+
return response2;
|
|
475
|
+
}
|
|
476
|
+
const snapshotId = randomUUID();
|
|
477
|
+
const values = /* @__PURE__ */ new Map();
|
|
478
|
+
const response = await transaction(namespace, selected, async (name, child) => {
|
|
479
|
+
values.set(name, await child.checkpoint(namespace));
|
|
480
|
+
return { checkpoint: `${snapshotId}:${name}` };
|
|
481
|
+
});
|
|
482
|
+
if (!response.ok)
|
|
483
|
+
return response;
|
|
484
|
+
checkpoints.set(snapshotId, { namespace, values, clock: clock(namespace).state() });
|
|
485
|
+
return json(201, {
|
|
486
|
+
...await response.json(),
|
|
487
|
+
id: snapshotId,
|
|
488
|
+
label: body.label
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
try {
|
|
493
|
+
const adminPrefix = resolveAdminPrefix(config.adminPrefix);
|
|
494
|
+
if (!isObject(config.services) || Object.keys(config.services).length === 0)
|
|
495
|
+
throw new Error("config must contain at least one service");
|
|
496
|
+
for (const [name, entry] of Object.entries(config.services)) {
|
|
497
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
|
|
498
|
+
throw new Error("invalid service name");
|
|
499
|
+
try {
|
|
500
|
+
const target = await options.load(name);
|
|
501
|
+
if (entry.protocol && entry.protocol !== ("protocol" in target ? target.protocol : "http"))
|
|
502
|
+
throw new Error("configured protocol does not match target");
|
|
503
|
+
const adminKey = entry.adminKey ?? config.adminKey ?? process.env.MOCKINGBIRD_ADMIN_KEY;
|
|
504
|
+
const child = "start" in target ? await target.start(entry) : await httpChild(target, {
|
|
505
|
+
...entry,
|
|
506
|
+
adminPrefix: resolveAdminPrefix(entry.adminPrefix ?? adminPrefix),
|
|
507
|
+
...adminKey !== void 0 ? { adminKey } : {}
|
|
508
|
+
}, options);
|
|
509
|
+
children.set(name, child);
|
|
510
|
+
if (!await child.ready())
|
|
511
|
+
throw new Error("protocol readiness failed");
|
|
512
|
+
} catch {
|
|
513
|
+
throw new Error(`service ${name} failed to start; all started listeners were closed`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
supervisor = await listen(api, {
|
|
517
|
+
port: config.control?.port ?? 0,
|
|
518
|
+
host: config.control?.host ?? "127.0.0.1"
|
|
519
|
+
});
|
|
520
|
+
const manifest = {
|
|
521
|
+
version: 1,
|
|
522
|
+
state: "ready",
|
|
523
|
+
pid: process.pid,
|
|
524
|
+
id,
|
|
525
|
+
startedAt: startedAt2,
|
|
526
|
+
adminBase: `${supervisor.url}${adminPrefix}/fleet`,
|
|
527
|
+
healthUrl: `${supervisor.url}${adminPrefix}/health`,
|
|
528
|
+
services: Object.fromEntries([...children].map(([name, child]) => [
|
|
529
|
+
name,
|
|
530
|
+
{
|
|
531
|
+
protocol: child.protocol,
|
|
532
|
+
url: publicUrl(child.url),
|
|
533
|
+
healthUrl: child.healthUrl ?? `${supervisor?.url}${adminPrefix}/health`,
|
|
534
|
+
adminUrl: child.adminUrl ?? `${supervisor?.url}${adminPrefix}/fleet`,
|
|
535
|
+
namespaces: child.namespaces,
|
|
536
|
+
processReady: true,
|
|
537
|
+
protocolReady: true
|
|
538
|
+
}
|
|
539
|
+
]))
|
|
540
|
+
};
|
|
541
|
+
if (options.connectionsFile)
|
|
542
|
+
await atomicWrite(options.connectionsFile, {
|
|
543
|
+
version: 1,
|
|
544
|
+
id,
|
|
545
|
+
pid: process.pid,
|
|
546
|
+
services: Object.fromEntries([...children].map(([name, child]) => [
|
|
547
|
+
name,
|
|
548
|
+
{ url: child.connection ?? child.url, namespaces: child.connections }
|
|
549
|
+
]))
|
|
550
|
+
});
|
|
551
|
+
if (options.readyFile)
|
|
552
|
+
await atomicWrite(options.readyFile, manifest);
|
|
553
|
+
options.onReady?.(manifest);
|
|
554
|
+
return { manifest, children, fetch: api.fetch, close };
|
|
555
|
+
} catch (error) {
|
|
556
|
+
await close();
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
115
561
|
// ../../adapters/node/dist/cli.js
|
|
116
562
|
var optionHelp = (options) => Object.entries(options).map(([name, option]) => {
|
|
117
563
|
const flag = `--${name}${option.type === "string" ? ` ${option.value ?? "<value>"}` : ""}`;
|
|
@@ -181,6 +627,11 @@ ${commandHelp(spec, name, command)}`);
|
|
|
181
627
|
var COMMON_SERVE_OPTIONS = {
|
|
182
628
|
port: { type: "string", value: "<port>", description: "Port to listen on" },
|
|
183
629
|
host: { type: "string", value: "<host>", description: "Interface to bind", default: "127.0.0.1" },
|
|
630
|
+
"admin-prefix": {
|
|
631
|
+
type: "string",
|
|
632
|
+
value: "<path>",
|
|
633
|
+
description: "Internal API prefix (default /__admin; env MOCKINGBIRD_ADMIN_PREFIX)"
|
|
634
|
+
},
|
|
184
635
|
"admin-key": {
|
|
185
636
|
type: "string",
|
|
186
637
|
value: "<key>",
|
|
@@ -201,6 +652,20 @@ var COMMON_SERVE_OPTIONS = {
|
|
|
201
652
|
type: "string",
|
|
202
653
|
value: "<file>",
|
|
203
654
|
description: "Serve every service in a mockingbird.json config instead"
|
|
655
|
+
},
|
|
656
|
+
"ready-file": {
|
|
657
|
+
type: "string",
|
|
658
|
+
description: "Atomically publish fleet endpoint JSON",
|
|
659
|
+
value: "<file>"
|
|
660
|
+
},
|
|
661
|
+
"connections-file": {
|
|
662
|
+
type: "string",
|
|
663
|
+
description: "Write private connection URLs with mode 0600",
|
|
664
|
+
value: "<file>"
|
|
665
|
+
},
|
|
666
|
+
"ready-json": {
|
|
667
|
+
type: "boolean",
|
|
668
|
+
description: "Emit one public JSON record after fleet readiness"
|
|
204
669
|
}
|
|
205
670
|
};
|
|
206
671
|
var formatLog = (format) => {
|
|
@@ -224,7 +689,13 @@ var loadTarget = async (name, own) => {
|
|
|
224
689
|
return own;
|
|
225
690
|
const specifier = `@crvouga/mockingbird-service-${name}/server`;
|
|
226
691
|
try {
|
|
227
|
-
|
|
692
|
+
let mod;
|
|
693
|
+
try {
|
|
694
|
+
mod = await import(specifier);
|
|
695
|
+
} catch {
|
|
696
|
+
const fromProject = createRequire(resolve(process.cwd(), "package.json"));
|
|
697
|
+
mod = await import(pathToFileURL(fromProject.resolve(specifier)).href);
|
|
698
|
+
}
|
|
228
699
|
if (!mod.serveTarget)
|
|
229
700
|
throw new Error(`${specifier} exports no serveTarget`);
|
|
230
701
|
return mod.serveTarget;
|
|
@@ -234,7 +705,9 @@ var loadTarget = async (name, own) => {
|
|
|
234
705
|
}
|
|
235
706
|
};
|
|
236
707
|
var start = async (target, values, config) => {
|
|
708
|
+
const adminPrefix = resolveAdminPrefix(config.adminPrefix);
|
|
237
709
|
const runtime = await target.create(values, {
|
|
710
|
+
adminPrefix,
|
|
238
711
|
adminKey: config.adminKey,
|
|
239
712
|
seed: config.seed,
|
|
240
713
|
onLog: formatLog(config.log)
|
|
@@ -242,24 +715,27 @@ var start = async (target, values, config) => {
|
|
|
242
715
|
const listening = await listen(runtime, { port: config.port, host: config.host });
|
|
243
716
|
target.listening?.(listening);
|
|
244
717
|
console.log(`${target.name} mock listening on ${listening.url}`);
|
|
245
|
-
console.log(`${target.name} health: GET ${listening.url}/health`);
|
|
246
|
-
console.log(`${target.name} admin: ${listening.url}
|
|
247
|
-
console.log(`${target.name} admin ui: ${listening.url}/
|
|
718
|
+
console.log(`${target.name} health: GET ${listening.url}${adminPrefix}/health`);
|
|
719
|
+
console.log(`${target.name} admin: ${listening.url}${adminPrefix} (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
|
|
720
|
+
console.log(`${target.name} admin ui: ${listening.url}${adminPrefix}/ui`);
|
|
248
721
|
for (const line of target.banner?.(runtime) ?? [])
|
|
249
|
-
console.log(`${target.name} ${line}`);
|
|
722
|
+
console.log(`${target.name} ${line.replaceAll("/__admin", adminPrefix)}`);
|
|
250
723
|
return listening;
|
|
251
724
|
};
|
|
252
|
-
var untilSignal = async (servers) => new Promise((
|
|
725
|
+
var untilSignal = async (servers) => new Promise((resolve2) => {
|
|
253
726
|
const stop = async () => {
|
|
727
|
+
;
|
|
728
|
+
process.removeListener("SIGINT", stop);
|
|
729
|
+
process.removeListener("SIGTERM", stop);
|
|
254
730
|
await Promise.allSettled(servers.map((s) => s.close()));
|
|
255
|
-
|
|
731
|
+
resolve2(0);
|
|
256
732
|
};
|
|
257
733
|
process.once("SIGINT", stop);
|
|
258
734
|
process.once("SIGTERM", stop);
|
|
259
735
|
});
|
|
260
736
|
var serveCommand = (target) => ({
|
|
261
|
-
summary: `Serve the ${target.name} mock
|
|
262
|
-
options: { ...COMMON_SERVE_OPTIONS, ...target.options },
|
|
737
|
+
summary: `Serve the ${target.name} mock or a configured fleet`,
|
|
738
|
+
options: { ...COMMON_SERVE_OPTIONS, ..."options" in target ? target.options : {} },
|
|
263
739
|
async run(values) {
|
|
264
740
|
const log = values["log-requests"] === true ? "json" : asString(values.log) ?? "pretty";
|
|
265
741
|
if (!["pretty", "json", "off"].includes(log)) {
|
|
@@ -268,25 +744,34 @@ var serveCommand = (target) => ({
|
|
|
268
744
|
}
|
|
269
745
|
const configPath = asString(values.config);
|
|
270
746
|
if (configPath !== void 0) {
|
|
271
|
-
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
272
|
-
const servers = [];
|
|
273
747
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
748
|
+
const config = JSON.parse(await readFile2(configPath, "utf8"));
|
|
749
|
+
config.adminPrefix = resolveAdminPrefix(config.adminPrefix ?? asString(values["admin-prefix"]) ?? process.env.MOCKINGBIRD_ADMIN_PREFIX);
|
|
750
|
+
const adminKey2 = config.adminKey ?? asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
|
|
751
|
+
if (adminKey2 !== void 0)
|
|
752
|
+
config.adminKey = adminKey2;
|
|
753
|
+
const fleet = await startFleet(config, {
|
|
754
|
+
load: (name) => loadTarget(name, target),
|
|
755
|
+
onLog: formatLog(config.log ?? log),
|
|
756
|
+
...asString(values["ready-file"]) ? { readyFile: asString(values["ready-file"]) } : {},
|
|
757
|
+
...asString(values["connections-file"]) ? { connectionsFile: asString(values["connections-file"]) } : {},
|
|
758
|
+
onReady: (manifest) => {
|
|
759
|
+
if (values["ready-json"])
|
|
760
|
+
console.log(JSON.stringify(manifest));
|
|
761
|
+
else
|
|
762
|
+
for (const [name, endpoint] of Object.entries(manifest.services))
|
|
763
|
+
console.log(`${name} mock listening on ${endpoint.url}`);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
return untilSignal([fleet]);
|
|
284
767
|
} catch (error) {
|
|
285
|
-
await Promise.allSettled(servers.map((s) => s.close()));
|
|
286
768
|
console.error(error instanceof Error ? error.message : String(error));
|
|
287
769
|
return 1;
|
|
288
770
|
}
|
|
289
|
-
|
|
771
|
+
}
|
|
772
|
+
if ("start" in target) {
|
|
773
|
+
console.error("Use --config to supervise protocol services with this command");
|
|
774
|
+
return 2;
|
|
290
775
|
}
|
|
291
776
|
const port = asString(values.port);
|
|
292
777
|
const adminKey = asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
|
|
@@ -294,6 +779,7 @@ var serveCommand = (target) => ({
|
|
|
294
779
|
let listening;
|
|
295
780
|
try {
|
|
296
781
|
listening = await start(target, values, {
|
|
782
|
+
adminPrefix: resolveAdminPrefix(asString(values["admin-prefix"]) ?? process.env.MOCKINGBIRD_ADMIN_PREFIX),
|
|
297
783
|
port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
|
|
298
784
|
host: asString(values.host) ?? "127.0.0.1",
|
|
299
785
|
...adminKey !== void 0 ? { adminKey } : {},
|
|
@@ -627,6 +1113,7 @@ var serveTarget = {
|
|
|
627
1113
|
...token ? { tokens: [token] } : {},
|
|
628
1114
|
...values["strict-channels"] === true ? { strictChannels: true } : {}
|
|
629
1115
|
},
|
|
1116
|
+
...common.adminPrefix !== void 0 ? { adminPrefix: common.adminPrefix } : {},
|
|
630
1117
|
...common.adminKey !== void 0 ? { adminKey: common.adminKey } : {},
|
|
631
1118
|
...common.seed !== void 0 ? { seed: common.seed } : {},
|
|
632
1119
|
...common.onLog ? { onLog: common.onLog } : {}
|
|
@@ -650,4 +1137,4 @@ export {
|
|
|
650
1137
|
createServer2 as createServer,
|
|
651
1138
|
serveTarget
|
|
652
1139
|
};
|
|
653
|
-
//# sourceMappingURL=chunk-
|
|
1140
|
+
//# sourceMappingURL=chunk-PVVAIER7.js.map
|