@crvouga/mockingbird-service-otel 3.6.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,12 @@
1
1
  # Changelog — @crvouga/mockingbird-service-otel
2
2
 
3
- ## 3.6.0 (2026-10-16)
3
+ ## 3.7.0 (2026-10-17)
4
+
5
+ ### Features
6
+
7
+ - resolve fleet and emulator issue batch ([26d2f8c](https://github.com/crvouga/mockingbird/commit/26d2f8cee99fefed93192204bac44fb1be35c1da))
8
+
9
+ ## 3.6.0 (2026-10-15)
4
10
 
5
11
  ### Features
6
12
 
@@ -3668,7 +3668,7 @@ var stateAdminRoutes = (routes) => {
3668
3668
  };
3669
3669
 
3670
3670
  // ../core/dist/version.js
3671
- var PACKAGE_VERSION = true ? "3.6.0" : UNRELEASED_VERSION;
3671
+ var PACKAGE_VERSION = true ? "3.7.0" : UNRELEASED_VERSION;
3672
3672
 
3673
3673
  // ../core/dist/webhooks.js
3674
3674
  var json4 = (status, body) => new Response(JSON.stringify(body), {
@@ -13,9 +13,6 @@ import {
13
13
 
14
14
  // ../../adapters/node/dist/cli.js
15
15
  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
16
  import { parseArgs } from "node:util";
20
17
 
21
18
  // ../../adapters/node/dist/fleet.js
@@ -29,6 +26,31 @@ import { createHash as createHash3, randomBytes, randomUUID as randomUUID3 } fro
29
26
 
30
27
  // ../../adapters/node/dist/fleet-archive.js
31
28
  import { createHash } from "node:crypto";
29
+ function validateCompatibilityTags(tags) {
30
+ if (!tags || typeof tags !== "object" || Array.isArray(tags))
31
+ throw new Error("invalid archive compatibility tags");
32
+ const entries = Object.entries(tags);
33
+ 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)))
34
+ throw new Error("invalid archive compatibility tags");
35
+ }
36
+ function assertCompatibilityTags(tags, policy) {
37
+ if (tags !== void 0)
38
+ validateCompatibilityTags(tags);
39
+ if (!policy)
40
+ return;
41
+ validateCompatibilityTags(policy.tags);
42
+ if (policy.missing !== void 0 && !["allow", "reject"].includes(policy.missing))
43
+ throw new Error("invalid missing compatibility policy");
44
+ if (tags === void 0 && policy.missing === "allow")
45
+ return;
46
+ if (canonical(tags) !== canonical(policy.tags))
47
+ throw new Error("application archive compatibility mismatch");
48
+ }
49
+ function assertArchiveCompatibility(input, policy) {
50
+ const archive = validateArchive(input);
51
+ assertCompatibilityTags(archive.compatibility, policy);
52
+ return archive;
53
+ }
32
54
  var canonical = (value) => {
33
55
  if (Array.isArray(value))
34
56
  return `[${value.map(canonical).join(",")}]`;
@@ -48,6 +70,8 @@ function validateArchive(value) {
48
70
  const { sha256, ...content } = doc;
49
71
  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
72
  throw new Error("unsupported or corrupt fleet archive");
73
+ if (doc.compatibility !== void 0)
74
+ validateCompatibilityTags(doc.compatibility);
51
75
  for (const [namespace, entry] of Object.entries(doc.namespaces)) {
52
76
  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
77
  throw new Error("invalid archive namespace");
@@ -63,6 +87,7 @@ function inspectArchive(input) {
63
87
  format: archive.format,
64
88
  createdAt: archive.createdAt,
65
89
  sha256: archive.sha256,
90
+ ...archive.compatibility ? { compatibility: archive.compatibility } : {},
66
91
  namespaces: Object.fromEntries(Object.entries(archive.namespaces).map(([name, ns]) => [
67
92
  name,
68
93
  {
@@ -705,11 +730,28 @@ var ingressSupport = {
705
730
  streaming: true,
706
731
  signing: "vendor request signing is not used; webhook signatures remain payload based"
707
732
  },
733
+ cognito: {
734
+ publicOrigin: true,
735
+ pathPrefix: true,
736
+ namespaces: "path-or-header",
737
+ streaming: true,
738
+ signing: "JWT issuer and JWKS use the configured public namespace; SigV4 verification is not modelled"
739
+ },
740
+ medplum: {
741
+ publicOrigin: true,
742
+ pathPrefix: true,
743
+ namespaces: "path-or-header",
744
+ streaming: true,
745
+ signing: "FHIR, OAuth and storage URLs use the configured public namespace"
746
+ },
708
747
  other: {
709
748
  publicOrigin: false,
710
749
  reason: "host/path-sensitive signing and hosted URL rewriting are not certified; use a direct endpoint"
711
750
  }
712
751
  };
752
+ function supportsPublicIngress(service) {
753
+ return service === "stripe" || service === "cognito" || service === "medplum";
754
+ }
713
755
  function advertisedHttpBase(entry) {
714
756
  if (entry.publicPath !== void 0 && !entry.publicOrigin)
715
757
  throw new Error("publicPath requires publicOrigin");
@@ -1248,7 +1290,7 @@ data: ${JSON.stringify(data)}
1248
1290
  {
1249
1291
  protocol: c.protocol,
1250
1292
  coverage: c.coverage,
1251
- ingress: c.targetName === "stripe" ? ingressSupport.stripe : ingressSupport.other,
1293
+ ingress: supportsPublicIngress(c.targetName ?? "") ? ingressSupport[c.targetName] : ingressSupport.other,
1252
1294
  target: c.targetName,
1253
1295
  outboundPolicy: c.outboundPolicy ?? "unsupported",
1254
1296
  version: 1,
@@ -2332,11 +2374,12 @@ data: ${JSON.stringify(data)}
2332
2374
  format: "mockingbird.fleet/v1",
2333
2375
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2334
2376
  sourceId: this.ctx.id,
2377
+ ...this.ctx.options.archiveCompatibility ? { compatibility: structuredClone(this.ctx.options.archiveCompatibility.tags) } : {},
2335
2378
  namespaces: contents
2336
2379
  });
2337
2380
  }
2338
2381
  async restoreArchive(input, selectedNamespace) {
2339
- const archive = validateArchive(input);
2382
+ const archive = assertArchiveCompatibility(input, this.ctx.options.archiveCompatibility);
2340
2383
  const entries = Object.entries(archive.namespaces).filter(([ns]) => selectedNamespace === void 0 || ns === selectedNamespace);
2341
2384
  if (!entries.length)
2342
2385
  throw new Error("selected namespace absent from archive");
@@ -2920,7 +2963,7 @@ async function preflightFleet(config, options) {
2920
2963
  if (entry.target !== void 0 && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.target))
2921
2964
  throw new Error("target must name a supported service suffix");
2922
2965
  const service = entry.target ?? name;
2923
- if (advertisedHttpBase(entry) && service !== "stripe")
2966
+ if (advertisedHttpBase(entry) && !supportsPublicIngress(service))
2924
2967
  throw new Error(`public ingress unsupported for ${service}; use a direct endpoint to preserve signed targets`);
2925
2968
  const target = await options.load(service);
2926
2969
  const protocol = "protocol" in target ? target.protocol : "http";
@@ -2971,7 +3014,7 @@ async function preflightFleet(config, options) {
2971
3014
  break;
2972
3015
  }
2973
3016
  }
2974
- targets.set(name, { ...target, packageVersion: packageVersion ?? "3.6.0" });
3017
+ targets.set(name, { ...target, packageVersion: packageVersion ?? "3.7.0" });
2975
3018
  } catch (error) {
2976
3019
  throw new Error(`service ${name}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2977
3020
  }
@@ -3015,6 +3058,11 @@ async function preflightFleet(config, options) {
3015
3058
  return ordered;
3016
3059
  }
3017
3060
  function validateFleetPolicies(options) {
3061
+ if (options.archiveCompatibility) {
3062
+ validateCompatibilityTags(options.archiveCompatibility.tags);
3063
+ if (options.archiveCompatibility.missing !== void 0 && !["allow", "reject"].includes(options.archiveCompatibility.missing))
3064
+ throw new Error("archiveCompatibility.missing must be allow or reject");
3065
+ }
3018
3066
  if (options.journal && !options.repository)
3019
3067
  throw new Error("durable operation journal requires an archive repository");
3020
3068
  if (options.cors) {
@@ -4007,10 +4055,44 @@ async function startFleet(config, options) {
4007
4055
  }
4008
4056
  }
4009
4057
 
4010
- // ../../adapters/node/dist/fleet-tools.js
4011
- import { readFile as readFile4 } from "node:fs/promises";
4058
+ // ../../adapters/node/dist/fleet-loader.js
4012
4059
  import { createRequire as createRequire2 } from "node:module";
4013
4060
  import { resolve as resolve2 } from "node:path";
4061
+ import { pathToFileURL } from "node:url";
4062
+ var suffix = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4063
+ function createInstalledServiceLoader(options) {
4064
+ if (!options.projectRoot)
4065
+ throw new Error("consumer projectRoot required");
4066
+ const allowed = new Set(options.services);
4067
+ for (const name of allowed)
4068
+ if (!suffix.test(name))
4069
+ throw new Error("invalid service suffix");
4070
+ const require2 = createRequire2(resolve2(options.projectRoot, "package.json"));
4071
+ const targets = new Map((options.targets ?? []).map((target) => [target.name, target]));
4072
+ return async (name) => {
4073
+ if (!suffix.test(name) || !allowed.has(name))
4074
+ throw new Error("service is not declared in loader allowlist");
4075
+ const own = targets.get(name);
4076
+ if (own)
4077
+ return own;
4078
+ const specifier = `@crvouga/mockingbird-service-${name}/server`;
4079
+ let location;
4080
+ try {
4081
+ location = require2.resolve(specifier);
4082
+ } catch {
4083
+ throw new Error(`cannot load service "${name}": install @crvouga/mockingbird-service-${name} in the consumer project; its /server export is required`);
4084
+ }
4085
+ const mod = await import(pathToFileURL(location).href);
4086
+ if (!mod.serveTarget || mod.serveTarget.name !== name || ("protocol" in mod.serveTarget ? typeof mod.serveTarget.start !== "function" : typeof mod.serveTarget.create !== "function"))
4087
+ throw new Error(`${specifier} exports no matching serveTarget`);
4088
+ return mod.serveTarget;
4089
+ };
4090
+ }
4091
+
4092
+ // ../../adapters/node/dist/fleet-tools.js
4093
+ import { readFile as readFile4 } from "node:fs/promises";
4094
+ import { createRequire as createRequire3 } from "node:module";
4095
+ import { resolve as resolve3 } from "node:path";
4014
4096
 
4015
4097
  // ../../adapters/node/dist/fleet-client.js
4016
4098
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -4024,6 +4106,14 @@ var FleetClientError = class extends Error {
4024
4106
  this.details = details;
4025
4107
  }
4026
4108
  };
4109
+ var FleetClientResponseError = class extends FleetClientError {
4110
+ code;
4111
+ constructor(code, status, origin) {
4112
+ super(status, { code, body: "omitted", ...origin ? { origin } : {} });
4113
+ this.code = code;
4114
+ this.name = "FleetClientResponseError";
4115
+ }
4116
+ };
4027
4117
  var FleetClient = class {
4028
4118
  base;
4029
4119
  options;
@@ -4036,7 +4126,14 @@ var FleetClient = class {
4036
4126
  if (url.username || url.password)
4037
4127
  throw new Error("fleet client URL must not contain credentials");
4038
4128
  url.searchParams.set("namespace", options.namespace ?? this.options.namespace ?? "default");
4129
+ const limit = options.maxResponseBytes ?? this.options.maxResponseBytes ?? 64 * 1024 * 1024;
4130
+ if (!Number.isSafeInteger(limit) || limit < 1)
4131
+ throw new Error("invalid response byte limit");
4132
+ const deadline = AbortSignal.timeout(this.options.timeoutMs ?? 3e4);
4133
+ const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
4134
+ signal.throwIfAborted();
4039
4135
  const response = await (this.options.fetch ?? fetch)(url, {
4136
+ redirect: "manual",
4040
4137
  method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
4041
4138
  headers: {
4042
4139
  "content-type": "application/json",
@@ -4044,11 +4141,71 @@ var FleetClient = class {
4044
4141
  ...options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}
4045
4142
  },
4046
4143
  ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
4047
- signal: options.signal ?? AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
4144
+ signal
4048
4145
  });
4049
- const data = await response.json();
4050
- if (!response.ok)
4051
- throw new FleetClientError(response.status, data);
4146
+ if (response.status >= 300 && response.status < 400) {
4147
+ await response.body?.cancel();
4148
+ throw new FleetClientResponseError("redirect", response.status, url.origin);
4149
+ }
4150
+ if (Number(response.headers.get("content-length")) > limit) {
4151
+ await response.body?.cancel();
4152
+ throw new FleetClientResponseError("response_too_large", response.status);
4153
+ }
4154
+ const reader = response.body?.getReader();
4155
+ const chunks = [];
4156
+ let size = 0;
4157
+ const abort = () => {
4158
+ void reader?.cancel(signal.reason).catch(() => {
4159
+ });
4160
+ };
4161
+ signal.addEventListener("abort", abort, { once: true });
4162
+ try {
4163
+ if (reader)
4164
+ while (true) {
4165
+ signal.throwIfAborted();
4166
+ const { done, value } = await reader.read();
4167
+ signal.throwIfAborted();
4168
+ if (done)
4169
+ break;
4170
+ size += value.byteLength;
4171
+ if (size > limit) {
4172
+ await reader.cancel();
4173
+ throw new FleetClientResponseError("response_too_large", response.status);
4174
+ }
4175
+ chunks.push(value);
4176
+ }
4177
+ } finally {
4178
+ signal.removeEventListener("abort", abort);
4179
+ if (signal.aborted)
4180
+ await reader?.cancel(signal.reason).catch(() => {
4181
+ });
4182
+ reader?.releaseLock();
4183
+ }
4184
+ const bytes = new Uint8Array(size);
4185
+ let offset = 0;
4186
+ for (const chunk of chunks) {
4187
+ bytes.set(chunk, offset);
4188
+ offset += chunk.byteLength;
4189
+ }
4190
+ const text2 = new TextDecoder().decode(bytes);
4191
+ let data;
4192
+ try {
4193
+ data = text2.length ? JSON.parse(text2) : void 0;
4194
+ } catch {
4195
+ if (response.ok)
4196
+ throw new FleetClientResponseError("invalid_json", response.status);
4197
+ }
4198
+ if (!response.ok) {
4199
+ const error = data && typeof data === "object" ? data.error : void 0;
4200
+ const detail = { body: "omitted" };
4201
+ if (error && typeof error === "object")
4202
+ for (const key of ["type", "code"]) {
4203
+ const value = error[key];
4204
+ if (typeof value === "string" && /^[A-Za-z0-9_.-]{1,128}$/.test(value))
4205
+ detail[key] = value;
4206
+ }
4207
+ throw new FleetClientError(response.status, detail);
4208
+ }
4052
4209
  return data;
4053
4210
  }
4054
4211
  discover() {
@@ -4114,17 +4271,52 @@ function renderConnections(manifest, privateManifest, namespace, mapping) {
4114
4271
  const child = manifest.services[entry.service], secret = privateManifest.services[entry.service];
4115
4272
  if (!child || !secret)
4116
4273
  throw new Error(`missing service mapping ${entry.service}`);
4274
+ let endpoint;
4117
4275
  if (child.protocol === "http") {
4118
4276
  const template = child.namespaces.path;
4119
4277
  if (typeof template !== "string")
4120
4278
  throw new Error(`missing HTTP namespace mapping ${entry.service}`);
4121
- result[variable] = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
4279
+ endpoint = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
4122
4280
  } else {
4123
- const endpoint = secret.namespaces?.[namespace];
4124
- if (!endpoint)
4281
+ const selected = secret.namespaces?.[namespace];
4282
+ if (!selected)
4125
4283
  throw new Error(`missing namespace mapping ${namespace}/${entry.service}`);
4126
- result[variable] = endpoint;
4284
+ endpoint = selected;
4127
4285
  }
4286
+ const url = new URL(endpoint);
4287
+ const field = entry.field ?? "url";
4288
+ let value;
4289
+ switch (field) {
4290
+ case "url":
4291
+ value = endpoint;
4292
+ break;
4293
+ case "hostname":
4294
+ value = url.hostname.replace(/^\[|\]$/g, "");
4295
+ break;
4296
+ case "port":
4297
+ value = url.port;
4298
+ break;
4299
+ case "username":
4300
+ value = decodeURIComponent(url.username);
4301
+ break;
4302
+ case "password":
4303
+ value = decodeURIComponent(url.password);
4304
+ break;
4305
+ case "pathname":
4306
+ value = decodeURIComponent(url.pathname);
4307
+ break;
4308
+ case "database":
4309
+ value = decodeURIComponent(url.pathname.replace(/^\//, ""));
4310
+ break;
4311
+ default:
4312
+ throw new Error("unsupported connection field");
4313
+ }
4314
+ if (!value) {
4315
+ if (typeof entry.fallback !== "string")
4316
+ throw new Error(`missing connection component ${field}`);
4317
+ value = entry.fallback;
4318
+ }
4319
+ result[variable] = value;
4128
4320
  }
4129
4321
  return result;
4130
4322
  }
@@ -4148,7 +4340,7 @@ async function doctorFleet(config, options, probes = []) {
4148
4340
  const plan = [];
4149
4341
  try {
4150
4342
  const targets = await preflightFleet(config, options);
4151
- const require2 = createRequire2(resolve2(process.cwd(), "package.json"));
4343
+ const require2 = createRequire3(resolve3(process.cwd(), "package.json"));
4152
4344
  for (const [service, target] of targets) {
4153
4345
  let version = "development";
4154
4346
  try {
@@ -4203,7 +4395,7 @@ async function doctorFleet(config, options, probes = []) {
4203
4395
  };
4204
4396
  }
4205
4397
  async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e4) {
4206
- const archive = validateArchive(input);
4398
+ const archive = assertArchiveCompatibility(input, options.archiveCompatibility);
4207
4399
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4208
4400
  const isolated = {
4209
4401
  ...config,
@@ -4215,7 +4407,7 @@ async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e
4215
4407
  };
4216
4408
  let fleet;
4217
4409
  try {
4218
- fleet = await startFleet(isolated, { load: options.load });
4410
+ fleet = await startFleet(isolated, options);
4219
4411
  const client = new FleetClient(fleet.manifest.adminBase, {
4220
4412
  ...config.adminKey ? { adminKey: config.adminKey } : {}
4221
4413
  });
@@ -4590,26 +4782,7 @@ var resolveServeControls = async (values, env = process.env, base) => {
4590
4782
  normalizeBootControls(document, "adminControls");
4591
4783
  return document;
4592
4784
  };
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
- };
4785
+ var loadTarget = (name, own) => createInstalledServiceLoader({ projectRoot: process.cwd(), services: [name], targets: [own] })(name);
4613
4786
  var start = async (target, values, config) => {
4614
4787
  const adminPrefix = resolveAdminPrefix(config.adminPrefix);
4615
4788
  const runtime = await target.create(values, {
@@ -4811,4 +4984,4 @@ export {
4811
4984
  createServer2 as createServer,
4812
4985
  serveTarget
4813
4986
  };
4814
- //# sourceMappingURL=chunk-XA2FY3X7.js.map
4987
+ //# sourceMappingURL=chunk-OO5YA5M3.js.map