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