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