@crvouga/mockingbird-service-junction 0.3.0 → 0.5.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.
@@ -213,8 +213,21 @@ var createControlPlane = (context) => {
213
213
  },
214
214
  "GET /faults": () => json(200, { faults: context.faults.list() }),
215
215
  "POST /faults": ({ body, namespace }) => {
216
- if (!isRecord(body) || typeof body.status !== "number") {
217
- return adminError(400, "a fault needs a numeric status");
216
+ if (isRecord(body) && typeof body.preset === "string") {
217
+ if (!context.applyPreset)
218
+ return adminError(400, `${context.name} has no fault presets`);
219
+ const { preset, ...overrides } = body;
220
+ try {
221
+ return json(201, {
222
+ preset,
223
+ rules: context.applyPreset(preset, namespace, overrides)
224
+ });
225
+ } catch (error) {
226
+ return adminError(404, error instanceof Error ? error.message : String(error));
227
+ }
228
+ }
229
+ if (!isRecord(body) || typeof body.status !== "number" && typeof body.delayMs !== "number" && typeof body.latencyMs !== "number" && body.drop !== true && typeof body.effect !== "string") {
230
+ return adminError(400, "a fault needs a numeric status, a delayMs/latencyMs, drop: true, an effect, or a preset");
218
231
  }
219
232
  const rule = {
220
233
  // Scoped to the caller's namespace unless it asks for every one, so one worker's
@@ -333,6 +346,21 @@ var createControlPlane = (context) => {
333
346
  };
334
347
  };
335
348
 
349
+ // ../core/dist/credentials.js
350
+ var createCredentialRegistry = () => {
351
+ const map = /* @__PURE__ */ new Map();
352
+ return {
353
+ set: (credential, namespace) => {
354
+ map.set(credential, namespace);
355
+ },
356
+ get: (credential) => map.get(credential),
357
+ remove: (credential) => map.delete(credential),
358
+ clear: () => map.clear(),
359
+ entries: () => [...map].map(([credential, namespace]) => ({ credential, namespace })).sort((a, b) => a.credential.localeCompare(b.credential))
360
+ };
361
+ };
362
+ var maskCredential = (credential) => credential.length <= 8 ? `${credential.slice(0, 2)}\u2026` : `${credential.slice(0, 6)}\u2026${credential.slice(-2)}`;
363
+
336
364
  // ../core/dist/rng.js
337
365
  var seedFrom = (value) => {
338
366
  let hash = 2166136261;
@@ -377,11 +405,14 @@ var matches = (rule, candidate) => {
377
405
  return true;
378
406
  };
379
407
  var faultResponse = (rule) => {
408
+ const status = rule.status ?? 500;
380
409
  const headers = { "content-type": "application/json", ...rule.headers };
381
410
  if (typeof rule.body === "string")
382
- return new Response(rule.body, { status: rule.status, headers });
411
+ return new Response(rule.body, { status, headers });
412
+ if (rule.body === null)
413
+ return new Response(null, { status, headers: rule.headers ?? {} });
383
414
  const body = rule.body === void 0 ? { detail: "Injected by Mockingbird" } : rule.body;
384
- return new Response(JSON.stringify(body), { status: rule.status, headers });
415
+ return new Response(JSON.stringify(body), { status, headers });
385
416
  };
386
417
  var createFaultRegistry = (rng = createRng(0)) => {
387
418
  const entries = [];
@@ -407,6 +438,7 @@ var createFaultRegistry = (rng = createRng(0)) => {
407
438
  entries.length = 0;
408
439
  },
409
440
  async take(candidate) {
441
+ const hits = [];
410
442
  for (const entry of entries) {
411
443
  if (entry.remaining === 0)
412
444
  continue;
@@ -418,12 +450,23 @@ var createFaultRegistry = (rng = createRng(0)) => {
418
450
  entry.hits++;
419
451
  if (entry.remaining !== null)
420
452
  entry.remaining--;
421
- if (entry.rule.delayMs !== void 0 && entry.rule.delayMs > 0) {
422
- await new Promise((resolve) => setTimeout(resolve, entry.rule.delayMs));
453
+ const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
454
+ if (delay !== void 0 && delay > 0) {
455
+ await new Promise((resolve) => setTimeout(resolve, delay));
423
456
  }
424
- return { id: entry.rule.id, response: faultResponse(entry.rule) };
457
+ const hit = { id: entry.rule.id };
458
+ if (entry.rule.effect !== void 0) {
459
+ hit.effect = { name: entry.rule.effect, params: entry.rule.params ?? {} };
460
+ }
461
+ if (entry.rule.drop === true)
462
+ hit.drop = true;
463
+ else if (entry.rule.status !== void 0)
464
+ hit.response = faultResponse(entry.rule);
465
+ hits.push(hit);
466
+ if (hit.drop || hit.response)
467
+ break;
425
468
  }
426
- return void 0;
469
+ return hits;
427
470
  }
428
471
  };
429
472
  };
@@ -996,6 +1039,7 @@ var createService = (options) => {
996
1039
  sqlite: options.sqlite,
997
1040
  namespace: options.namespace,
998
1041
  operation,
1042
+ document: options.document,
999
1043
  now
1000
1044
  };
1001
1045
  const short = await options.before?.(context);
@@ -1038,12 +1082,123 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
1038
1082
  };
1039
1083
 
1040
1084
  // ../core/dist/version.js
1041
- var PACKAGE_VERSION = true ? "0.3.0" : UNRELEASED_VERSION;
1085
+ var PACKAGE_VERSION = true ? "0.5.0" : UNRELEASED_VERSION;
1086
+
1087
+ // ../core/dist/webhooks.js
1088
+ var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
1089
+ var adminError2 = (status, message) => json2(status, { error: { type: "mockingbird_admin", message } });
1090
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1091
+ var parseEndpoint = (value) => {
1092
+ if (!isRecord3(value) || typeof value.url !== "string")
1093
+ return "each endpoint needs a url";
1094
+ try {
1095
+ new URL(value.url);
1096
+ } catch {
1097
+ return `not a URL: ${value.url}`;
1098
+ }
1099
+ const endpoint = { url: value.url };
1100
+ if (typeof value.id === "string")
1101
+ endpoint.id = value.id;
1102
+ if (typeof value.secret === "string")
1103
+ endpoint.secret = value.secret;
1104
+ if (typeof value.signUrl === "string")
1105
+ endpoint.signUrl = value.signUrl;
1106
+ const events = value.events ?? value.enabledEvents;
1107
+ if (Array.isArray(events))
1108
+ endpoint.events = events.map(String);
1109
+ if (isRecord3(value.tags)) {
1110
+ endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
1111
+ }
1112
+ if (typeof value.account === "string")
1113
+ endpoint.tags = { ...endpoint.tags, account: value.account };
1114
+ if (isRecord3(value.headers)) {
1115
+ endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
1116
+ }
1117
+ return endpoint;
1118
+ };
1119
+ var webhookAdminRoutes = (hub) => ({
1120
+ "GET /webhooks": ({ url, namespace }) => json2(200, {
1121
+ deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
1122
+ const type = url.searchParams.get("type");
1123
+ return type === null || d.type === type;
1124
+ })
1125
+ }),
1126
+ "GET /webhooks/events": ({ url, namespace }) => {
1127
+ const type = url.searchParams.get("type");
1128
+ return json2(200, {
1129
+ events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
1130
+ });
1131
+ },
1132
+ "POST /webhooks/:id/replay": async ({ params }) => {
1133
+ const replayed = await hub.replay(params.id);
1134
+ return replayed ? json2(200, replayed) : adminError2(404, `no delivery ${params.id}`);
1135
+ },
1136
+ "POST /webhooks/flush": async () => {
1137
+ await hub.flush();
1138
+ return json2(200, { status: "ok" });
1139
+ },
1140
+ "POST /webhooks/faults": ({ body, namespace }) => {
1141
+ if (!isRecord3(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
1142
+ return adminError2(400, "mode must be duplicate, reorder or drop");
1143
+ }
1144
+ const fault = { mode: body.mode };
1145
+ if (typeof body.count === "number")
1146
+ fault.count = body.count;
1147
+ hub.fault(namespace, fault);
1148
+ return json2(201, { namespace, ...fault });
1149
+ },
1150
+ "GET /webhook-endpoints": ({ namespace }) => json2(200, {
1151
+ endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
1152
+ ...rest,
1153
+ secret: secret ? "(set)" : null
1154
+ }))
1155
+ }),
1156
+ "PUT /webhook-endpoints": ({ body, namespace }) => {
1157
+ const list = Array.isArray(body) ? body : isRecord3(body) ? body.endpoints : void 0;
1158
+ if (!Array.isArray(list))
1159
+ return adminError2(400, "expected [{url, secret?, events?}]");
1160
+ const parsed = [];
1161
+ for (const each of list) {
1162
+ const endpoint = parseEndpoint(each);
1163
+ if (typeof endpoint === "string")
1164
+ return adminError2(400, endpoint);
1165
+ parsed.push(endpoint);
1166
+ }
1167
+ const set = hub.setEndpoints(namespace, parsed);
1168
+ return json2(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
1169
+ },
1170
+ "DELETE /webhook-endpoints": ({ namespace }) => {
1171
+ hub.setEndpoints(namespace, []);
1172
+ return json2(200, { status: "ok" });
1173
+ }
1174
+ });
1175
+ var parsePayload = (message) => {
1176
+ if (message.contentType.startsWith("application/json")) {
1177
+ try {
1178
+ return JSON.parse(message.body);
1179
+ } catch {
1180
+ return message.body;
1181
+ }
1182
+ }
1183
+ if (message.contentType.startsWith("application/x-www-form-urlencoded")) {
1184
+ return Object.fromEntries(new URLSearchParams(message.body));
1185
+ }
1186
+ return message.body;
1187
+ };
1042
1188
 
1043
1189
  // ../core/dist/runtime.js
1044
1190
  var MOCKINGBIRD_HEADER = "x-mockingbird";
1045
1191
  var DEFAULT_NAMESPACE = "default";
1046
1192
  var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
1193
+ var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
1194
+ var effects = /* @__PURE__ */ new WeakMap();
1195
+ var DroppedConnectionError = class extends TypeError {
1196
+ code = "MOCKINGBIRD_DROP";
1197
+ constructor() {
1198
+ super("fetch failed: connection dropped by Mockingbird fault");
1199
+ this.name = "TypeError";
1200
+ }
1201
+ };
1047
1202
  var operationMatcher = (document2) => {
1048
1203
  const matchers = listOperations(document2).map((operation) => ({
1049
1204
  operationId: operation.operationId,
@@ -1062,6 +1217,7 @@ var createRuntime = (options) => {
1062
1217
  const journal = createJournal(options.journalSize ?? DEFAULT_JOURNAL_SIZE);
1063
1218
  const version = options.version ?? PACKAGE_VERSION;
1064
1219
  const instances = /* @__PURE__ */ new Map();
1220
+ const credentials = createCredentialRegistry();
1065
1221
  const operationIdFor = options.document ? operationMatcher(options.document) : () => void 0;
1066
1222
  const storageNamespace = (name) => name === DEFAULT_NAMESPACE ? options.name : `${options.name}:${name}`;
1067
1223
  const instance = (name = DEFAULT_NAMESPACE) => {
@@ -1083,10 +1239,12 @@ var createRuntime = (options) => {
1083
1239
  };
1084
1240
  const reset = async (name = DEFAULT_NAMESPACE) => {
1085
1241
  if (name === "*") {
1242
+ options.webhooks?.clear();
1086
1243
  for (const each of instances.values())
1087
1244
  await each.reset();
1088
1245
  return;
1089
1246
  }
1247
+ options.webhooks?.clear(name);
1090
1248
  const target = instances.get(name);
1091
1249
  if (target)
1092
1250
  await target.reset();
@@ -1109,13 +1267,57 @@ var createRuntime = (options) => {
1109
1267
  metrics,
1110
1268
  journal,
1111
1269
  rng,
1270
+ credentials,
1271
+ webhooks: options.webhooks,
1272
+ applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
1273
+ const preset = options.presets?.[name];
1274
+ if (!preset)
1275
+ throw new RangeError(`no fault preset ${JSON.stringify(name)}`);
1276
+ const added = (preset.rules ?? []).map((rule, index) => faults.add({
1277
+ namespace,
1278
+ ...rule,
1279
+ ...overrides,
1280
+ preset: name,
1281
+ id: `${overrides.id ?? name}${(preset.rules ?? []).length > 1 ? `_${index + 1}` : ""}`
1282
+ }));
1283
+ if (preset.webhook && options.webhooks) {
1284
+ options.webhooks.fault(namespace, {
1285
+ ...preset.webhook,
1286
+ ...overrides.count !== void 0 ? { count: overrides.count } : {}
1287
+ });
1288
+ }
1289
+ return added;
1290
+ },
1112
1291
  instance,
1113
1292
  namespaces: () => [...instances.keys()].sort(),
1114
1293
  reset,
1115
1294
  snapshot,
1116
1295
  restore,
1117
- fetch: async (request2) => {
1118
- const namespace = control.namespaceOf(request2);
1296
+ fetch: async (incoming) => {
1297
+ let request2 = incoming;
1298
+ const prefixed = PATH_PREFIX.exec(new URL(request2.url).pathname);
1299
+ if (prefixed) {
1300
+ const url2 = new URL(request2.url);
1301
+ url2.pathname = prefixed[2] ?? "/";
1302
+ const headers = new Headers(request2.headers);
1303
+ if (!headers.has(NAMESPACE_HEADER)) {
1304
+ headers.set(NAMESPACE_HEADER, decodeURIComponent(prefixed[1]));
1305
+ }
1306
+ const hasBody = request2.method !== "GET" && request2.method !== "HEAD";
1307
+ request2 = new Request(url2, {
1308
+ method: request2.method,
1309
+ headers,
1310
+ ...hasBody ? { body: await request2.arrayBuffer() } : {},
1311
+ signal: request2.signal
1312
+ });
1313
+ }
1314
+ let namespace = control.namespaceOf(request2);
1315
+ if (!request2.headers.has(NAMESPACE_HEADER) && options.credential) {
1316
+ const credential = options.credential(request2);
1317
+ const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
1318
+ if (mapped !== void 0)
1319
+ namespace = mapped;
1320
+ }
1119
1321
  const stamp = (response2) => {
1120
1322
  const value = NAMESPACE_PATTERN.test(namespace) ? `${options.name}@${version}; ns=${namespace}` : `${options.name}@${version}`;
1121
1323
  try {
@@ -1161,18 +1363,26 @@ var createRuntime = (options) => {
1161
1363
  }
1162
1364
  }), { status: 400, headers: { "content-type": "application/json" } }));
1163
1365
  }
1164
- const faulted = await faults.take({
1366
+ const hits = await faults.take({
1165
1367
  operationId,
1166
1368
  method: request2.method,
1167
1369
  path: url.pathname,
1168
1370
  namespace
1169
1371
  });
1170
- if (faulted) {
1171
- log(faulted.response.status, faulted.id);
1172
- return stamp(faulted.response);
1372
+ const final = hits.find((hit) => hit.drop || hit.response);
1373
+ if (final?.drop) {
1374
+ log(0, final.id);
1375
+ throw new DroppedConnectionError();
1173
1376
  }
1377
+ if (final?.response) {
1378
+ log(final.response.status, final.id);
1379
+ return stamp(final.response);
1380
+ }
1381
+ const fired = hits.filter((hit) => hit.effect !== void 0);
1382
+ if (fired.length > 0)
1383
+ effects.set(request2, fired.map((hit) => hit.effect));
1174
1384
  const response = await instance(namespace).fetch(request2);
1175
- log(response.status, void 0, response);
1385
+ log(response.status, fired[0]?.id, response);
1176
1386
  return stamp(response);
1177
1387
  }
1178
1388
  };
@@ -1189,11 +1399,83 @@ var createRuntime = (options) => {
1189
1399
  snapshot: (name) => snapshot(name),
1190
1400
  restore: (name, from) => restore(from, name),
1191
1401
  describe: options.describe ?? (() => ({})),
1192
- routes: options.admin?.(runtime) ?? {},
1402
+ ...options.presets ? {
1403
+ applyPreset: (name, namespace, overrides) => runtime.applyPreset(name, namespace, overrides)
1404
+ } : {},
1405
+ routes: {
1406
+ ...credentialRoutes(credentials),
1407
+ ...options.presets ? presetRoutes(options.presets, runtime) : {},
1408
+ ...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
1409
+ ...options.admin?.(runtime) ?? {}
1410
+ },
1193
1411
  adminKey: options.adminKey
1194
1412
  });
1195
1413
  return runtime;
1196
1414
  };
1415
+ var adminJson = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
1416
+ var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
1417
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1418
+ var credentialRoutes = (registry) => ({
1419
+ "GET /credentials": () => adminJson(200, {
1420
+ credentials: registry.entries().map(({ credential, namespace }) => ({
1421
+ credential: maskCredential(credential),
1422
+ namespace
1423
+ }))
1424
+ }),
1425
+ "PUT /credentials": ({ body, namespace }) => {
1426
+ const pairs = [];
1427
+ const list = Array.isArray(body) ? body : isObject(body) ? body.credentials : void 0;
1428
+ if (Array.isArray(list)) {
1429
+ for (const each of list) {
1430
+ if (typeof each === "string")
1431
+ pairs.push([each, namespace]);
1432
+ else if (isObject(each) && typeof each.credential === "string") {
1433
+ pairs.push([
1434
+ each.credential,
1435
+ typeof each.namespace === "string" ? each.namespace : namespace
1436
+ ]);
1437
+ } else
1438
+ return adminFail(400, "each entry is a credential string or {credential, namespace}");
1439
+ }
1440
+ } else if (isObject(list)) {
1441
+ for (const [credential, target] of Object.entries(list)) {
1442
+ if (typeof target !== "string")
1443
+ return adminFail(400, `namespace for ${credential} must be a string`);
1444
+ pairs.push([credential, target]);
1445
+ }
1446
+ } else if (isObject(body) && typeof body.credential === "string") {
1447
+ pairs.push([body.credential, typeof body.namespace === "string" ? body.namespace : namespace]);
1448
+ } else {
1449
+ return adminFail(400, 'expected {"credentials": {"<credential>": "<namespace>"}}');
1450
+ }
1451
+ for (const [credential, target] of pairs) {
1452
+ if (!NAMESPACE_PATTERN.test(target))
1453
+ return adminFail(400, `bad namespace ${JSON.stringify(target)}`);
1454
+ registry.set(credential, target);
1455
+ }
1456
+ return adminJson(200, { mapped: pairs.length });
1457
+ },
1458
+ "DELETE /credentials": ({ url }) => {
1459
+ const credential = url.searchParams.get("credential");
1460
+ if (credential === null)
1461
+ registry.clear();
1462
+ else
1463
+ registry.remove(credential);
1464
+ return adminJson(200, { status: "ok" });
1465
+ }
1466
+ });
1467
+ var presetRoutes = (presets, runtime) => ({
1468
+ "GET /faults/presets": () => adminJson(200, {
1469
+ presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
1470
+ }),
1471
+ "POST /faults/presets/:name": ({ params, body, namespace }) => {
1472
+ const name = params.name;
1473
+ if (!presets[name])
1474
+ return adminFail(404, `no fault preset ${name}; GET /__admin/faults/presets`);
1475
+ const overrides = isObject(body) ? body : {};
1476
+ return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
1477
+ }
1478
+ });
1197
1479
 
1198
1480
  // src/catalog.ts
1199
1481
  var LAB_TEST_CATALOG = [
@@ -2626,7 +2908,7 @@ var renderLabAccount = (entry) => ({
2626
2908
  ),
2627
2909
  team_id_allowlist: [...entry.team_id_allowlist]
2628
2910
  });
2629
- var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2911
+ var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2630
2912
  var DELEGATED_FLOWS = [
2631
2913
  "order_delegated",
2632
2914
  "result_delegated",
@@ -2642,7 +2924,7 @@ var checkStates = (value, where) => {
2642
2924
  };
2643
2925
  var stringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string") ? [...value] : void 0;
2644
2926
  var labAccountFromInput = (input, teamId = MOCK_TEAM_ID) => {
2645
- if (!isRecord3(input)) throw new TypeError("lab account must be an object");
2927
+ if (!isRecord4(input)) throw new TypeError("lab account must be an object");
2646
2928
  if (typeof input.id !== "string" || input.id === "")
2647
2929
  throw new TypeError("lab account needs a non-empty id");
2648
2930
  const where = `lab account ${input.id}`;
@@ -2654,7 +2936,7 @@ var labAccountFromInput = (input, teamId = MOCK_TEAM_ID) => {
2654
2936
  throw new TypeError(`${where}: delegated_flow must be one of ${DELEGATED_FLOWS.join(", ")}`);
2655
2937
  if (input.states !== void 0) checkStates(input.states, `${where}: states`);
2656
2938
  if (input.allowed_billing !== void 0) {
2657
- if (!isRecord3(input.allowed_billing))
2939
+ if (!isRecord4(input.allowed_billing))
2658
2940
  throw new TypeError(`${where}: allowed_billing must map billing types to state lists`);
2659
2941
  for (const [type, states] of Object.entries(input.allowed_billing)) {
2660
2942
  if (!BILLING_TYPES.includes(type))
@@ -2693,7 +2975,7 @@ var labAccountFromInput = (input, teamId = MOCK_TEAM_ID) => {
2693
2975
  };
2694
2976
  };
2695
2977
  var patchLabAccount = (existing, patch, teamId) => {
2696
- if (!isRecord3(patch)) throw new TypeError("lab account patch must be an object");
2978
+ if (!isRecord4(patch)) throw new TypeError("lab account patch must be an object");
2697
2979
  if (patch.id !== void 0 && patch.id !== existing.id)
2698
2980
  throw new TypeError(`lab account ${existing.id}: id cannot be changed`);
2699
2981
  const { states, ...rest } = patch;
@@ -2713,7 +2995,7 @@ var patchLabAccount = (existing, patch, teamId) => {
2713
2995
  var labAccountFromClientFacing = (value, recordedTeamId) => {
2714
2996
  if (typeof value.id !== "string" || typeof value.lab !== "string") return void 0;
2715
2997
  const billing = {};
2716
- if (isRecord3(value.allowed_billing)) {
2998
+ if (isRecord4(value.allowed_billing)) {
2717
2999
  for (const [type, states] of Object.entries(value.allowed_billing)) {
2718
3000
  const list = stringList(states);
2719
3001
  if (list) billing[type] = list;
@@ -2967,18 +3249,18 @@ var resolveTransition = (method, target) => {
2967
3249
  error: `${JSON.stringify(target)} is not a ${method} status. Use one of: ${names.join(", ")}, or a full status such as ${allowed.slice(0, 2).map((s) => `${s.split(".")[0]}.${method}.${s.split(".")[1]}`).join(", ")}`
2968
3250
  };
2969
3251
  };
2970
- var adminError2 = (status, message) => jsonRes(status, { error: { type: "mockingbird_admin", message } });
2971
- var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3252
+ var adminError3 = (status, message) => jsonRes(status, { error: { type: "mockingbird_admin", message } });
3253
+ var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2972
3254
  var guarded = (run) => {
2973
3255
  try {
2974
3256
  return run();
2975
3257
  } catch (error) {
2976
- if (error instanceof TypeError) return adminError2(400, error.message);
3258
+ if (error instanceof TypeError) return adminError3(400, error.message);
2977
3259
  if (error instanceof Error && error.name === "LabAccountConflict") {
2978
- return adminError2(409, error.message);
3260
+ return adminError3(409, error.message);
2979
3261
  }
2980
3262
  const { status, message } = fixtureErrorMessage(error);
2981
- return adminError2(status, message);
3263
+ return adminError3(status, message);
2982
3264
  }
2983
3265
  };
2984
3266
  var junctionAdminRoutes = (options) => (runtime) => {
@@ -2994,20 +3276,20 @@ var junctionAdminRoutes = (options) => (runtime) => {
2994
3276
  }))
2995
3277
  }),
2996
3278
  "POST /orders/:id/transition": (request2) => {
2997
- const body = isRecord4(request2.body) ? request2.body : {};
3279
+ const body = isRecord5(request2.body) ? request2.body : {};
2998
3280
  const order = api(request2).order(request2.params.id);
2999
- if (!order) return adminError2(404, `no order ${request2.params.id} in ${request2.namespace}`);
3281
+ if (!order) return adminError3(404, `no order ${request2.params.id} in ${request2.namespace}`);
3000
3282
  if (typeof body.to !== "string") {
3001
- return adminError2(400, 'body needs "to": a status like "completed" or "at_lab"');
3283
+ return adminError3(400, 'body needs "to": a status like "completed" or "at_lab"');
3002
3284
  }
3003
3285
  const method = methodOf(order);
3004
3286
  const resolved = resolveTransition(method, body.to);
3005
- if ("error" in resolved) return adminError2(400, resolved.error);
3006
- let flags = isRecord4(body.flags) ? body.flags : null;
3287
+ if ("error" in resolved) return adminError3(400, resolved.error);
3288
+ let flags = isRecord5(body.flags) ? body.flags : null;
3007
3289
  if (typeof body.result === "string") {
3008
3290
  const named = RESULT_FIXTURES[body.result];
3009
3291
  if (!named) {
3010
- return adminError2(
3292
+ return adminError3(
3011
3293
  400,
3012
3294
  `no result fixture ${body.result}; one of ${Object.keys(RESULT_FIXTURES).join(", ")}`
3013
3295
  );
@@ -3027,23 +3309,23 @@ var junctionAdminRoutes = (options) => (runtime) => {
3027
3309
  "GET /result-fixtures": () => jsonRes(200, { fixtures: RESULT_FIXTURES }),
3028
3310
  "GET /results/:id": (request2) => {
3029
3311
  const fixture = api(request2).resultFixture(request2.params.id);
3030
- return fixture ? jsonRes(200, fixture) : adminError2(404, "no result fixture for that order");
3312
+ return fixture ? jsonRes(200, fixture) : adminError3(404, "no result fixture for that order");
3031
3313
  },
3032
3314
  "PUT /results/:id": (request2) => {
3033
- if (!isRecord4(request2.body)) return adminError2(400, "expected a result fixture object");
3315
+ if (!isRecord5(request2.body)) return adminError3(400, "expected a result fixture object");
3034
3316
  const fixture = { name: "custom", ...request2.body };
3035
3317
  if (fixture.results !== void 0 && !Array.isArray(fixture.results)) {
3036
- return adminError2(400, "results must be an array of result lines");
3318
+ return adminError3(400, "results must be an array of result lines");
3037
3319
  }
3038
- return api(request2).installResultFixture(request2.params.id, fixture) ? jsonRes(200, fixture) : adminError2(404, `no order ${request2.params.id} in ${request2.namespace}`);
3320
+ return api(request2).installResultFixture(request2.params.id, fixture) ? jsonRes(200, fixture) : adminError3(404, `no order ${request2.params.id} in ${request2.namespace}`);
3039
3321
  },
3040
3322
  "GET /lab-accounts": (request2) => jsonRes(200, { data: api(request2).labAccounts() }),
3041
3323
  "PUT /lab-accounts": (request2) => {
3042
- const body = isRecord4(request2.body) ? request2.body : {};
3324
+ const body = isRecord5(request2.body) ? request2.body : {};
3043
3325
  const accounts = body.accounts;
3044
3326
  const presets = body.presets;
3045
3327
  if (accounts === void 0 && presets === void 0 || accounts !== void 0 && accounts !== null && !Array.isArray(accounts) || presets !== void 0 && !Array.isArray(presets)) {
3046
- return adminError2(
3328
+ return adminError3(
3047
3329
  400,
3048
3330
  'body needs "accounts": an array (or null to restore the default), and/or "presets": an array of preset names'
3049
3331
  );
@@ -3055,20 +3337,20 @@ var junctionAdminRoutes = (options) => (runtime) => {
3055
3337
  return guarded(() => jsonRes(200, { data: api(request2).configureLabAccounts(layout) }));
3056
3338
  },
3057
3339
  "POST /lab-accounts": (request2) => {
3058
- if (!isRecord4(request2.body)) return adminError2(400, "expected one lab account object");
3340
+ if (!isRecord5(request2.body)) return adminError3(400, "expected one lab account object");
3059
3341
  const input = request2.body;
3060
3342
  return guarded(() => jsonRes(201, api(request2).addLabAccount(input)));
3061
3343
  },
3062
3344
  "PATCH /lab-accounts/:id": (request2) => {
3063
- if (!isRecord4(request2.body))
3064
- return adminError2(400, "expected an object of fields to change");
3345
+ if (!isRecord5(request2.body))
3346
+ return adminError3(400, "expected an object of fields to change");
3065
3347
  const patch = request2.body;
3066
3348
  return guarded(() => {
3067
3349
  const updated = api(request2).patchLabAccount(request2.params.id, patch);
3068
- return updated ? jsonRes(200, updated) : adminError2(404, `no lab account ${request2.params.id} in ${request2.namespace}`);
3350
+ return updated ? jsonRes(200, updated) : adminError3(404, `no lab account ${request2.params.id} in ${request2.namespace}`);
3069
3351
  });
3070
3352
  },
3071
- "DELETE /lab-accounts/:id": (request2) => api(request2).removeLabAccount(request2.params.id) ? jsonRes(200, { status: "ok" }) : adminError2(404, `no lab account ${request2.params.id} in ${request2.namespace}`),
3353
+ "DELETE /lab-accounts/:id": (request2) => api(request2).removeLabAccount(request2.params.id) ? jsonRes(200, { status: "ok" }) : adminError3(404, `no lab account ${request2.params.id} in ${request2.namespace}`),
3072
3354
  "GET /lab-accounts/presets": (request2) => jsonRes(200, {
3073
3355
  presets: Object.fromEntries(
3074
3356
  Object.keys(LAB_ACCOUNT_PRESETS).map((name) => {
@@ -3078,14 +3360,14 @@ var junctionAdminRoutes = (options) => (runtime) => {
3078
3360
  )
3079
3361
  }),
3080
3362
  "POST /lab-accounts/presets/:name": (request2) => {
3081
- const body = isRecord4(request2.body) ? request2.body : {};
3363
+ const body = isRecord5(request2.body) ? request2.body : {};
3082
3364
  if (body.id !== void 0 && typeof body.id !== "string") {
3083
- return adminError2(400, "id must be a string");
3365
+ return adminError3(400, "id must be a string");
3084
3366
  }
3085
3367
  const name = request2.params.name;
3086
3368
  return guarded(() => {
3087
3369
  const record = api(request2).addLabAccountPreset(name, body.id);
3088
- return record ? jsonRes(201, record) : adminError2(
3370
+ return record ? jsonRes(201, record) : adminError3(
3089
3371
  404,
3090
3372
  `no lab-account preset ${name}; one of ${Object.keys(LAB_ACCOUNT_PRESETS).join(", ")}`
3091
3373
  );
@@ -3094,34 +3376,34 @@ var junctionAdminRoutes = (options) => (runtime) => {
3094
3376
  "GET /team": (request2) => jsonRes(200, { teamId: api(request2).teamId }),
3095
3377
  "GET /limits": (request2) => jsonRes(200, { limits: api(request2).limits }),
3096
3378
  "PUT /limits": (request2) => {
3097
- if (!isRecord4(request2.body)) return adminError2(400, "expected an object of limits");
3379
+ if (!isRecord5(request2.body)) return adminError3(400, "expected an object of limits");
3098
3380
  const input = request2.body;
3099
3381
  return guarded(() => jsonRes(200, { limits: api(request2).configureLimits(input) }));
3100
3382
  },
3101
3383
  "GET /identity": (request2) => jsonRes(200, { identity: api(request2).identity }),
3102
3384
  "PUT /identity": (request2) => {
3103
- const mode = isRecord4(request2.body) ? request2.body.mode : void 0;
3385
+ const mode = isRecord5(request2.body) ? request2.body.mode : void 0;
3104
3386
  if (!IDENTITY_MODES.includes(mode)) {
3105
- return adminError2(400, `body needs "mode": one of ${IDENTITY_MODES.join(", ")}`);
3387
+ return adminError3(400, `body needs "mode": one of ${IDENTITY_MODES.join(", ")}`);
3106
3388
  }
3107
3389
  api(request2).identity = mode;
3108
3390
  return jsonRes(200, { identity: mode });
3109
3391
  },
3110
3392
  "POST /users": (request2) => {
3111
- if (!isRecord4(request2.body)) return adminError2(400, "expected one user object");
3393
+ if (!isRecord5(request2.body)) return adminError3(400, "expected one user object");
3112
3394
  const input = request2.body;
3113
3395
  return guarded(() => jsonRes(201, api(request2).insertUsers([input])[0]));
3114
3396
  },
3115
3397
  "POST /users/bulk": (request2) => {
3116
- const users = isRecord4(request2.body) ? request2.body.users : void 0;
3117
- if (!Array.isArray(users)) return adminError2(400, 'body needs "users": an array');
3398
+ const users = isRecord5(request2.body) ? request2.body.users : void 0;
3399
+ if (!Array.isArray(users)) return adminError3(400, 'body needs "users": an array');
3118
3400
  return guarded(
3119
3401
  () => jsonRes(201, { users: api(request2).insertUsers(users) })
3120
3402
  );
3121
3403
  },
3122
- "DELETE /users/:id": (request2) => api(request2).hardDeleteUser(request2.params.id) ? jsonRes(200, { status: "ok" }) : adminError2(404, `no user ${request2.params.id} in ${request2.namespace}`),
3404
+ "DELETE /users/:id": (request2) => api(request2).hardDeleteUser(request2.params.id) ? jsonRes(200, { status: "ok" }) : adminError3(404, `no user ${request2.params.id} in ${request2.namespace}`),
3123
3405
  "POST /orders": (request2) => {
3124
- if (!isRecord4(request2.body)) return adminError2(400, "expected one order object");
3406
+ if (!isRecord5(request2.body)) return adminError3(400, "expected one order object");
3125
3407
  const { emitWebhooks, ...input } = request2.body;
3126
3408
  return guarded(
3127
3409
  () => jsonRes(
@@ -3133,8 +3415,8 @@ var junctionAdminRoutes = (options) => (runtime) => {
3133
3415
  );
3134
3416
  },
3135
3417
  "POST /import": (request2) => {
3136
- if (!isRecord4(request2.body)) {
3137
- return adminError2(400, 'expected { "users": [...], "orders": [...] }');
3418
+ if (!isRecord5(request2.body)) {
3419
+ return adminError3(400, 'expected { "users": [...], "orders": [...] }');
3138
3420
  }
3139
3421
  const { emitWebhooks, ...fixtures } = request2.body;
3140
3422
  return guarded(() => {
@@ -3149,25 +3431,25 @@ var junctionAdminRoutes = (options) => (runtime) => {
3149
3431
  geo: api(request2).geoMode
3150
3432
  }),
3151
3433
  "PUT /geo": (request2) => {
3152
- const mode = isRecord4(request2.body) ? request2.body.mode : void 0;
3434
+ const mode = isRecord5(request2.body) ? request2.body.mode : void 0;
3153
3435
  if (mode !== "corpus" && mode !== "synthetic") {
3154
- return adminError2(400, 'body needs "mode": "corpus" or "synthetic"');
3436
+ return adminError3(400, 'body needs "mode": "corpus" or "synthetic"');
3155
3437
  }
3156
3438
  api(request2).geoMode = mode;
3157
3439
  return jsonRes(200, { geo: mode });
3158
3440
  },
3159
3441
  "GET /webhooks/events": (request2) => jsonRes(200, { events: api(request2).webhookEvents() }),
3160
- "GET /webhooks": (request2) => options.webhooks ? jsonRes(200, { deliveries: options.webhooks.deliveries(request2.namespace) }) : adminError2(
3442
+ "GET /webhooks": (request2) => options.webhooks ? jsonRes(200, { deliveries: options.webhooks.deliveries(request2.namespace) }) : adminError3(
3161
3443
  409,
3162
3444
  "webhook delivery is off; serve with --webhook-url and --webhook-secret"
3163
3445
  ),
3164
3446
  "POST /webhooks/:id/replay": async (request2) => {
3165
- if (!options.webhooks) return adminError2(409, "webhook delivery is off");
3447
+ if (!options.webhooks) return adminError3(409, "webhook delivery is off");
3166
3448
  const delivery = await options.webhooks.replay(request2.params.id);
3167
- return delivery ? jsonRes(200, delivery) : adminError2(404, `no message ${request2.params.id}`);
3449
+ return delivery ? jsonRes(200, delivery) : adminError3(404, `no message ${request2.params.id}`);
3168
3450
  },
3169
3451
  "POST /webhooks/flush": async () => {
3170
- if (!options.webhooks) return adminError2(409, "webhook delivery is off");
3452
+ if (!options.webhooks) return adminError3(409, "webhook delivery is off");
3171
3453
  await options.webhooks.flush();
3172
3454
  return jsonRes(200, { status: "ok" });
3173
3455
  },
@@ -3175,12 +3457,12 @@ var junctionAdminRoutes = (options) => (runtime) => {
3175
3457
  "POST /faults/presets/:name": (request2) => {
3176
3458
  const preset = FAULT_PRESETS[request2.params.name];
3177
3459
  if (!preset) {
3178
- return adminError2(
3460
+ return adminError3(
3179
3461
  404,
3180
3462
  `no preset ${request2.params.name}; one of ${Object.keys(FAULT_PRESETS).join(", ")}`
3181
3463
  );
3182
3464
  }
3183
- const overrides = isRecord4(request2.body) ? request2.body : {};
3465
+ const overrides = isRecord5(request2.body) ? request2.body : {};
3184
3466
  const rule = runtime.faults.add({
3185
3467
  namespace: request2.namespace,
3186
3468
  ...preset,
@@ -5807,7 +6089,7 @@ var FixtureError = class extends Error {
5807
6089
  }
5808
6090
  status;
5809
6091
  };
5810
- var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6092
+ var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5811
6093
  var instant = (value, field, fallback) => {
5812
6094
  if (value === void 0) return fallback;
5813
6095
  if (typeof value !== "string" || Number.isNaN(Date.parse(value)))
@@ -5831,7 +6113,7 @@ var DEFAULT_PATIENT_ADDRESS = {
5831
6113
  country: "US"
5832
6114
  };
5833
6115
  var buildUser = (state, input, now) => {
5834
- if (!isRecord5(input)) throw new FixtureError(400, "a user fixture must be an object");
6116
+ if (!isRecord6(input)) throw new FixtureError(400, "a user fixture must be an object");
5835
6117
  if (typeof input.client_user_id !== "string" || input.client_user_id === "")
5836
6118
  throw new FixtureError(400, "client_user_id must be a non-empty string");
5837
6119
  if (input.user_id !== void 0 && (typeof input.user_id !== "string" || !isUuid3(input.user_id)))
@@ -5881,7 +6163,7 @@ var insertOrders = (state, inputs, now, options = {}) => {
5881
6163
  const prepared = [];
5882
6164
  const ids = /* @__PURE__ */ new Set();
5883
6165
  for (const input of inputs) {
5884
- if (!isRecord5(input)) throw new FixtureError(400, "an order fixture must be an object");
6166
+ if (!isRecord6(input)) throw new FixtureError(400, "an order fixture must be an object");
5885
6167
  if (typeof input.user_id !== "string") throw new FixtureError(400, "user_id is required");
5886
6168
  if (typeof input.lab_test_id !== "string")
5887
6169
  throw new FixtureError(400, "lab_test_id is required");
@@ -5951,7 +6233,7 @@ var insertOrders = (state, inputs, now, options = {}) => {
5951
6233
  return prepared.flatMap(({ order }) => state.orders.get(order.id) ?? []);
5952
6234
  };
5953
6235
  var importFixtures = (state, fixtures, now, options = {}) => {
5954
- if (!isRecord5(fixtures))
6236
+ if (!isRecord6(fixtures))
5955
6237
  throw new FixtureError(400, 'fixtures must be { "users": [...], "orders": [...] }');
5956
6238
  const { users = [], orders = [] } = fixtures;
5957
6239
  if (!Array.isArray(users)) throw new FixtureError(400, "users must be an array");
@@ -5976,7 +6258,7 @@ var requireUser = (state, userId, context, detail = "User does not exist on this
5976
6258
  var fixtureErrorMessage = (error) => {
5977
6259
  if (error instanceof FixtureError) return { status: error.status, message: error.message };
5978
6260
  if (error instanceof HttpError) {
5979
- const detail = isRecord5(error.body) ? error.body.detail : error.body;
6261
+ const detail = isRecord6(error.body) ? error.body.detail : error.body;
5980
6262
  return {
5981
6263
  status: error.status,
5982
6264
  message: typeof detail === "string" ? detail : JSON.stringify(detail)
@@ -8846,4 +9128,4 @@ export {
8846
9128
  JunctionAPI,
8847
9129
  LabAccountConflict
8848
9130
  };
8849
- //# sourceMappingURL=chunk-NFWUEPW4.js.map
9131
+ //# sourceMappingURL=chunk-ICINEQ7D.js.map