@crvouga/mockingbird-service-rxvortex 3.5.0 → 3.6.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-rxvortex
2
2
 
3
- ## 3.5.0 (2026-10-16)
3
+ ## 3.6.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.5.0 (2026-10-15)
4
10
 
5
11
  ### Features
6
12
 
@@ -3970,7 +3970,7 @@ var stateAdminRoutes = (routes) => {
3970
3970
  };
3971
3971
 
3972
3972
  // ../core/dist/version.js
3973
- var PACKAGE_VERSION = true ? "3.5.0" : UNRELEASED_VERSION;
3973
+ var PACKAGE_VERSION = true ? "3.6.0" : UNRELEASED_VERSION;
3974
3974
 
3975
3975
  // ../core/dist/signing.js
3976
3976
  var encoder = new TextEncoder();
@@ -16,9 +16,6 @@ import { readFileSync } from "node:fs";
16
16
 
17
17
  // ../../adapters/node/dist/cli.js
18
18
  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
19
  import { parseArgs } from "node:util";
23
20
 
24
21
  // ../../adapters/node/dist/fleet.js
@@ -32,6 +29,31 @@ import { createHash as createHash3, randomBytes, randomUUID as randomUUID3 } fro
32
29
 
33
30
  // ../../adapters/node/dist/fleet-archive.js
34
31
  import { createHash } from "node:crypto";
32
+ function validateCompatibilityTags(tags) {
33
+ if (!tags || typeof tags !== "object" || Array.isArray(tags))
34
+ throw new Error("invalid archive compatibility tags");
35
+ const entries = Object.entries(tags);
36
+ 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)))
37
+ throw new Error("invalid archive compatibility tags");
38
+ }
39
+ function assertCompatibilityTags(tags, policy) {
40
+ if (tags !== void 0)
41
+ validateCompatibilityTags(tags);
42
+ if (!policy)
43
+ return;
44
+ validateCompatibilityTags(policy.tags);
45
+ if (policy.missing !== void 0 && !["allow", "reject"].includes(policy.missing))
46
+ throw new Error("invalid missing compatibility policy");
47
+ if (tags === void 0 && policy.missing === "allow")
48
+ return;
49
+ if (canonical(tags) !== canonical(policy.tags))
50
+ throw new Error("application archive compatibility mismatch");
51
+ }
52
+ function assertArchiveCompatibility(input, policy) {
53
+ const archive = validateArchive(input);
54
+ assertCompatibilityTags(archive.compatibility, policy);
55
+ return archive;
56
+ }
35
57
  var canonical = (value) => {
36
58
  if (Array.isArray(value))
37
59
  return `[${value.map(canonical).join(",")}]`;
@@ -51,6 +73,8 @@ function validateArchive(value) {
51
73
  const { sha256, ...content } = doc;
52
74
  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
75
  throw new Error("unsupported or corrupt fleet archive");
76
+ if (doc.compatibility !== void 0)
77
+ validateCompatibilityTags(doc.compatibility);
54
78
  for (const [namespace, entry] of Object.entries(doc.namespaces)) {
55
79
  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
80
  throw new Error("invalid archive namespace");
@@ -66,6 +90,7 @@ function inspectArchive(input) {
66
90
  format: archive.format,
67
91
  createdAt: archive.createdAt,
68
92
  sha256: archive.sha256,
93
+ ...archive.compatibility ? { compatibility: archive.compatibility } : {},
69
94
  namespaces: Object.fromEntries(Object.entries(archive.namespaces).map(([name, ns]) => [
70
95
  name,
71
96
  {
@@ -708,11 +733,28 @@ var ingressSupport = {
708
733
  streaming: true,
709
734
  signing: "vendor request signing is not used; webhook signatures remain payload based"
710
735
  },
736
+ cognito: {
737
+ publicOrigin: true,
738
+ pathPrefix: true,
739
+ namespaces: "path-or-header",
740
+ streaming: true,
741
+ signing: "JWT issuer and JWKS use the configured public namespace; SigV4 verification is not modelled"
742
+ },
743
+ medplum: {
744
+ publicOrigin: true,
745
+ pathPrefix: true,
746
+ namespaces: "path-or-header",
747
+ streaming: true,
748
+ signing: "FHIR, OAuth and storage URLs use the configured public namespace"
749
+ },
711
750
  other: {
712
751
  publicOrigin: false,
713
752
  reason: "host/path-sensitive signing and hosted URL rewriting are not certified; use a direct endpoint"
714
753
  }
715
754
  };
755
+ function supportsPublicIngress(service) {
756
+ return service === "stripe" || service === "cognito" || service === "medplum";
757
+ }
716
758
  function advertisedHttpBase(entry) {
717
759
  if (entry.publicPath !== void 0 && !entry.publicOrigin)
718
760
  throw new Error("publicPath requires publicOrigin");
@@ -1251,7 +1293,7 @@ data: ${JSON.stringify(data)}
1251
1293
  {
1252
1294
  protocol: c.protocol,
1253
1295
  coverage: c.coverage,
1254
- ingress: c.targetName === "stripe" ? ingressSupport.stripe : ingressSupport.other,
1296
+ ingress: supportsPublicIngress(c.targetName ?? "") ? ingressSupport[c.targetName] : ingressSupport.other,
1255
1297
  target: c.targetName,
1256
1298
  outboundPolicy: c.outboundPolicy ?? "unsupported",
1257
1299
  version: 1,
@@ -2335,11 +2377,12 @@ data: ${JSON.stringify(data)}
2335
2377
  format: "mockingbird.fleet/v1",
2336
2378
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2337
2379
  sourceId: this.ctx.id,
2380
+ ...this.ctx.options.archiveCompatibility ? { compatibility: structuredClone(this.ctx.options.archiveCompatibility.tags) } : {},
2338
2381
  namespaces: contents
2339
2382
  });
2340
2383
  }
2341
2384
  async restoreArchive(input, selectedNamespace) {
2342
- const archive = validateArchive(input);
2385
+ const archive = assertArchiveCompatibility(input, this.ctx.options.archiveCompatibility);
2343
2386
  const entries = Object.entries(archive.namespaces).filter(([ns]) => selectedNamespace === void 0 || ns === selectedNamespace);
2344
2387
  if (!entries.length)
2345
2388
  throw new Error("selected namespace absent from archive");
@@ -2923,7 +2966,7 @@ async function preflightFleet(config, options) {
2923
2966
  if (entry.target !== void 0 && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.target))
2924
2967
  throw new Error("target must name a supported service suffix");
2925
2968
  const service = entry.target ?? name;
2926
- if (advertisedHttpBase(entry) && service !== "stripe")
2969
+ if (advertisedHttpBase(entry) && !supportsPublicIngress(service))
2927
2970
  throw new Error(`public ingress unsupported for ${service}; use a direct endpoint to preserve signed targets`);
2928
2971
  const target = await options.load(service);
2929
2972
  const protocol = "protocol" in target ? target.protocol : "http";
@@ -2974,7 +3017,7 @@ async function preflightFleet(config, options) {
2974
3017
  break;
2975
3018
  }
2976
3019
  }
2977
- targets.set(name, { ...target, packageVersion: packageVersion ?? "3.5.0" });
3020
+ targets.set(name, { ...target, packageVersion: packageVersion ?? "3.6.0" });
2978
3021
  } catch (error) {
2979
3022
  throw new Error(`service ${name}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2980
3023
  }
@@ -3018,6 +3061,11 @@ async function preflightFleet(config, options) {
3018
3061
  return ordered;
3019
3062
  }
3020
3063
  function validateFleetPolicies(options) {
3064
+ if (options.archiveCompatibility) {
3065
+ validateCompatibilityTags(options.archiveCompatibility.tags);
3066
+ if (options.archiveCompatibility.missing !== void 0 && !["allow", "reject"].includes(options.archiveCompatibility.missing))
3067
+ throw new Error("archiveCompatibility.missing must be allow or reject");
3068
+ }
3021
3069
  if (options.journal && !options.repository)
3022
3070
  throw new Error("durable operation journal requires an archive repository");
3023
3071
  if (options.cors) {
@@ -4010,10 +4058,44 @@ async function startFleet(config, options) {
4010
4058
  }
4011
4059
  }
4012
4060
 
4013
- // ../../adapters/node/dist/fleet-tools.js
4014
- import { readFile as readFile4 } from "node:fs/promises";
4061
+ // ../../adapters/node/dist/fleet-loader.js
4015
4062
  import { createRequire as createRequire2 } from "node:module";
4016
4063
  import { resolve as resolve2 } from "node:path";
4064
+ import { pathToFileURL } from "node:url";
4065
+ var suffix = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4066
+ function createInstalledServiceLoader(options) {
4067
+ if (!options.projectRoot)
4068
+ throw new Error("consumer projectRoot required");
4069
+ const allowed = new Set(options.services);
4070
+ for (const name of allowed)
4071
+ if (!suffix.test(name))
4072
+ throw new Error("invalid service suffix");
4073
+ const require2 = createRequire2(resolve2(options.projectRoot, "package.json"));
4074
+ const targets = new Map((options.targets ?? []).map((target) => [target.name, target]));
4075
+ return async (name) => {
4076
+ if (!suffix.test(name) || !allowed.has(name))
4077
+ throw new Error("service is not declared in loader allowlist");
4078
+ const own = targets.get(name);
4079
+ if (own)
4080
+ return own;
4081
+ const specifier = `@crvouga/mockingbird-service-${name}/server`;
4082
+ let location;
4083
+ try {
4084
+ location = require2.resolve(specifier);
4085
+ } catch {
4086
+ throw new Error(`cannot load service "${name}": install @crvouga/mockingbird-service-${name} in the consumer project; its /server export is required`);
4087
+ }
4088
+ const mod = await import(pathToFileURL(location).href);
4089
+ if (!mod.serveTarget || mod.serveTarget.name !== name || ("protocol" in mod.serveTarget ? typeof mod.serveTarget.start !== "function" : typeof mod.serveTarget.create !== "function"))
4090
+ throw new Error(`${specifier} exports no matching serveTarget`);
4091
+ return mod.serveTarget;
4092
+ };
4093
+ }
4094
+
4095
+ // ../../adapters/node/dist/fleet-tools.js
4096
+ import { readFile as readFile4 } from "node:fs/promises";
4097
+ import { createRequire as createRequire3 } from "node:module";
4098
+ import { resolve as resolve3 } from "node:path";
4017
4099
 
4018
4100
  // ../../adapters/node/dist/fleet-client.js
4019
4101
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -4027,6 +4109,14 @@ var FleetClientError = class extends Error {
4027
4109
  this.details = details;
4028
4110
  }
4029
4111
  };
4112
+ var FleetClientResponseError = class extends FleetClientError {
4113
+ code;
4114
+ constructor(code, status, origin) {
4115
+ super(status, { code, body: "omitted", ...origin ? { origin } : {} });
4116
+ this.code = code;
4117
+ this.name = "FleetClientResponseError";
4118
+ }
4119
+ };
4030
4120
  var FleetClient = class {
4031
4121
  base;
4032
4122
  options;
@@ -4039,7 +4129,14 @@ var FleetClient = class {
4039
4129
  if (url.username || url.password)
4040
4130
  throw new Error("fleet client URL must not contain credentials");
4041
4131
  url.searchParams.set("namespace", options.namespace ?? this.options.namespace ?? "default");
4132
+ const limit = options.maxResponseBytes ?? this.options.maxResponseBytes ?? 64 * 1024 * 1024;
4133
+ if (!Number.isSafeInteger(limit) || limit < 1)
4134
+ throw new Error("invalid response byte limit");
4135
+ const deadline = AbortSignal.timeout(this.options.timeoutMs ?? 3e4);
4136
+ const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
4137
+ signal.throwIfAborted();
4042
4138
  const response = await (this.options.fetch ?? fetch)(url, {
4139
+ redirect: "manual",
4043
4140
  method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
4044
4141
  headers: {
4045
4142
  "content-type": "application/json",
@@ -4047,11 +4144,71 @@ var FleetClient = class {
4047
4144
  ...options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}
4048
4145
  },
4049
4146
  ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
4050
- signal: options.signal ?? AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
4147
+ signal
4051
4148
  });
4052
- const data = await response.json();
4053
- if (!response.ok)
4054
- throw new FleetClientError(response.status, data);
4149
+ if (response.status >= 300 && response.status < 400) {
4150
+ await response.body?.cancel();
4151
+ throw new FleetClientResponseError("redirect", response.status, url.origin);
4152
+ }
4153
+ if (Number(response.headers.get("content-length")) > limit) {
4154
+ await response.body?.cancel();
4155
+ throw new FleetClientResponseError("response_too_large", response.status);
4156
+ }
4157
+ const reader = response.body?.getReader();
4158
+ const chunks = [];
4159
+ let size = 0;
4160
+ const abort = () => {
4161
+ void reader?.cancel(signal.reason).catch(() => {
4162
+ });
4163
+ };
4164
+ signal.addEventListener("abort", abort, { once: true });
4165
+ try {
4166
+ if (reader)
4167
+ while (true) {
4168
+ signal.throwIfAborted();
4169
+ const { done, value } = await reader.read();
4170
+ signal.throwIfAborted();
4171
+ if (done)
4172
+ break;
4173
+ size += value.byteLength;
4174
+ if (size > limit) {
4175
+ await reader.cancel();
4176
+ throw new FleetClientResponseError("response_too_large", response.status);
4177
+ }
4178
+ chunks.push(value);
4179
+ }
4180
+ } finally {
4181
+ signal.removeEventListener("abort", abort);
4182
+ if (signal.aborted)
4183
+ await reader?.cancel(signal.reason).catch(() => {
4184
+ });
4185
+ reader?.releaseLock();
4186
+ }
4187
+ const bytes = new Uint8Array(size);
4188
+ let offset = 0;
4189
+ for (const chunk of chunks) {
4190
+ bytes.set(chunk, offset);
4191
+ offset += chunk.byteLength;
4192
+ }
4193
+ const text2 = new TextDecoder().decode(bytes);
4194
+ let data;
4195
+ try {
4196
+ data = text2.length ? JSON.parse(text2) : void 0;
4197
+ } catch {
4198
+ if (response.ok)
4199
+ throw new FleetClientResponseError("invalid_json", response.status);
4200
+ }
4201
+ if (!response.ok) {
4202
+ const error = data && typeof data === "object" ? data.error : void 0;
4203
+ const detail = { body: "omitted" };
4204
+ if (error && typeof error === "object")
4205
+ for (const key of ["type", "code"]) {
4206
+ const value = error[key];
4207
+ if (typeof value === "string" && /^[A-Za-z0-9_.-]{1,128}$/.test(value))
4208
+ detail[key] = value;
4209
+ }
4210
+ throw new FleetClientError(response.status, detail);
4211
+ }
4055
4212
  return data;
4056
4213
  }
4057
4214
  discover() {
@@ -4117,17 +4274,52 @@ function renderConnections(manifest, privateManifest, namespace, mapping) {
4117
4274
  const child = manifest.services[entry.service], secret = privateManifest.services[entry.service];
4118
4275
  if (!child || !secret)
4119
4276
  throw new Error(`missing service mapping ${entry.service}`);
4277
+ let endpoint;
4120
4278
  if (child.protocol === "http") {
4121
4279
  const template = child.namespaces.path;
4122
4280
  if (typeof template !== "string")
4123
4281
  throw new Error(`missing HTTP namespace mapping ${entry.service}`);
4124
- result[variable] = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
4282
+ endpoint = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
4125
4283
  } else {
4126
- const endpoint = secret.namespaces?.[namespace];
4127
- if (!endpoint)
4284
+ const selected = secret.namespaces?.[namespace];
4285
+ if (!selected)
4128
4286
  throw new Error(`missing namespace mapping ${namespace}/${entry.service}`);
4129
- result[variable] = endpoint;
4287
+ endpoint = selected;
4130
4288
  }
4289
+ const url = new URL(endpoint);
4290
+ const field = entry.field ?? "url";
4291
+ let value;
4292
+ switch (field) {
4293
+ case "url":
4294
+ value = endpoint;
4295
+ break;
4296
+ case "hostname":
4297
+ value = url.hostname.replace(/^\[|\]$/g, "");
4298
+ break;
4299
+ case "port":
4300
+ value = url.port;
4301
+ break;
4302
+ case "username":
4303
+ value = decodeURIComponent(url.username);
4304
+ break;
4305
+ case "password":
4306
+ value = decodeURIComponent(url.password);
4307
+ break;
4308
+ case "pathname":
4309
+ value = decodeURIComponent(url.pathname);
4310
+ break;
4311
+ case "database":
4312
+ value = decodeURIComponent(url.pathname.replace(/^\//, ""));
4313
+ break;
4314
+ default:
4315
+ throw new Error("unsupported connection field");
4316
+ }
4317
+ if (!value) {
4318
+ if (typeof entry.fallback !== "string")
4319
+ throw new Error(`missing connection component ${field}`);
4320
+ value = entry.fallback;
4321
+ }
4322
+ result[variable] = value;
4131
4323
  }
4132
4324
  return result;
4133
4325
  }
@@ -4151,7 +4343,7 @@ async function doctorFleet(config, options, probes = []) {
4151
4343
  const plan = [];
4152
4344
  try {
4153
4345
  const targets = await preflightFleet(config, options);
4154
- const require2 = createRequire2(resolve2(process.cwd(), "package.json"));
4346
+ const require2 = createRequire3(resolve3(process.cwd(), "package.json"));
4155
4347
  for (const [service, target] of targets) {
4156
4348
  let version = "development";
4157
4349
  try {
@@ -4206,7 +4398,7 @@ async function doctorFleet(config, options, probes = []) {
4206
4398
  };
4207
4399
  }
4208
4400
  async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e4) {
4209
- const archive = validateArchive(input);
4401
+ const archive = assertArchiveCompatibility(input, options.archiveCompatibility);
4210
4402
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4211
4403
  const isolated = {
4212
4404
  ...config,
@@ -4218,7 +4410,7 @@ async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e
4218
4410
  };
4219
4411
  let fleet;
4220
4412
  try {
4221
- fleet = await startFleet(isolated, { load: options.load });
4413
+ fleet = await startFleet(isolated, options);
4222
4414
  const client = new FleetClient(fleet.manifest.adminBase, {
4223
4415
  ...config.adminKey ? { adminKey: config.adminKey } : {}
4224
4416
  });
@@ -4593,26 +4785,7 @@ var resolveServeControls = async (values, env = process.env, base) => {
4593
4785
  normalizeBootControls(document, "adminControls");
4594
4786
  return document;
4595
4787
  };
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
- };
4788
+ var loadTarget = (name, own) => createInstalledServiceLoader({ projectRoot: process.cwd(), services: [name], targets: [own] })(name);
4616
4789
  var start = async (target, values, config) => {
4617
4790
  const adminPrefix = resolveAdminPrefix(config.adminPrefix);
4618
4791
  const runtime = await target.create(values, {
@@ -4827,4 +5000,4 @@ export {
4827
5000
  loadCatalogFile,
4828
5001
  serveTarget
4829
5002
  };
4830
- //# sourceMappingURL=chunk-5NISHHYG.js.map
5003
+ //# sourceMappingURL=chunk-ZYWNK53Z.js.map