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