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