@crvouga/mockingbird-service-mailosaur 3.5.0 → 3.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-mailosaur
2
2
 
3
- ## 3.5.0 (2026-10-16)
3
+ ## 3.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
+ ## 3.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
+ ## 3.5.0 (2026-10-15)
4
16
 
5
17
  ### Features
6
18
 
@@ -3,12 +3,13 @@ import {
3
3
  createRuntime,
4
4
  normalizeBootControls,
5
5
  normalizeControls,
6
+ normalizeIdBase,
6
7
  ownedTransport,
7
8
  parseDuration,
8
9
  parseFaultHeader,
9
10
  resolveAdminPrefix,
10
11
  restoreClock
11
- } from "./chunk-U3FYDM4L.js";
12
+ } from "./chunk-PROCZMVA.js";
12
13
 
13
14
  // src/server.ts
14
15
  import { writeFileSync } from "node:fs";
@@ -21,11 +22,21 @@ import { createServer as createTlsServer } from "node:tls";
21
22
 
22
23
  // ../../adapters/node/dist/cli.js
23
24
  import { readFile as readFile5 } from "node:fs/promises";
24
- import { createRequire as createRequire3 } from "node:module";
25
- import { resolve as resolve3 } from "node:path";
26
- import { pathToFileURL } from "node:url";
27
25
  import { parseArgs } from "node:util";
28
26
 
27
+ // ../../adapters/node/dist/boot-settings.js
28
+ var checkSettings = (target, settings, label) => {
29
+ if (!target.parseSettings)
30
+ throw new Error(`${label}: ${target.name} takes no boot settings`);
31
+ try {
32
+ return target.parseSettings(settings);
33
+ } catch (error) {
34
+ throw new Error(`${label}: ${error instanceof Error ? error.message : String(error)}`, {
35
+ cause: error
36
+ });
37
+ }
38
+ };
39
+
29
40
  // ../../adapters/node/dist/fleet.js
30
41
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
31
42
  import { createHash as createHash4, randomUUID as randomUUID4 } from "node:crypto";
@@ -37,6 +48,31 @@ import { createHash as createHash3, randomBytes, randomUUID as randomUUID3 } fro
37
48
 
38
49
  // ../../adapters/node/dist/fleet-archive.js
39
50
  import { createHash } from "node:crypto";
51
+ function validateCompatibilityTags(tags) {
52
+ if (!tags || typeof tags !== "object" || Array.isArray(tags))
53
+ throw new Error("invalid archive compatibility tags");
54
+ const entries = Object.entries(tags);
55
+ 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)))
56
+ throw new Error("invalid archive compatibility tags");
57
+ }
58
+ function assertCompatibilityTags(tags, policy) {
59
+ if (tags !== void 0)
60
+ validateCompatibilityTags(tags);
61
+ if (!policy)
62
+ return;
63
+ validateCompatibilityTags(policy.tags);
64
+ if (policy.missing !== void 0 && !["allow", "reject"].includes(policy.missing))
65
+ throw new Error("invalid missing compatibility policy");
66
+ if (tags === void 0 && policy.missing === "allow")
67
+ return;
68
+ if (canonical(tags) !== canonical(policy.tags))
69
+ throw new Error("application archive compatibility mismatch");
70
+ }
71
+ function assertArchiveCompatibility(input, policy) {
72
+ const archive = validateArchive(input);
73
+ assertCompatibilityTags(archive.compatibility, policy);
74
+ return archive;
75
+ }
40
76
  var canonical = (value) => {
41
77
  if (Array.isArray(value))
42
78
  return `[${value.map(canonical).join(",")}]`;
@@ -56,6 +92,8 @@ function validateArchive(value) {
56
92
  const { sha256, ...content } = doc;
57
93
  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))
58
94
  throw new Error("unsupported or corrupt fleet archive");
95
+ if (doc.compatibility !== void 0)
96
+ validateCompatibilityTags(doc.compatibility);
59
97
  for (const [namespace, entry] of Object.entries(doc.namespaces)) {
60
98
  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)
61
99
  throw new Error("invalid archive namespace");
@@ -71,6 +109,7 @@ function inspectArchive(input) {
71
109
  format: archive.format,
72
110
  createdAt: archive.createdAt,
73
111
  sha256: archive.sha256,
112
+ ...archive.compatibility ? { compatibility: archive.compatibility } : {},
74
113
  namespaces: Object.fromEntries(Object.entries(archive.namespaces).map(([name2, ns]) => [
75
114
  name2,
76
115
  {
@@ -713,11 +752,28 @@ var ingressSupport = {
713
752
  streaming: true,
714
753
  signing: "vendor request signing is not used; webhook signatures remain payload based"
715
754
  },
755
+ cognito: {
756
+ publicOrigin: true,
757
+ pathPrefix: true,
758
+ namespaces: "path-or-header",
759
+ streaming: true,
760
+ signing: "JWT issuer and JWKS use the configured public namespace; SigV4 verification is not modelled"
761
+ },
762
+ medplum: {
763
+ publicOrigin: true,
764
+ pathPrefix: true,
765
+ namespaces: "path-or-header",
766
+ streaming: true,
767
+ signing: "FHIR, OAuth and storage URLs use the configured public namespace"
768
+ },
716
769
  other: {
717
770
  publicOrigin: false,
718
771
  reason: "host/path-sensitive signing and hosted URL rewriting are not certified; use a direct endpoint"
719
772
  }
720
773
  };
774
+ function supportsPublicIngress(service) {
775
+ return service === "stripe" || service === "cognito" || service === "medplum";
776
+ }
721
777
  function advertisedHttpBase(entry) {
722
778
  if (entry.publicPath !== void 0 && !entry.publicOrigin)
723
779
  throw new Error("publicPath requires publicOrigin");
@@ -1256,7 +1312,7 @@ data: ${JSON.stringify(data)}
1256
1312
  {
1257
1313
  protocol: c.protocol,
1258
1314
  coverage: c.coverage,
1259
- ingress: c.targetName === "stripe" ? ingressSupport.stripe : ingressSupport.other,
1315
+ ingress: supportsPublicIngress(c.targetName ?? "") ? ingressSupport[c.targetName] : ingressSupport.other,
1260
1316
  target: c.targetName,
1261
1317
  outboundPolicy: c.outboundPolicy ?? "unsupported",
1262
1318
  version: 1,
@@ -2340,11 +2396,12 @@ data: ${JSON.stringify(data)}
2340
2396
  format: "mockingbird.fleet/v1",
2341
2397
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2342
2398
  sourceId: this.ctx.id,
2399
+ ...this.ctx.options.archiveCompatibility ? { compatibility: structuredClone(this.ctx.options.archiveCompatibility.tags) } : {},
2343
2400
  namespaces: contents
2344
2401
  });
2345
2402
  }
2346
2403
  async restoreArchive(input, selectedNamespace) {
2347
- const archive = validateArchive(input);
2404
+ const archive = assertArchiveCompatibility(input, this.ctx.options.archiveCompatibility);
2348
2405
  const entries = Object.entries(archive.namespaces).filter(([ns]) => selectedNamespace === void 0 || ns === selectedNamespace);
2349
2406
  if (!entries.length)
2350
2407
  throw new Error("selected namespace absent from archive");
@@ -2629,6 +2686,9 @@ var fleet_config_schema_default = {
2629
2686
  adminControls: {
2630
2687
  $ref: "#/$defs/controls"
2631
2688
  },
2689
+ idBase: {
2690
+ $ref: "#/$defs/idBase"
2691
+ },
2632
2692
  namespace: {
2633
2693
  type: "string",
2634
2694
  pattern: "^[A-Za-z0-9_.-]{1,64}$"
@@ -2718,6 +2778,12 @@ var fleet_config_schema_default = {
2718
2778
  adminControls: {
2719
2779
  $ref: "#/$defs/controls"
2720
2780
  },
2781
+ idBase: {
2782
+ $ref: "#/$defs/idBase"
2783
+ },
2784
+ settings: {
2785
+ type: "object"
2786
+ },
2721
2787
  fixtures: {
2722
2788
  type: "object"
2723
2789
  },
@@ -2764,6 +2830,18 @@ var fleet_config_schema_default = {
2764
2830
  }
2765
2831
  }
2766
2832
  },
2833
+ idBase: {
2834
+ anyOf: [
2835
+ {
2836
+ type: "integer",
2837
+ minimum: 0
2838
+ },
2839
+ {
2840
+ type: "string",
2841
+ pattern: "^(boot|[0-9]+)$"
2842
+ }
2843
+ ]
2844
+ },
2767
2845
  controls: {
2768
2846
  type: "object",
2769
2847
  additionalProperties: false,
@@ -2913,6 +2991,8 @@ async function preflightFleet(config, options) {
2913
2991
  throw new Error("protected archive policy requires a protected repository; configure its protector before startup");
2914
2992
  resolveAdminPrefix(config.adminPrefix);
2915
2993
  normalizeBootControls(config.adminControls);
2994
+ if (config.idBase !== void 0)
2995
+ normalizeIdBase(config.idBase, "idBase");
2916
2996
  const targets = /* @__PURE__ */ new Map();
2917
2997
  const ports = /* @__PURE__ */ new Set();
2918
2998
  for (const [name2, entry] of Object.entries(config.services)) {
@@ -2928,7 +3008,7 @@ async function preflightFleet(config, options) {
2928
3008
  if (entry.target !== void 0 && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.target))
2929
3009
  throw new Error("target must name a supported service suffix");
2930
3010
  const service = entry.target ?? name2;
2931
- if (advertisedHttpBase(entry) && service !== "stripe")
3011
+ if (advertisedHttpBase(entry) && !supportsPublicIngress(service))
2932
3012
  throw new Error(`public ingress unsupported for ${service}; use a direct endpoint to preserve signed targets`);
2933
3013
  const target = await options.load(service);
2934
3014
  const protocol = "protocol" in target ? target.protocol : "http";
@@ -2944,6 +3024,13 @@ async function preflightFleet(config, options) {
2944
3024
  throw new Error("invalid protocol namespace selector");
2945
3025
  }
2946
3026
  }
3027
+ if (entry.idBase !== void 0)
3028
+ normalizeIdBase(entry.idBase, "idBase");
3029
+ if (entry.settings !== void 0) {
3030
+ if (protocol !== "http" || !("create" in target))
3031
+ throw new Error("settings: only HTTP services take boot settings");
3032
+ checkSettings(target, entry.settings, "settings");
3033
+ }
2947
3034
  const port = entry.port ?? target.defaultPort;
2948
3035
  if (port !== 0) {
2949
3036
  const key = `${entry.host ?? "127.0.0.1"}:${port}`;
@@ -2979,7 +3066,7 @@ async function preflightFleet(config, options) {
2979
3066
  break;
2980
3067
  }
2981
3068
  }
2982
- targets.set(name2, { ...target, packageVersion: packageVersion ?? "3.5.0" });
3069
+ targets.set(name2, { ...target, packageVersion: packageVersion ?? "3.7.0" });
2983
3070
  } catch (error) {
2984
3071
  throw new Error(`service ${name2}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2985
3072
  }
@@ -3023,6 +3110,11 @@ async function preflightFleet(config, options) {
3023
3110
  return ordered;
3024
3111
  }
3025
3112
  function validateFleetPolicies(options) {
3113
+ if (options.archiveCompatibility) {
3114
+ validateCompatibilityTags(options.archiveCompatibility.tags);
3115
+ if (options.archiveCompatibility.missing !== void 0 && !["allow", "reject"].includes(options.archiveCompatibility.missing))
3116
+ throw new Error("archiveCompatibility.missing must be allow or reject");
3117
+ }
3026
3118
  if (options.journal && !options.repository)
3027
3119
  throw new Error("durable operation journal requires an archive repository");
3028
3120
  if (options.cors) {
@@ -3294,6 +3386,7 @@ var httpChild = async (target, entry, options, admission, generation, privateAcc
3294
3386
  options.onLog?.(entry2);
3295
3387
  },
3296
3388
  ...entry.adminControls !== void 0 ? { adminControls: entry.adminControls } : {},
3389
+ ...entry.settings !== void 0 ? { settings: checkSettings(target, entry.settings, "settings") } : {},
3297
3390
  ...entry.fixtures !== void 0 ? { fixtures: entry.fixtures } : {},
3298
3391
  ...(advertisedHttpBase(entry) ?? entry.baseUrl) !== void 0 ? { baseUrl: advertisedHttpBase(entry) ?? entry.baseUrl } : {}
3299
3392
  });
@@ -3312,6 +3405,8 @@ var httpChild = async (target, entry, options, admission, generation, privateAcc
3312
3405
  ].some((method) => typeof created[method] !== "function"))
3313
3406
  throw new Error("HTTP target does not implement the fleet runtime protocol; build/install matching supported service versions");
3314
3407
  const runtime = created;
3408
+ if (entry.idBase !== void 0)
3409
+ runtime.setIdBase?.(entry.idBase);
3315
3410
  const unobserve = runtime.webhooks?.observeDeliveries(observer.delivery);
3316
3411
  if (options.outbound && runtime.webhooks)
3317
3412
  runtime.webhooks.setTransportPolicy(ownedTransport(options.outbound));
@@ -3910,6 +4005,7 @@ async function startFleet(config, options) {
3910
4005
  ...entry,
3911
4006
  adminPrefix: resolveAdminPrefix(entry.adminPrefix ?? adminPrefix),
3912
4007
  ...adminKey !== void 0 ? { adminKey } : {},
4008
+ ...(entry.idBase ?? config.idBase) !== void 0 ? { idBase: entry.idBase ?? config.idBase } : {},
3913
4009
  // Fleet-wide controls, with the service's own keys replacing them.
3914
4010
  ...config.adminControls !== void 0 || entry.adminControls !== void 0 ? { adminControls: { ...config.adminControls, ...entry.adminControls } } : {}
3915
4011
  }, options, (namespace, request) => management?.admit(namespace, request), (namespace) => management?.generation(namespace), (namespace, request) => management?.privateRequest(namespace, request) ?? false, {
@@ -4015,10 +4111,44 @@ async function startFleet(config, options) {
4015
4111
  }
4016
4112
  }
4017
4113
 
4018
- // ../../adapters/node/dist/fleet-tools.js
4019
- import { readFile as readFile4 } from "node:fs/promises";
4114
+ // ../../adapters/node/dist/fleet-loader.js
4020
4115
  import { createRequire as createRequire2 } from "node:module";
4021
4116
  import { resolve as resolve2 } from "node:path";
4117
+ import { pathToFileURL } from "node:url";
4118
+ var suffix = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4119
+ function createInstalledServiceLoader(options) {
4120
+ if (!options.projectRoot)
4121
+ throw new Error("consumer projectRoot required");
4122
+ const allowed = new Set(options.services);
4123
+ for (const name2 of allowed)
4124
+ if (!suffix.test(name2))
4125
+ throw new Error("invalid service suffix");
4126
+ const require2 = createRequire2(resolve2(options.projectRoot, "package.json"));
4127
+ const targets = new Map((options.targets ?? []).map((target) => [target.name, target]));
4128
+ return async (name2) => {
4129
+ if (!suffix.test(name2) || !allowed.has(name2))
4130
+ throw new Error("service is not declared in loader allowlist");
4131
+ const own = targets.get(name2);
4132
+ if (own)
4133
+ return own;
4134
+ const specifier = `@crvouga/mockingbird-service-${name2}/server`;
4135
+ let location;
4136
+ try {
4137
+ location = require2.resolve(specifier);
4138
+ } catch {
4139
+ throw new Error(`cannot load service "${name2}": install @crvouga/mockingbird-service-${name2} in the consumer project; its /server export is required`);
4140
+ }
4141
+ const mod = await import(pathToFileURL(location).href);
4142
+ if (!mod.serveTarget || mod.serveTarget.name !== name2 || ("protocol" in mod.serveTarget ? typeof mod.serveTarget.start !== "function" : typeof mod.serveTarget.create !== "function"))
4143
+ throw new Error(`${specifier} exports no matching serveTarget`);
4144
+ return mod.serveTarget;
4145
+ };
4146
+ }
4147
+
4148
+ // ../../adapters/node/dist/fleet-tools.js
4149
+ import { readFile as readFile4 } from "node:fs/promises";
4150
+ import { createRequire as createRequire3 } from "node:module";
4151
+ import { resolve as resolve3 } from "node:path";
4022
4152
 
4023
4153
  // ../../adapters/node/dist/fleet-client.js
4024
4154
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -4032,6 +4162,14 @@ var FleetClientError = class extends Error {
4032
4162
  this.details = details;
4033
4163
  }
4034
4164
  };
4165
+ var FleetClientResponseError = class extends FleetClientError {
4166
+ code;
4167
+ constructor(code, status, origin) {
4168
+ super(status, { code, body: "omitted", ...origin ? { origin } : {} });
4169
+ this.code = code;
4170
+ this.name = "FleetClientResponseError";
4171
+ }
4172
+ };
4035
4173
  var FleetClient = class {
4036
4174
  base;
4037
4175
  options;
@@ -4044,7 +4182,14 @@ var FleetClient = class {
4044
4182
  if (url.username || url.password)
4045
4183
  throw new Error("fleet client URL must not contain credentials");
4046
4184
  url.searchParams.set("namespace", options.namespace ?? this.options.namespace ?? "default");
4185
+ const limit = options.maxResponseBytes ?? this.options.maxResponseBytes ?? 64 * 1024 * 1024;
4186
+ if (!Number.isSafeInteger(limit) || limit < 1)
4187
+ throw new Error("invalid response byte limit");
4188
+ const deadline = AbortSignal.timeout(this.options.timeoutMs ?? 3e4);
4189
+ const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
4190
+ signal.throwIfAborted();
4047
4191
  const response = await (this.options.fetch ?? fetch)(url, {
4192
+ redirect: "manual",
4048
4193
  method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
4049
4194
  headers: {
4050
4195
  "content-type": "application/json",
@@ -4052,11 +4197,71 @@ var FleetClient = class {
4052
4197
  ...options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}
4053
4198
  },
4054
4199
  ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
4055
- signal: options.signal ?? AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
4200
+ signal
4056
4201
  });
4057
- const data = await response.json();
4058
- if (!response.ok)
4059
- throw new FleetClientError(response.status, data);
4202
+ if (response.status >= 300 && response.status < 400) {
4203
+ await response.body?.cancel();
4204
+ throw new FleetClientResponseError("redirect", response.status, url.origin);
4205
+ }
4206
+ if (Number(response.headers.get("content-length")) > limit) {
4207
+ await response.body?.cancel();
4208
+ throw new FleetClientResponseError("response_too_large", response.status);
4209
+ }
4210
+ const reader = response.body?.getReader();
4211
+ const chunks = [];
4212
+ let size = 0;
4213
+ const abort = () => {
4214
+ void reader?.cancel(signal.reason).catch(() => {
4215
+ });
4216
+ };
4217
+ signal.addEventListener("abort", abort, { once: true });
4218
+ try {
4219
+ if (reader)
4220
+ while (true) {
4221
+ signal.throwIfAborted();
4222
+ const { done, value } = await reader.read();
4223
+ signal.throwIfAborted();
4224
+ if (done)
4225
+ break;
4226
+ size += value.byteLength;
4227
+ if (size > limit) {
4228
+ await reader.cancel();
4229
+ throw new FleetClientResponseError("response_too_large", response.status);
4230
+ }
4231
+ chunks.push(value);
4232
+ }
4233
+ } finally {
4234
+ signal.removeEventListener("abort", abort);
4235
+ if (signal.aborted)
4236
+ await reader?.cancel(signal.reason).catch(() => {
4237
+ });
4238
+ reader?.releaseLock();
4239
+ }
4240
+ const bytes2 = new Uint8Array(size);
4241
+ let offset = 0;
4242
+ for (const chunk of chunks) {
4243
+ bytes2.set(chunk, offset);
4244
+ offset += chunk.byteLength;
4245
+ }
4246
+ const text2 = new TextDecoder().decode(bytes2);
4247
+ let data;
4248
+ try {
4249
+ data = text2.length ? JSON.parse(text2) : void 0;
4250
+ } catch {
4251
+ if (response.ok)
4252
+ throw new FleetClientResponseError("invalid_json", response.status);
4253
+ }
4254
+ if (!response.ok) {
4255
+ const error = data && typeof data === "object" ? data.error : void 0;
4256
+ const detail = { body: "omitted" };
4257
+ if (error && typeof error === "object")
4258
+ for (const key of ["type", "code"]) {
4259
+ const value = error[key];
4260
+ if (typeof value === "string" && /^[A-Za-z0-9_.-]{1,128}$/.test(value))
4261
+ detail[key] = value;
4262
+ }
4263
+ throw new FleetClientError(response.status, detail);
4264
+ }
4060
4265
  return data;
4061
4266
  }
4062
4267
  discover() {
@@ -4122,17 +4327,52 @@ function renderConnections(manifest, privateManifest, namespace, mapping) {
4122
4327
  const child = manifest.services[entry.service], secret = privateManifest.services[entry.service];
4123
4328
  if (!child || !secret)
4124
4329
  throw new Error(`missing service mapping ${entry.service}`);
4330
+ let endpoint;
4125
4331
  if (child.protocol === "http") {
4126
4332
  const template = child.namespaces.path;
4127
4333
  if (typeof template !== "string")
4128
4334
  throw new Error(`missing HTTP namespace mapping ${entry.service}`);
4129
- result[variable] = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
4335
+ endpoint = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
4130
4336
  } else {
4131
- const endpoint = secret.namespaces?.[namespace];
4132
- if (!endpoint)
4337
+ const selected = secret.namespaces?.[namespace];
4338
+ if (!selected)
4133
4339
  throw new Error(`missing namespace mapping ${namespace}/${entry.service}`);
4134
- result[variable] = endpoint;
4340
+ endpoint = selected;
4341
+ }
4342
+ const url = new URL(endpoint);
4343
+ const field = entry.field ?? "url";
4344
+ let value;
4345
+ switch (field) {
4346
+ case "url":
4347
+ value = endpoint;
4348
+ break;
4349
+ case "hostname":
4350
+ value = url.hostname.replace(/^\[|\]$/g, "");
4351
+ break;
4352
+ case "port":
4353
+ value = url.port;
4354
+ break;
4355
+ case "username":
4356
+ value = decodeURIComponent(url.username);
4357
+ break;
4358
+ case "password":
4359
+ value = decodeURIComponent(url.password);
4360
+ break;
4361
+ case "pathname":
4362
+ value = decodeURIComponent(url.pathname);
4363
+ break;
4364
+ case "database":
4365
+ value = decodeURIComponent(url.pathname.replace(/^\//, ""));
4366
+ break;
4367
+ default:
4368
+ throw new Error("unsupported connection field");
4135
4369
  }
4370
+ if (!value) {
4371
+ if (typeof entry.fallback !== "string")
4372
+ throw new Error(`missing connection component ${field}`);
4373
+ value = entry.fallback;
4374
+ }
4375
+ result[variable] = value;
4136
4376
  }
4137
4377
  return result;
4138
4378
  }
@@ -4156,7 +4396,7 @@ async function doctorFleet(config, options, probes = []) {
4156
4396
  const plan = [];
4157
4397
  try {
4158
4398
  const targets = await preflightFleet(config, options);
4159
- const require2 = createRequire2(resolve2(process.cwd(), "package.json"));
4399
+ const require2 = createRequire3(resolve3(process.cwd(), "package.json"));
4160
4400
  for (const [service, target] of targets) {
4161
4401
  let version = "development";
4162
4402
  try {
@@ -4211,7 +4451,7 @@ async function doctorFleet(config, options, probes = []) {
4211
4451
  };
4212
4452
  }
4213
4453
  async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e4) {
4214
- const archive = validateArchive(input);
4454
+ const archive = assertArchiveCompatibility(input, options.archiveCompatibility);
4215
4455
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4216
4456
  const isolated = {
4217
4457
  ...config,
@@ -4223,7 +4463,7 @@ async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e
4223
4463
  };
4224
4464
  let fleet;
4225
4465
  try {
4226
- fleet = await startFleet(isolated, { load: options.load });
4466
+ fleet = await startFleet(isolated, options);
4227
4467
  const client = new FleetClient(fleet.manifest.adminBase, {
4228
4468
  ...config.adminKey ? { adminKey: config.adminKey } : {}
4229
4469
  });
@@ -4450,6 +4690,16 @@ var COMMON_SERVE_OPTIONS = {
4450
4690
  value: "<seed>",
4451
4691
  description: "Seed for every random choice (env MOCKINGBIRD_SEED)"
4452
4692
  },
4693
+ "id-base": {
4694
+ type: "string",
4695
+ value: "<n|boot>",
4696
+ 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)"
4697
+ },
4698
+ settings: {
4699
+ type: "string",
4700
+ value: "<json|file>",
4701
+ 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)"
4702
+ },
4453
4703
  "admin-controls": {
4454
4704
  type: "string",
4455
4705
  value: "<json|file>",
@@ -4544,19 +4794,40 @@ var readJsonFile = async (path, label) => {
4544
4794
  }
4545
4795
  };
4546
4796
  var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4797
+ var readJsonOption = async (source, label) => {
4798
+ const trimmed = source.trim();
4799
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
4800
+ try {
4801
+ return JSON.parse(trimmed);
4802
+ } catch {
4803
+ throw new Error(`${label}: not valid JSON`);
4804
+ }
4805
+ }
4806
+ return readJsonFile(source, label);
4807
+ };
4808
+ var resolveServeIdBase = (values, env = process.env) => {
4809
+ const source = asString(values["id-base"]) ?? env.MOCKINGBIRD_ID_BASE;
4810
+ if (source === void 0 || source.trim() === "")
4811
+ return void 0;
4812
+ normalizeIdBase(source.trim(), "--id-base");
4813
+ return source.trim();
4814
+ };
4815
+ var resolveServeSettings = async (target, values, env = process.env) => {
4816
+ const flag = asString(values.settings);
4817
+ const source = flag ?? env.MOCKINGBIRD_SETTINGS;
4818
+ if (source === void 0 || source.trim() === "")
4819
+ return void 0;
4820
+ const label = flag !== void 0 ? "--settings" : "MOCKINGBIRD_SETTINGS";
4821
+ const parsed = await readJsonOption(source, label);
4822
+ if (!isObject2(parsed))
4823
+ throw new Error(`${label}: expected a JSON object`);
4824
+ return checkSettings(target, parsed, label);
4825
+ };
4547
4826
  var resolveServeControls = async (values, env = process.env, base) => {
4548
4827
  const source = asString(values["admin-controls"]) ?? env.MOCKINGBIRD_ADMIN_CONTROLS;
4549
4828
  let document = isObject2(base) ? { ...base } : {};
4550
4829
  if (source !== void 0 && source.trim() !== "") {
4551
- let parsed;
4552
- if (source.trim().startsWith("{")) {
4553
- try {
4554
- parsed = JSON.parse(source);
4555
- } catch {
4556
- throw new Error("--admin-controls: not valid JSON");
4557
- }
4558
- } else
4559
- parsed = await readJsonFile(source, "--admin-controls");
4830
+ const parsed = source.trim().startsWith("{") ? await readJsonOption(source, "--admin-controls") : await readJsonFile(source, "--admin-controls");
4560
4831
  if (!isObject2(parsed))
4561
4832
  throw new Error("--admin-controls: expected a JSON object");
4562
4833
  document = { ...document, ...parsed };
@@ -4598,26 +4869,7 @@ var resolveServeControls = async (values, env = process.env, base) => {
4598
4869
  normalizeBootControls(document, "adminControls");
4599
4870
  return document;
4600
4871
  };
4601
- var loadTarget = async (name2, own) => {
4602
- if (name2 === own.name)
4603
- return own;
4604
- const specifier = `@crvouga/mockingbird-service-${name2}/server`;
4605
- try {
4606
- let mod;
4607
- try {
4608
- mod = await import(specifier);
4609
- } catch {
4610
- const fromProject = createRequire3(resolve3(process.cwd(), "package.json"));
4611
- mod = await import(pathToFileURL(fromProject.resolve(specifier)).href);
4612
- }
4613
- if (!mod.serveTarget)
4614
- throw new Error(`${specifier} exports no serveTarget`);
4615
- return mod.serveTarget;
4616
- } catch (error) {
4617
- const reason = error instanceof Error ? error.message : String(error);
4618
- throw new Error(`cannot load service "${name2}": ${reason}. Install @crvouga/mockingbird-service-${name2}.`);
4619
- }
4620
- };
4872
+ var loadTarget = (name2, own) => createInstalledServiceLoader({ projectRoot: process.cwd(), services: [name2], targets: [own] })(name2);
4621
4873
  var start = async (target, values, config) => {
4622
4874
  const adminPrefix = resolveAdminPrefix(config.adminPrefix);
4623
4875
  const runtime = await target.create(values, {
@@ -4625,8 +4877,11 @@ var start = async (target, values, config) => {
4625
4877
  adminKey: config.adminKey,
4626
4878
  seed: config.seed,
4627
4879
  onLog: formatLog(config.log),
4880
+ ...config.settings !== void 0 ? { settings: config.settings } : {},
4628
4881
  ...config.adminControls !== void 0 ? { adminControls: config.adminControls } : {}
4629
4882
  });
4883
+ if (config.idBase !== void 0)
4884
+ runtime.setIdBase?.(config.idBase);
4630
4885
  const listening = await listen(runtime, { port: config.port, host: config.host });
4631
4886
  target.listening?.(listening);
4632
4887
  console.log(`${target.name} emulator listening on ${listening.url}`);
@@ -4672,6 +4927,11 @@ var serveCommand = (target) => ({
4672
4927
  const adminControls = await resolveServeControls(values, process.env, config.adminControls);
4673
4928
  if (adminControls !== void 0)
4674
4929
  config.adminControls = adminControls;
4930
+ if (values.settings !== void 0)
4931
+ throw new Error("--settings serves a single service; give each service its own `settings` in the config");
4932
+ const idBase = config.idBase ?? resolveServeIdBase(values);
4933
+ if (idBase !== void 0)
4934
+ config.idBase = idBase;
4675
4935
  const fleet = await startFleet(config, {
4676
4936
  load: (name2) => loadTarget(name2, target),
4677
4937
  onLog: formatLog(config.log ?? log),
@@ -4701,8 +4961,12 @@ var serveCommand = (target) => ({
4701
4961
  let listening;
4702
4962
  try {
4703
4963
  const adminControls = await resolveServeControls(values);
4964
+ const idBase = resolveServeIdBase(values);
4965
+ const settings = await resolveServeSettings(target, values);
4704
4966
  listening = await start(target, values, {
4705
4967
  ...adminControls !== void 0 ? { adminControls } : {},
4968
+ ...idBase !== void 0 ? { idBase } : {},
4969
+ ...settings !== void 0 ? { settings } : {},
4706
4970
  adminPrefix: resolveAdminPrefix(asString(values["admin-prefix"]) ?? process.env.MOCKINGBIRD_ADMIN_PREFIX),
4707
4971
  port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
4708
4972
  host: asString(values.host) ?? "127.0.0.1",
@@ -4976,4 +5240,4 @@ export {
4976
5240
  createServer2 as createServer,
4977
5241
  serveTarget
4978
5242
  };
4979
- //# sourceMappingURL=chunk-RTJ3O3AX.js.map
5243
+ //# sourceMappingURL=chunk-GRXH63CD.js.map