@crvouga/mockingbird-service-payload-cms 1.7.1 → 1.8.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,20 @@
1
1
  # Changelog — @crvouga/mockingbird-service-payload-cms
2
2
 
3
+ ## 1.8.0 (2026-10-02)
4
+
5
+ ### Features
6
+
7
+ - coordinate HTTP and protocol mocks ([3168653](https://github.com/crvouga/mockingbird/commit/31686534c55a9823fdaa92afc9cc456cf741b809))
8
+
9
+ ### Fixes and improvements
10
+
11
+ - decouple smoke types from built packages ([a4b31c6](https://github.com/crvouga/mockingbird/commit/a4b31c6205d92aa0c6f7c9de3518d5ae44d38829))
12
+ - protect child admin endpoints ([5410b94](https://github.com/crvouga/mockingbird/commit/5410b948a30dcedc21e9a70e44fd415c1ea25394))
13
+ - harden protocol startup and state ([6f5b557](https://github.com/crvouga/mockingbird/commit/6f5b557e87b48b31985949e04f3b5ef58f51d295))
14
+ - preserve checkpoints and lifecycle ([50e8c32](https://github.com/crvouga/mockingbird/commit/50e8c322feded2f7f45afce9d8cdef43c37ab9ab))
15
+ - clean branch state on reset ([3b714d4](https://github.com/crvouga/mockingbird/commit/3b714d4a5df8e75fee0bb31e98d7c7566e164381))
16
+ - type signal listener cleanup ([5279852](https://github.com/crvouga/mockingbird/commit/5279852e56ea60262ccf17aa6e46b3c31091a34a))
17
+
3
18
  ## 1.7.1 (2026-10-01)
4
19
 
5
20
  ### Fixes and improvements
@@ -321,29 +321,30 @@ var createControlPlane = (context) => {
321
321
  return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
322
322
  },
323
323
  "GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
324
- "GET /clock": () => json(200, context.clock.state()),
325
- "POST /clock": ({ body }) => {
324
+ "GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
325
+ "POST /clock": ({ body, namespace }) => {
326
+ const clock = context.namespaceClock?.(namespace) ?? context.clock;
326
327
  if (!isRecord(body))
327
328
  return adminError(400, "expected a JSON object");
328
329
  if (body.reset === true)
329
- context.clock.reset();
330
+ clock.reset();
330
331
  if (body.freeze === true)
331
- context.clock.freeze();
332
+ clock.freeze();
332
333
  if (body.set !== void 0) {
333
334
  const instant = parseInstant(body.set);
334
335
  if (instant === void 0)
335
336
  return adminError(400, "set: expected epoch ms or ISO-8601");
336
- context.clock.set(instant);
337
+ clock.set(instant);
337
338
  }
338
339
  if (body.advance !== void 0) {
339
340
  const delta = parseDuration(body.advance);
340
341
  if (delta === void 0)
341
342
  return adminError(400, 'advance: expected ms or "15m"-style');
342
- context.clock.advance(delta);
343
+ clock.advance(delta);
343
344
  }
344
345
  if (body.freeze === false)
345
- context.clock.unfreeze();
346
- return json(200, context.clock.state());
346
+ clock.unfreeze();
347
+ return json(200, clock.state());
347
348
  },
348
349
  "GET /faults": () => json(200, { faults: context.faults.list() }),
349
350
  "POST /faults": ({ body, namespace }) => {
@@ -612,7 +613,33 @@ var faultResponse = (rule) => {
612
613
  };
613
614
  var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
614
615
  const entries = [];
616
+ let isolated = false;
617
+ const scopedRngs = /* @__PURE__ */ new Map();
618
+ const random = (namespace) => {
619
+ if (!isolated || namespace === "default")
620
+ return rng;
621
+ let scoped = scopedRngs.get(namespace);
622
+ if (!scoped) {
623
+ scoped = createRng(rng.seed);
624
+ scopedRngs.set(namespace, scoped);
625
+ }
626
+ return scoped;
627
+ };
615
628
  return {
629
+ isolateNamespaces() {
630
+ isolated = true;
631
+ },
632
+ snapshot(namespace) {
633
+ return {
634
+ rules: structuredClone(entries.map((entry, position) => ({
635
+ ...entry.rule,
636
+ remaining: entry.remaining,
637
+ hits: entry.hits,
638
+ position
639
+ })).filter((entry) => entry.namespace === namespace)),
640
+ rngState: random(namespace).state()
641
+ };
642
+ },
616
643
  add(rule) {
617
644
  const existing = entries.findIndex((e) => e.rule.id === rule.id);
618
645
  const entry = { rule, remaining: rule.count ?? null, hits: 0 };
@@ -632,6 +659,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
632
659
  },
633
660
  clear() {
634
661
  entries.length = 0;
662
+ rng.reset();
663
+ scopedRngs.clear();
664
+ },
665
+ restore(namespace, state) {
666
+ for (let i = entries.length - 1; i >= 0; i--)
667
+ if (entries[i]?.rule.namespace === namespace)
668
+ entries.splice(i, 1);
669
+ for (const { remaining, hits, position, ...rule } of state.rules)
670
+ entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
671
+ random(namespace).setState(state.rngState);
635
672
  },
636
673
  async take(candidate) {
637
674
  const hits = [];
@@ -641,7 +678,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
641
678
  if (!matches(entry.rule, candidate))
642
679
  continue;
643
680
  const rate = entry.rule.rate ?? 1;
644
- if (rng.next() >= rate)
681
+ if (random(candidate.namespace).next() >= rate)
645
682
  continue;
646
683
  entry.hits++;
647
684
  if (entry.remaining !== null)
@@ -876,7 +913,8 @@ var annotateResponse = (response, extra) => {
876
913
  var responseNotes = (response) => notes.get(response);
877
914
 
878
915
  // ../core/dist/metrics.js
879
- var createMetrics = () => {
916
+ var createMetrics = (partition = true) => {
917
+ const namespaces = /* @__PURE__ */ new Map();
880
918
  let requests = 0;
881
919
  let faults = 0;
882
920
  let totalDurationMs = 0;
@@ -884,6 +922,14 @@ var createMetrics = () => {
884
922
  const unmatched = /* @__PURE__ */ new Map();
885
923
  return {
886
924
  record(entry) {
925
+ if (partition) {
926
+ let scoped = namespaces.get(entry.namespace);
927
+ if (!scoped) {
928
+ scoped = createMetrics(false);
929
+ namespaces.set(entry.namespace, scoped);
930
+ }
931
+ scoped.record(entry);
932
+ }
887
933
  requests++;
888
934
  totalDurationMs += entry.durationMs;
889
935
  if (entry.faultId !== void 0)
@@ -895,7 +941,7 @@ var createMetrics = () => {
895
941
  unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
896
942
  }
897
943
  },
898
- report: () => ({
944
+ report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
899
945
  requests,
900
946
  byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
901
947
  unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
@@ -904,8 +950,13 @@ var createMetrics = () => {
904
950
  }),
905
951
  faults,
906
952
  totalDurationMs
907
- }),
908
- reset() {
953
+ },
954
+ reset(namespace) {
955
+ if (namespace !== void 0) {
956
+ namespaces.delete(namespace);
957
+ return;
958
+ }
959
+ namespaces.clear();
909
960
  requests = 0;
910
961
  faults = 0;
911
962
  totalDurationMs = 0;
@@ -3803,7 +3854,7 @@ var stateAdminRoutes = (routes) => {
3803
3854
  };
3804
3855
 
3805
3856
  // ../core/dist/version.js
3806
- var PACKAGE_VERSION = true ? "1.7.1" : UNRELEASED_VERSION;
3857
+ var PACKAGE_VERSION = true ? "1.8.0" : UNRELEASED_VERSION;
3807
3858
 
3808
3859
  // ../core/dist/webhooks.js
3809
3860
  var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -3969,6 +4020,20 @@ var createRuntime = (options) => {
3969
4020
  const instances = /* @__PURE__ */ new Map();
3970
4021
  const publicNamespaces = /* @__PURE__ */ new Set();
3971
4022
  const branchRngs = /* @__PURE__ */ new Map();
4023
+ const namespaceClocks = /* @__PURE__ */ new Map();
4024
+ const namespaceForStorage = /* @__PURE__ */ new Map();
4025
+ let isolatedNamespaces = false;
4026
+ const namespaceClock = (name = DEFAULT_NAMESPACE) => {
4027
+ if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
4028
+ return clock;
4029
+ let found = namespaceClocks.get(name);
4030
+ if (!found) {
4031
+ found = createClock();
4032
+ namespaceClocks.set(name, found);
4033
+ }
4034
+ return found;
4035
+ };
4036
+ const fleetPoints = /* @__PURE__ */ new WeakMap();
3972
4037
  const timelines = /* @__PURE__ */ new Map();
3973
4038
  const branchStorage = /* @__PURE__ */ new Map();
3974
4039
  const captured = /* @__PURE__ */ new Map();
@@ -3982,17 +4047,21 @@ var createRuntime = (options) => {
3982
4047
  if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
3983
4048
  throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
3984
4049
  }
4050
+ const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
3985
4051
  const created = options.create({
3986
4052
  namespace: storageNamespace(key),
3987
4053
  publicNamespace,
3988
4054
  sqlite,
3989
- clock,
3990
- rng: isolatedRng ?? rng
4055
+ clock: namespaceClock(publicNamespace),
4056
+ rng: createdContextRng
3991
4057
  });
3992
4058
  instances.set(key, created);
4059
+ namespaceForStorage.set(key, publicNamespace);
3993
4060
  publicNamespaces.add(publicNamespace);
3994
4061
  if (isolatedRng)
3995
4062
  branchRngs.set(key, isolatedRng);
4063
+ else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
4064
+ branchRngs.set(key, createdContextRng);
3996
4065
  return created;
3997
4066
  };
3998
4067
  const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
@@ -4010,7 +4079,7 @@ var createRuntime = (options) => {
4010
4079
  captured.set(storage, snapshot2);
4011
4080
  return Object.freeze({
4012
4081
  snapshot: snapshot2,
4013
- clock: Object.freeze(clock.state()),
4082
+ clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
4014
4083
  rngState: (branchRngs.get(storage) ?? rng).state()
4015
4084
  });
4016
4085
  };
@@ -4020,7 +4089,7 @@ var createRuntime = (options) => {
4020
4089
  return found;
4021
4090
  instance(name);
4022
4091
  found = new Timeline({
4023
- now: clock.now,
4092
+ now: namespaceClock(name).now,
4024
4093
  ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
4025
4094
  });
4026
4095
  found.commit(capture(name));
@@ -4047,12 +4116,13 @@ var createRuntime = (options) => {
4047
4116
  const point = history2.checkout("main", at);
4048
4117
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
4049
4118
  captured.set(namespace, point.value.snapshot);
4050
- rng.setState(point.value.rngState);
4051
- clock.set(point.value.clock.now);
4119
+ (branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
4120
+ const selectedClock = namespaceClock(namespace);
4121
+ selectedClock.set(point.value.clock.now);
4052
4122
  if (point.value.clock.frozen)
4053
- clock.freeze();
4123
+ selectedClock.freeze();
4054
4124
  else
4055
- clock.unfreeze();
4125
+ selectedClock.unfreeze();
4056
4126
  }
4057
4127
  return namespace;
4058
4128
  }
@@ -4110,11 +4180,12 @@ var createRuntime = (options) => {
4110
4180
  const storage = ensureBranch(namespace, branchName);
4111
4181
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
4112
4182
  captured.set(storage, point.value.snapshot);
4113
- clock.set(point.value.clock.now);
4183
+ const selectedClock = namespaceClock(namespace);
4184
+ selectedClock.set(point.value.clock.now);
4114
4185
  if (point.value.clock.frozen)
4115
- clock.freeze();
4186
+ selectedClock.freeze();
4116
4187
  else
4117
- clock.unfreeze();
4188
+ selectedClock.unfreeze();
4118
4189
  (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
4119
4190
  };
4120
4191
  const reset = async (name = DEFAULT_NAMESPACE) => {
@@ -4122,10 +4193,21 @@ var createRuntime = (options) => {
4122
4193
  options.webhooks?.clear();
4123
4194
  for (const each of instances.values())
4124
4195
  await each.reset();
4196
+ for (const each of branchRngs.values())
4197
+ each.reset();
4125
4198
  timelines.clear();
4199
+ for (const storage of branchStorage.values()) {
4200
+ instances.delete(storage);
4201
+ branchRngs.delete(storage);
4202
+ namespaceForStorage.delete(storage);
4203
+ }
4126
4204
  branchStorage.clear();
4127
- branchRngs.clear();
4128
4205
  captured.clear();
4206
+ faults.clear();
4207
+ clock.reset();
4208
+ rng.reset();
4209
+ for (const each of namespaceClocks.values())
4210
+ each.reset();
4129
4211
  return;
4130
4212
  }
4131
4213
  options.webhooks?.clear(name);
@@ -4143,11 +4225,18 @@ var createRuntime = (options) => {
4143
4225
  else
4144
4226
  clearNamespace(sqlite, storageNamespace(storage));
4145
4227
  branchStorage.delete(mapping);
4228
+ instances.delete(storage);
4229
+ namespaceForStorage.delete(storage);
4146
4230
  branchRngs.delete(storage);
4147
4231
  captured.delete(storage);
4148
4232
  }
4149
4233
  timelines.delete(name);
4150
4234
  captured.delete(name);
4235
+ faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
4236
+ namespaceClock(name).reset();
4237
+ (branchRngs.get(name) ?? rng).reset();
4238
+ journal.clear(name);
4239
+ metrics.reset(name);
4151
4240
  };
4152
4241
  const snapshot = (name = DEFAULT_NAMESPACE) => {
4153
4242
  return checkpoint(name, "main").value.snapshot;
@@ -4185,6 +4274,53 @@ var createRuntime = (options) => {
4185
4274
  rng,
4186
4275
  credentials,
4187
4276
  webhooks: options.webhooks,
4277
+ isolateNamespaces() {
4278
+ if (publicNamespaces.size > 1)
4279
+ throw new Error("enable namespace isolation before creating named namespaces");
4280
+ isolatedNamespaces = true;
4281
+ faults.isolateNamespaces();
4282
+ },
4283
+ namespaceClock,
4284
+ namespaceOf(request) {
4285
+ const url = new URL(request.url);
4286
+ const prefixed = PATH_PREFIX.exec(url.pathname);
4287
+ const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
4288
+ const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
4289
+ if (path === "/__admin" || path.startsWith("/__admin/"))
4290
+ return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
4291
+ if (selected !== void 0)
4292
+ return selected;
4293
+ const credential = options.credential?.(request);
4294
+ return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
4295
+ },
4296
+ fleetSnapshot(namespace) {
4297
+ instance(namespace);
4298
+ const handle = {};
4299
+ fleetPoints.set(handle, {
4300
+ namespace,
4301
+ state: capture(namespace),
4302
+ faults: faults.snapshot(namespace),
4303
+ webhooks: options.webhooks?.snapshot(namespace)
4304
+ });
4305
+ return handle;
4306
+ },
4307
+ fleetRestore(handle, namespace) {
4308
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
4309
+ if (!point || point.namespace !== namespace)
4310
+ throw new Error("invalid fleet checkpoint");
4311
+ restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
4312
+ captured.set(namespace, point.state.snapshot);
4313
+ const selectedClock = namespaceClock(namespace);
4314
+ selectedClock.freeze();
4315
+ selectedClock.set(point.state.clock.now);
4316
+ if (!point.state.clock.frozen)
4317
+ selectedClock.unfreeze();
4318
+ (branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
4319
+ faults.restore(namespace, point.faults);
4320
+ if (point.webhooks !== void 0)
4321
+ options.webhooks?.restore(namespace, point.webhooks);
4322
+ timelines.delete(namespace);
4323
+ },
4188
4324
  applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
4189
4325
  const preset = options.presets?.[name];
4190
4326
  if (!preset)
@@ -4360,6 +4496,7 @@ var createRuntime = (options) => {
4360
4496
  startedAt: wallNow(),
4361
4497
  wallNow,
4362
4498
  clock,
4499
+ namespaceClock,
4363
4500
  faults,
4364
4501
  metrics,
4365
4502
  journal,
@@ -4957,6 +5094,8 @@ var PayloadCmsAPI = class {
4957
5094
  };
4958
5095
 
4959
5096
  export {
5097
+ createClock,
5098
+ parseDuration,
4960
5099
  document2 as document,
4961
5100
  operationIds,
4962
5101
  supportedOperationIds,
@@ -4970,4 +5109,4 @@ export {
4970
5109
  compileWhere,
4971
5110
  PayloadCmsAPI
4972
5111
  };
4973
- //# sourceMappingURL=chunk-MW3LF4G2.js.map
5112
+ //# sourceMappingURL=chunk-COTVMNU6.js.map