@crvouga/mockingbird-service-eventbridge 1.5.0 → 1.7.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 CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog — @crvouga/mockingbird-service-eventbridge
2
2
 
3
- ## 1.5.0 (2026-10-16)
3
+ ## 1.7.0 (2026-10-19)
4
+
5
+ ### Features
6
+
7
+ - resolve the agent-reported issue queue in one batch ([25c52b6](https://github.com/crvouga/mockingbird/commit/25c52b668a53ddb80b968419601a20abf33f956b))
8
+
9
+ ## 1.6.0 (2026-10-16)
10
+
11
+ ### Features
12
+
13
+ - resolve fleet and emulator issue batch ([26d2f8c](https://github.com/crvouga/mockingbird/commit/26d2f8cee99fefed93192204bac44fb1be35c1da))
14
+
15
+ ## 1.5.0 (2026-10-15)
4
16
 
5
17
  ### Features
6
18
 
@@ -4,20 +4,31 @@ import {
4
4
  createRuntime2,
5
5
  normalizeBootControls,
6
6
  normalizeControls,
7
+ normalizeIdBase,
7
8
  ownedTransport,
8
9
  parseDuration,
9
10
  parseFaultHeader,
10
11
  resolveAdminPrefix,
11
12
  restoreClock
12
- } from "./chunk-V3532EMQ.js";
13
+ } from "./chunk-S3JSAF35.js";
13
14
 
14
15
  // ../../adapters/node/dist/cli.js
15
16
  import { readFile as readFile5 } from "node:fs/promises";
16
- import { createRequire as createRequire3 } from "node:module";
17
- import { resolve as resolve3 } from "node:path";
18
- import { pathToFileURL } from "node:url";
19
17
  import { parseArgs } from "node:util";
20
18
 
19
+ // ../../adapters/node/dist/boot-settings.js
20
+ var checkSettings = (target, settings, label) => {
21
+ if (!target.parseSettings)
22
+ throw new Error(`${label}: ${target.name} takes no boot settings`);
23
+ try {
24
+ return target.parseSettings(settings);
25
+ } catch (error) {
26
+ throw new Error(`${label}: ${error instanceof Error ? error.message : String(error)}`, {
27
+ cause: error
28
+ });
29
+ }
30
+ };
31
+
21
32
  // ../../adapters/node/dist/fleet.js
22
33
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
23
34
  import { createHash as createHash4, randomUUID as randomUUID4 } from "node:crypto";
@@ -29,6 +40,31 @@ import { createHash as createHash3, randomBytes, randomUUID as randomUUID3 } fro
29
40
 
30
41
  // ../../adapters/node/dist/fleet-archive.js
31
42
  import { createHash } from "node:crypto";
43
+ function validateCompatibilityTags(tags) {
44
+ if (!tags || typeof tags !== "object" || Array.isArray(tags))
45
+ throw new Error("invalid archive compatibility tags");
46
+ const entries = Object.entries(tags);
47
+ if (entries.length > 32 || entries.some(([key, value]) => !/^[A-Za-z0-9_.-]{1,64}$/.test(key) || typeof value !== "string" || value.length < 1 || value.length > 256 || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)))
48
+ throw new Error("invalid archive compatibility tags");
49
+ }
50
+ function assertCompatibilityTags(tags, policy) {
51
+ if (tags !== void 0)
52
+ validateCompatibilityTags(tags);
53
+ if (!policy)
54
+ return;
55
+ validateCompatibilityTags(policy.tags);
56
+ if (policy.missing !== void 0 && !["allow", "reject"].includes(policy.missing))
57
+ throw new Error("invalid missing compatibility policy");
58
+ if (tags === void 0 && policy.missing === "allow")
59
+ return;
60
+ if (canonical(tags) !== canonical(policy.tags))
61
+ throw new Error("application archive compatibility mismatch");
62
+ }
63
+ function assertArchiveCompatibility(input, policy) {
64
+ const archive = validateArchive(input);
65
+ assertCompatibilityTags(archive.compatibility, policy);
66
+ return archive;
67
+ }
32
68
  var canonical = (value) => {
33
69
  if (Array.isArray(value))
34
70
  return `[${value.map(canonical).join(",")}]`;
@@ -48,6 +84,8 @@ function validateArchive(value) {
48
84
  const { sha256, ...content } = doc;
49
85
  if (doc.format !== "mockingbird.fleet/v1" || typeof doc.sourceId !== "string" || typeof doc.createdAt !== "string" || !doc.namespaces || typeof doc.namespaces !== "object" || Array.isArray(doc.namespaces) || sha256 !== digest(content))
50
86
  throw new Error("unsupported or corrupt fleet archive");
87
+ if (doc.compatibility !== void 0)
88
+ validateCompatibilityTags(doc.compatibility);
51
89
  for (const [namespace, entry] of Object.entries(doc.namespaces)) {
52
90
  if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace) || !entry || !Number.isFinite(entry.clock?.now) || typeof entry.clock?.frozen !== "boolean" || !entry.services || !Object.keys(entry.services).length)
53
91
  throw new Error("invalid archive namespace");
@@ -63,6 +101,7 @@ function inspectArchive(input) {
63
101
  format: archive.format,
64
102
  createdAt: archive.createdAt,
65
103
  sha256: archive.sha256,
104
+ ...archive.compatibility ? { compatibility: archive.compatibility } : {},
66
105
  namespaces: Object.fromEntries(Object.entries(archive.namespaces).map(([name, ns]) => [
67
106
  name,
68
107
  {
@@ -705,11 +744,28 @@ var ingressSupport = {
705
744
  streaming: true,
706
745
  signing: "vendor request signing is not used; webhook signatures remain payload based"
707
746
  },
747
+ cognito: {
748
+ publicOrigin: true,
749
+ pathPrefix: true,
750
+ namespaces: "path-or-header",
751
+ streaming: true,
752
+ signing: "JWT issuer and JWKS use the configured public namespace; SigV4 verification is not modelled"
753
+ },
754
+ medplum: {
755
+ publicOrigin: true,
756
+ pathPrefix: true,
757
+ namespaces: "path-or-header",
758
+ streaming: true,
759
+ signing: "FHIR, OAuth and storage URLs use the configured public namespace"
760
+ },
708
761
  other: {
709
762
  publicOrigin: false,
710
763
  reason: "host/path-sensitive signing and hosted URL rewriting are not certified; use a direct endpoint"
711
764
  }
712
765
  };
766
+ function supportsPublicIngress(service) {
767
+ return service === "stripe" || service === "cognito" || service === "medplum";
768
+ }
713
769
  function advertisedHttpBase(entry) {
714
770
  if (entry.publicPath !== void 0 && !entry.publicOrigin)
715
771
  throw new Error("publicPath requires publicOrigin");
@@ -1248,7 +1304,7 @@ data: ${JSON.stringify(data)}
1248
1304
  {
1249
1305
  protocol: c.protocol,
1250
1306
  coverage: c.coverage,
1251
- ingress: c.targetName === "stripe" ? ingressSupport.stripe : ingressSupport.other,
1307
+ ingress: supportsPublicIngress(c.targetName ?? "") ? ingressSupport[c.targetName] : ingressSupport.other,
1252
1308
  target: c.targetName,
1253
1309
  outboundPolicy: c.outboundPolicy ?? "unsupported",
1254
1310
  version: 1,
@@ -2332,11 +2388,12 @@ data: ${JSON.stringify(data)}
2332
2388
  format: "mockingbird.fleet/v1",
2333
2389
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2334
2390
  sourceId: this.ctx.id,
2391
+ ...this.ctx.options.archiveCompatibility ? { compatibility: structuredClone(this.ctx.options.archiveCompatibility.tags) } : {},
2335
2392
  namespaces: contents
2336
2393
  });
2337
2394
  }
2338
2395
  async restoreArchive(input, selectedNamespace) {
2339
- const archive = validateArchive(input);
2396
+ const archive = assertArchiveCompatibility(input, this.ctx.options.archiveCompatibility);
2340
2397
  const entries = Object.entries(archive.namespaces).filter(([ns]) => selectedNamespace === void 0 || ns === selectedNamespace);
2341
2398
  if (!entries.length)
2342
2399
  throw new Error("selected namespace absent from archive");
@@ -2621,6 +2678,9 @@ var fleet_config_schema_default = {
2621
2678
  adminControls: {
2622
2679
  $ref: "#/$defs/controls"
2623
2680
  },
2681
+ idBase: {
2682
+ $ref: "#/$defs/idBase"
2683
+ },
2624
2684
  namespace: {
2625
2685
  type: "string",
2626
2686
  pattern: "^[A-Za-z0-9_.-]{1,64}$"
@@ -2710,6 +2770,12 @@ var fleet_config_schema_default = {
2710
2770
  adminControls: {
2711
2771
  $ref: "#/$defs/controls"
2712
2772
  },
2773
+ idBase: {
2774
+ $ref: "#/$defs/idBase"
2775
+ },
2776
+ settings: {
2777
+ type: "object"
2778
+ },
2713
2779
  fixtures: {
2714
2780
  type: "object"
2715
2781
  },
@@ -2756,6 +2822,18 @@ var fleet_config_schema_default = {
2756
2822
  }
2757
2823
  }
2758
2824
  },
2825
+ idBase: {
2826
+ anyOf: [
2827
+ {
2828
+ type: "integer",
2829
+ minimum: 0
2830
+ },
2831
+ {
2832
+ type: "string",
2833
+ pattern: "^(boot|[0-9]+)$"
2834
+ }
2835
+ ]
2836
+ },
2759
2837
  controls: {
2760
2838
  type: "object",
2761
2839
  additionalProperties: false,
@@ -2905,6 +2983,8 @@ async function preflightFleet(config, options) {
2905
2983
  throw new Error("protected archive policy requires a protected repository; configure its protector before startup");
2906
2984
  resolveAdminPrefix(config.adminPrefix);
2907
2985
  normalizeBootControls(config.adminControls);
2986
+ if (config.idBase !== void 0)
2987
+ normalizeIdBase(config.idBase, "idBase");
2908
2988
  const targets = /* @__PURE__ */ new Map();
2909
2989
  const ports = /* @__PURE__ */ new Set();
2910
2990
  for (const [name, entry] of Object.entries(config.services)) {
@@ -2920,7 +3000,7 @@ async function preflightFleet(config, options) {
2920
3000
  if (entry.target !== void 0 && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.target))
2921
3001
  throw new Error("target must name a supported service suffix");
2922
3002
  const service = entry.target ?? name;
2923
- if (advertisedHttpBase(entry) && service !== "stripe")
3003
+ if (advertisedHttpBase(entry) && !supportsPublicIngress(service))
2924
3004
  throw new Error(`public ingress unsupported for ${service}; use a direct endpoint to preserve signed targets`);
2925
3005
  const target = await options.load(service);
2926
3006
  const protocol = "protocol" in target ? target.protocol : "http";
@@ -2936,6 +3016,13 @@ async function preflightFleet(config, options) {
2936
3016
  throw new Error("invalid protocol namespace selector");
2937
3017
  }
2938
3018
  }
3019
+ if (entry.idBase !== void 0)
3020
+ normalizeIdBase(entry.idBase, "idBase");
3021
+ if (entry.settings !== void 0) {
3022
+ if (protocol !== "http" || !("create" in target))
3023
+ throw new Error("settings: only HTTP services take boot settings");
3024
+ checkSettings(target, entry.settings, "settings");
3025
+ }
2939
3026
  const port = entry.port ?? target.defaultPort;
2940
3027
  if (port !== 0) {
2941
3028
  const key = `${entry.host ?? "127.0.0.1"}:${port}`;
@@ -2971,7 +3058,7 @@ async function preflightFleet(config, options) {
2971
3058
  break;
2972
3059
  }
2973
3060
  }
2974
- targets.set(name, { ...target, packageVersion: packageVersion ?? "1.5.0" });
3061
+ targets.set(name, { ...target, packageVersion: packageVersion ?? "1.7.0" });
2975
3062
  } catch (error) {
2976
3063
  throw new Error(`service ${name}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2977
3064
  }
@@ -3015,6 +3102,11 @@ async function preflightFleet(config, options) {
3015
3102
  return ordered;
3016
3103
  }
3017
3104
  function validateFleetPolicies(options) {
3105
+ if (options.archiveCompatibility) {
3106
+ validateCompatibilityTags(options.archiveCompatibility.tags);
3107
+ if (options.archiveCompatibility.missing !== void 0 && !["allow", "reject"].includes(options.archiveCompatibility.missing))
3108
+ throw new Error("archiveCompatibility.missing must be allow or reject");
3109
+ }
3018
3110
  if (options.journal && !options.repository)
3019
3111
  throw new Error("durable operation journal requires an archive repository");
3020
3112
  if (options.cors) {
@@ -3286,6 +3378,7 @@ var httpChild = async (target, entry, options, admission, generation, privateAcc
3286
3378
  options.onLog?.(entry2);
3287
3379
  },
3288
3380
  ...entry.adminControls !== void 0 ? { adminControls: entry.adminControls } : {},
3381
+ ...entry.settings !== void 0 ? { settings: checkSettings(target, entry.settings, "settings") } : {},
3289
3382
  ...entry.fixtures !== void 0 ? { fixtures: entry.fixtures } : {},
3290
3383
  ...(advertisedHttpBase(entry) ?? entry.baseUrl) !== void 0 ? { baseUrl: advertisedHttpBase(entry) ?? entry.baseUrl } : {}
3291
3384
  });
@@ -3304,6 +3397,8 @@ var httpChild = async (target, entry, options, admission, generation, privateAcc
3304
3397
  ].some((method) => typeof created[method] !== "function"))
3305
3398
  throw new Error("HTTP target does not implement the fleet runtime protocol; build/install matching supported service versions");
3306
3399
  const runtime = created;
3400
+ if (entry.idBase !== void 0)
3401
+ runtime.setIdBase?.(entry.idBase);
3307
3402
  const unobserve = runtime.webhooks?.observeDeliveries(observer.delivery);
3308
3403
  if (options.outbound && runtime.webhooks)
3309
3404
  runtime.webhooks.setTransportPolicy(ownedTransport(options.outbound));
@@ -3902,6 +3997,7 @@ async function startFleet(config, options) {
3902
3997
  ...entry,
3903
3998
  adminPrefix: resolveAdminPrefix(entry.adminPrefix ?? adminPrefix),
3904
3999
  ...adminKey !== void 0 ? { adminKey } : {},
4000
+ ...(entry.idBase ?? config.idBase) !== void 0 ? { idBase: entry.idBase ?? config.idBase } : {},
3905
4001
  // Fleet-wide controls, with the service's own keys replacing them.
3906
4002
  ...config.adminControls !== void 0 || entry.adminControls !== void 0 ? { adminControls: { ...config.adminControls, ...entry.adminControls } } : {}
3907
4003
  }, options, (namespace, request) => management?.admit(namespace, request), (namespace) => management?.generation(namespace), (namespace, request) => management?.privateRequest(namespace, request) ?? false, {
@@ -4007,10 +4103,44 @@ async function startFleet(config, options) {
4007
4103
  }
4008
4104
  }
4009
4105
 
4010
- // ../../adapters/node/dist/fleet-tools.js
4011
- import { readFile as readFile4 } from "node:fs/promises";
4106
+ // ../../adapters/node/dist/fleet-loader.js
4012
4107
  import { createRequire as createRequire2 } from "node:module";
4013
4108
  import { resolve as resolve2 } from "node:path";
4109
+ import { pathToFileURL } from "node:url";
4110
+ var suffix = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4111
+ function createInstalledServiceLoader(options) {
4112
+ if (!options.projectRoot)
4113
+ throw new Error("consumer projectRoot required");
4114
+ const allowed = new Set(options.services);
4115
+ for (const name of allowed)
4116
+ if (!suffix.test(name))
4117
+ throw new Error("invalid service suffix");
4118
+ const require2 = createRequire2(resolve2(options.projectRoot, "package.json"));
4119
+ const targets = new Map((options.targets ?? []).map((target) => [target.name, target]));
4120
+ return async (name) => {
4121
+ if (!suffix.test(name) || !allowed.has(name))
4122
+ throw new Error("service is not declared in loader allowlist");
4123
+ const own = targets.get(name);
4124
+ if (own)
4125
+ return own;
4126
+ const specifier = `@crvouga/mockingbird-service-${name}/server`;
4127
+ let location;
4128
+ try {
4129
+ location = require2.resolve(specifier);
4130
+ } catch {
4131
+ throw new Error(`cannot load service "${name}": install @crvouga/mockingbird-service-${name} in the consumer project; its /server export is required`);
4132
+ }
4133
+ const mod = await import(pathToFileURL(location).href);
4134
+ if (!mod.serveTarget || mod.serveTarget.name !== name || ("protocol" in mod.serveTarget ? typeof mod.serveTarget.start !== "function" : typeof mod.serveTarget.create !== "function"))
4135
+ throw new Error(`${specifier} exports no matching serveTarget`);
4136
+ return mod.serveTarget;
4137
+ };
4138
+ }
4139
+
4140
+ // ../../adapters/node/dist/fleet-tools.js
4141
+ import { readFile as readFile4 } from "node:fs/promises";
4142
+ import { createRequire as createRequire3 } from "node:module";
4143
+ import { resolve as resolve3 } from "node:path";
4014
4144
 
4015
4145
  // ../../adapters/node/dist/fleet-client.js
4016
4146
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -4024,6 +4154,14 @@ var FleetClientError = class extends Error {
4024
4154
  this.details = details;
4025
4155
  }
4026
4156
  };
4157
+ var FleetClientResponseError = class extends FleetClientError {
4158
+ code;
4159
+ constructor(code, status, origin) {
4160
+ super(status, { code, body: "omitted", ...origin ? { origin } : {} });
4161
+ this.code = code;
4162
+ this.name = "FleetClientResponseError";
4163
+ }
4164
+ };
4027
4165
  var FleetClient = class {
4028
4166
  base;
4029
4167
  options;
@@ -4036,7 +4174,14 @@ var FleetClient = class {
4036
4174
  if (url.username || url.password)
4037
4175
  throw new Error("fleet client URL must not contain credentials");
4038
4176
  url.searchParams.set("namespace", options.namespace ?? this.options.namespace ?? "default");
4177
+ const limit = options.maxResponseBytes ?? this.options.maxResponseBytes ?? 64 * 1024 * 1024;
4178
+ if (!Number.isSafeInteger(limit) || limit < 1)
4179
+ throw new Error("invalid response byte limit");
4180
+ const deadline = AbortSignal.timeout(this.options.timeoutMs ?? 3e4);
4181
+ const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
4182
+ signal.throwIfAborted();
4039
4183
  const response = await (this.options.fetch ?? fetch)(url, {
4184
+ redirect: "manual",
4040
4185
  method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
4041
4186
  headers: {
4042
4187
  "content-type": "application/json",
@@ -4044,11 +4189,71 @@ var FleetClient = class {
4044
4189
  ...options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}
4045
4190
  },
4046
4191
  ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
4047
- signal: options.signal ?? AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
4192
+ signal
4048
4193
  });
4049
- const data = await response.json();
4050
- if (!response.ok)
4051
- throw new FleetClientError(response.status, data);
4194
+ if (response.status >= 300 && response.status < 400) {
4195
+ await response.body?.cancel();
4196
+ throw new FleetClientResponseError("redirect", response.status, url.origin);
4197
+ }
4198
+ if (Number(response.headers.get("content-length")) > limit) {
4199
+ await response.body?.cancel();
4200
+ throw new FleetClientResponseError("response_too_large", response.status);
4201
+ }
4202
+ const reader = response.body?.getReader();
4203
+ const chunks = [];
4204
+ let size = 0;
4205
+ const abort = () => {
4206
+ void reader?.cancel(signal.reason).catch(() => {
4207
+ });
4208
+ };
4209
+ signal.addEventListener("abort", abort, { once: true });
4210
+ try {
4211
+ if (reader)
4212
+ while (true) {
4213
+ signal.throwIfAborted();
4214
+ const { done, value } = await reader.read();
4215
+ signal.throwIfAborted();
4216
+ if (done)
4217
+ break;
4218
+ size += value.byteLength;
4219
+ if (size > limit) {
4220
+ await reader.cancel();
4221
+ throw new FleetClientResponseError("response_too_large", response.status);
4222
+ }
4223
+ chunks.push(value);
4224
+ }
4225
+ } finally {
4226
+ signal.removeEventListener("abort", abort);
4227
+ if (signal.aborted)
4228
+ await reader?.cancel(signal.reason).catch(() => {
4229
+ });
4230
+ reader?.releaseLock();
4231
+ }
4232
+ const bytes = new Uint8Array(size);
4233
+ let offset = 0;
4234
+ for (const chunk of chunks) {
4235
+ bytes.set(chunk, offset);
4236
+ offset += chunk.byteLength;
4237
+ }
4238
+ const text = new TextDecoder().decode(bytes);
4239
+ let data;
4240
+ try {
4241
+ data = text.length ? JSON.parse(text) : void 0;
4242
+ } catch {
4243
+ if (response.ok)
4244
+ throw new FleetClientResponseError("invalid_json", response.status);
4245
+ }
4246
+ if (!response.ok) {
4247
+ const error = data && typeof data === "object" ? data.error : void 0;
4248
+ const detail = { body: "omitted" };
4249
+ if (error && typeof error === "object")
4250
+ for (const key of ["type", "code"]) {
4251
+ const value = error[key];
4252
+ if (typeof value === "string" && /^[A-Za-z0-9_.-]{1,128}$/.test(value))
4253
+ detail[key] = value;
4254
+ }
4255
+ throw new FleetClientError(response.status, detail);
4256
+ }
4052
4257
  return data;
4053
4258
  }
4054
4259
  discover() {
@@ -4114,17 +4319,52 @@ function renderConnections(manifest, privateManifest, namespace, mapping) {
4114
4319
  const child = manifest.services[entry.service], secret = privateManifest.services[entry.service];
4115
4320
  if (!child || !secret)
4116
4321
  throw new Error(`missing service mapping ${entry.service}`);
4322
+ let endpoint;
4117
4323
  if (child.protocol === "http") {
4118
4324
  const template = child.namespaces.path;
4119
4325
  if (typeof template !== "string")
4120
4326
  throw new Error(`missing HTTP namespace mapping ${entry.service}`);
4121
- result[variable] = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
4327
+ endpoint = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
4122
4328
  } else {
4123
- const endpoint = secret.namespaces?.[namespace];
4124
- if (!endpoint)
4329
+ const selected = secret.namespaces?.[namespace];
4330
+ if (!selected)
4125
4331
  throw new Error(`missing namespace mapping ${namespace}/${entry.service}`);
4126
- result[variable] = endpoint;
4332
+ endpoint = selected;
4333
+ }
4334
+ const url = new URL(endpoint);
4335
+ const field = entry.field ?? "url";
4336
+ let value;
4337
+ switch (field) {
4338
+ case "url":
4339
+ value = endpoint;
4340
+ break;
4341
+ case "hostname":
4342
+ value = url.hostname.replace(/^\[|\]$/g, "");
4343
+ break;
4344
+ case "port":
4345
+ value = url.port;
4346
+ break;
4347
+ case "username":
4348
+ value = decodeURIComponent(url.username);
4349
+ break;
4350
+ case "password":
4351
+ value = decodeURIComponent(url.password);
4352
+ break;
4353
+ case "pathname":
4354
+ value = decodeURIComponent(url.pathname);
4355
+ break;
4356
+ case "database":
4357
+ value = decodeURIComponent(url.pathname.replace(/^\//, ""));
4358
+ break;
4359
+ default:
4360
+ throw new Error("unsupported connection field");
4127
4361
  }
4362
+ if (!value) {
4363
+ if (typeof entry.fallback !== "string")
4364
+ throw new Error(`missing connection component ${field}`);
4365
+ value = entry.fallback;
4366
+ }
4367
+ result[variable] = value;
4128
4368
  }
4129
4369
  return result;
4130
4370
  }
@@ -4148,7 +4388,7 @@ async function doctorFleet(config, options, probes = []) {
4148
4388
  const plan = [];
4149
4389
  try {
4150
4390
  const targets = await preflightFleet(config, options);
4151
- const require2 = createRequire2(resolve2(process.cwd(), "package.json"));
4391
+ const require2 = createRequire3(resolve3(process.cwd(), "package.json"));
4152
4392
  for (const [service, target] of targets) {
4153
4393
  let version = "development";
4154
4394
  try {
@@ -4203,7 +4443,7 @@ async function doctorFleet(config, options, probes = []) {
4203
4443
  };
4204
4444
  }
4205
4445
  async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e4) {
4206
- const archive = validateArchive(input);
4446
+ const archive = assertArchiveCompatibility(input, options.archiveCompatibility);
4207
4447
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4208
4448
  const isolated = {
4209
4449
  ...config,
@@ -4215,7 +4455,7 @@ async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e
4215
4455
  };
4216
4456
  let fleet;
4217
4457
  try {
4218
- fleet = await startFleet(isolated, { load: options.load });
4458
+ fleet = await startFleet(isolated, options);
4219
4459
  const client = new FleetClient(fleet.manifest.adminBase, {
4220
4460
  ...config.adminKey ? { adminKey: config.adminKey } : {}
4221
4461
  });
@@ -4442,6 +4682,16 @@ var COMMON_SERVE_OPTIONS = {
4442
4682
  value: "<seed>",
4443
4683
  description: "Seed for every random choice (env MOCKINGBIRD_SEED)"
4444
4684
  },
4685
+ "id-base": {
4686
+ type: "string",
4687
+ value: "<n|boot>",
4688
+ description: "Start generated ids at this base so a restart does not reissue the last boot's ids; boot derives one from the clock (env MOCKINGBIRD_ID_BASE; default 0, the same ids every boot)"
4689
+ },
4690
+ settings: {
4691
+ type: "string",
4692
+ value: "<json|file>",
4693
+ description: "Service settings to boot with, as JSON or a JSON file; every namespace starts from them (env MOCKINGBIRD_SETTINGS; only for services that declare settings)"
4694
+ },
4445
4695
  "admin-controls": {
4446
4696
  type: "string",
4447
4697
  value: "<json|file>",
@@ -4536,19 +4786,40 @@ var readJsonFile = async (path, label) => {
4536
4786
  }
4537
4787
  };
4538
4788
  var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4789
+ var readJsonOption = async (source, label) => {
4790
+ const trimmed = source.trim();
4791
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
4792
+ try {
4793
+ return JSON.parse(trimmed);
4794
+ } catch {
4795
+ throw new Error(`${label}: not valid JSON`);
4796
+ }
4797
+ }
4798
+ return readJsonFile(source, label);
4799
+ };
4800
+ var resolveServeIdBase = (values, env = process.env) => {
4801
+ const source = asString(values["id-base"]) ?? env.MOCKINGBIRD_ID_BASE;
4802
+ if (source === void 0 || source.trim() === "")
4803
+ return void 0;
4804
+ normalizeIdBase(source.trim(), "--id-base");
4805
+ return source.trim();
4806
+ };
4807
+ var resolveServeSettings = async (target, values, env = process.env) => {
4808
+ const flag = asString(values.settings);
4809
+ const source = flag ?? env.MOCKINGBIRD_SETTINGS;
4810
+ if (source === void 0 || source.trim() === "")
4811
+ return void 0;
4812
+ const label = flag !== void 0 ? "--settings" : "MOCKINGBIRD_SETTINGS";
4813
+ const parsed = await readJsonOption(source, label);
4814
+ if (!isObject2(parsed))
4815
+ throw new Error(`${label}: expected a JSON object`);
4816
+ return checkSettings(target, parsed, label);
4817
+ };
4539
4818
  var resolveServeControls = async (values, env = process.env, base) => {
4540
4819
  const source = asString(values["admin-controls"]) ?? env.MOCKINGBIRD_ADMIN_CONTROLS;
4541
4820
  let document = isObject2(base) ? { ...base } : {};
4542
4821
  if (source !== void 0 && source.trim() !== "") {
4543
- let parsed;
4544
- if (source.trim().startsWith("{")) {
4545
- try {
4546
- parsed = JSON.parse(source);
4547
- } catch {
4548
- throw new Error("--admin-controls: not valid JSON");
4549
- }
4550
- } else
4551
- parsed = await readJsonFile(source, "--admin-controls");
4822
+ const parsed = source.trim().startsWith("{") ? await readJsonOption(source, "--admin-controls") : await readJsonFile(source, "--admin-controls");
4552
4823
  if (!isObject2(parsed))
4553
4824
  throw new Error("--admin-controls: expected a JSON object");
4554
4825
  document = { ...document, ...parsed };
@@ -4590,26 +4861,7 @@ var resolveServeControls = async (values, env = process.env, base) => {
4590
4861
  normalizeBootControls(document, "adminControls");
4591
4862
  return document;
4592
4863
  };
4593
- var loadTarget = async (name, own) => {
4594
- if (name === own.name)
4595
- return own;
4596
- const specifier = `@crvouga/mockingbird-service-${name}/server`;
4597
- try {
4598
- let mod;
4599
- try {
4600
- mod = await import(specifier);
4601
- } catch {
4602
- const fromProject = createRequire3(resolve3(process.cwd(), "package.json"));
4603
- mod = await import(pathToFileURL(fromProject.resolve(specifier)).href);
4604
- }
4605
- if (!mod.serveTarget)
4606
- throw new Error(`${specifier} exports no serveTarget`);
4607
- return mod.serveTarget;
4608
- } catch (error) {
4609
- const reason = error instanceof Error ? error.message : String(error);
4610
- throw new Error(`cannot load service "${name}": ${reason}. Install @crvouga/mockingbird-service-${name}.`);
4611
- }
4612
- };
4864
+ var loadTarget = (name, own) => createInstalledServiceLoader({ projectRoot: process.cwd(), services: [name], targets: [own] })(name);
4613
4865
  var start = async (target, values, config) => {
4614
4866
  const adminPrefix = resolveAdminPrefix(config.adminPrefix);
4615
4867
  const runtime = await target.create(values, {
@@ -4617,8 +4869,11 @@ var start = async (target, values, config) => {
4617
4869
  adminKey: config.adminKey,
4618
4870
  seed: config.seed,
4619
4871
  onLog: formatLog(config.log),
4872
+ ...config.settings !== void 0 ? { settings: config.settings } : {},
4620
4873
  ...config.adminControls !== void 0 ? { adminControls: config.adminControls } : {}
4621
4874
  });
4875
+ if (config.idBase !== void 0)
4876
+ runtime.setIdBase?.(config.idBase);
4622
4877
  const listening = await listen(runtime, { port: config.port, host: config.host });
4623
4878
  target.listening?.(listening);
4624
4879
  console.log(`${target.name} emulator listening on ${listening.url}`);
@@ -4664,6 +4919,11 @@ var serveCommand = (target) => ({
4664
4919
  const adminControls = await resolveServeControls(values, process.env, config.adminControls);
4665
4920
  if (adminControls !== void 0)
4666
4921
  config.adminControls = adminControls;
4922
+ if (values.settings !== void 0)
4923
+ throw new Error("--settings serves a single service; give each service its own `settings` in the config");
4924
+ const idBase = config.idBase ?? resolveServeIdBase(values);
4925
+ if (idBase !== void 0)
4926
+ config.idBase = idBase;
4667
4927
  const fleet = await startFleet(config, {
4668
4928
  load: (name) => loadTarget(name, target),
4669
4929
  onLog: formatLog(config.log ?? log),
@@ -4693,8 +4953,12 @@ var serveCommand = (target) => ({
4693
4953
  let listening;
4694
4954
  try {
4695
4955
  const adminControls = await resolveServeControls(values);
4956
+ const idBase = resolveServeIdBase(values);
4957
+ const settings = await resolveServeSettings(target, values);
4696
4958
  listening = await start(target, values, {
4697
4959
  ...adminControls !== void 0 ? { adminControls } : {},
4960
+ ...idBase !== void 0 ? { idBase } : {},
4961
+ ...settings !== void 0 ? { settings } : {},
4698
4962
  adminPrefix: resolveAdminPrefix(asString(values["admin-prefix"]) ?? process.env.MOCKINGBIRD_ADMIN_PREFIX),
4699
4963
  port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
4700
4964
  host: asString(values.host) ?? "127.0.0.1",
@@ -4747,4 +5011,4 @@ export {
4747
5011
  createServer3 as createServer2,
4748
5012
  serveTarget
4749
5013
  };
4750
- //# sourceMappingURL=chunk-3LYIXCE7.js.map
5014
+ //# sourceMappingURL=chunk-LYDM4GWT.js.map