@crvouga/mockingbird-service-twilio 3.3.0 → 3.5.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,14 +1,28 @@
1
1
  # Changelog — @crvouga/mockingbird-service-twilio
2
2
 
3
- ## 3.3.0 (2026-10-12)
3
+ ## 3.5.0 (2026-10-16)
4
4
 
5
5
  ### Features
6
6
 
7
- - unify the emulator control plane ([2068547](https://github.com/crvouga/mockingbird/commit/2068547bedde4e2e2331c1657c1a78120e84428b))
7
+ - resolve SQL, routing and writer gaps ([73f97d0](https://github.com/crvouga/mockingbird/commit/73f97d0d2379f44b9131180498f4fab76648fed9))
8
8
 
9
- ### Dependencies
9
+ ## 3.4.0 (2026-10-15)
10
+
11
+ ### Features
12
+
13
+ - finish durable run lifecycle ([725ea1a](https://github.com/crvouga/mockingbird/commit/725ea1a966dac65434e98f06a9c33aca0d6f32d7))
14
+ - add management and portable archives ([0d2a6dd](https://github.com/crvouga/mockingbird/commit/0d2a6dd0f9fecb3a1ea7c1638f6639787395c2f4))
15
+
16
+ ### Fixes and improvements
10
17
 
11
- - `@crvouga/mockingbird-service-sqlite`
18
+ - correlate cross-namespace operations ([3457a4b](https://github.com/crvouga/mockingbird/commit/3457a4b7dbfd9ad9c8a10671a5744b7c8425d000))
19
+ - harden HTTP stream handling ([674307a](https://github.com/crvouga/mockingbird/commit/674307a869f57907567d462c4f506c3b0a237e13))
20
+
21
+ ## 3.3.0 (2026-10-12)
22
+
23
+ ### Features
24
+
25
+ - unify the emulator control plane ([2068547](https://github.com/crvouga/mockingbird/commit/2068547bedde4e2e2331c1657c1a78120e84428b))
12
26
 
13
27
  ## 3.2.0 (2026-10-08)
14
28
 
@@ -1173,7 +1173,7 @@ var assertSnapshotDocument = (value, label = "snapshot") => {
1173
1173
  });
1174
1174
  }
1175
1175
  if (value.clock !== void 0) {
1176
- if (!isRecord2(value.clock) || typeof value.clock.now !== "number" || typeof value.clock.frozen !== "boolean")
1176
+ if (!isRecord2(value.clock) || typeof value.clock.now !== "number" || !Number.isFinite(value.clock.now) || typeof value.clock.frozen !== "boolean")
1177
1177
  throw new ControlError(`${label}.clock: expected {now, frozen}`);
1178
1178
  }
1179
1179
  return value;
@@ -3064,6 +3064,91 @@ var createMetrics = (partition = true) => {
3064
3064
  };
3065
3065
  };
3066
3066
 
3067
+ // ../core/dist/outbound.js
3068
+ function ownedTransport(policy) {
3069
+ if (!["offline", "allowlist"].includes(policy.mode) || policy.origins?.some((origin) => {
3070
+ try {
3071
+ return new URL(origin).origin !== origin;
3072
+ } catch {
3073
+ return true;
3074
+ }
3075
+ }))
3076
+ throw new Error("invalid outbound transport policy");
3077
+ const reject = (namespace, reason) => {
3078
+ try {
3079
+ policy.onReject?.({ namespace, policy: policy.mode, reason });
3080
+ } catch {
3081
+ }
3082
+ throw new Error(`outbound policy ${policy.mode}: ${reason}`);
3083
+ };
3084
+ return async (request, namespace, send) => {
3085
+ if (policy.mode === "offline")
3086
+ return reject(namespace, "delivery disabled");
3087
+ const allowed = (url2) => ["http:", "https:"].includes(url2.protocol) && !url2.username && !url2.password && (policy.origins ?? []).includes(url2.origin);
3088
+ let url = new URL(request.url);
3089
+ if (!allowed(url))
3090
+ return reject(namespace, "destination not allowed");
3091
+ const max = policy.maxBodyBytes ?? 1024 * 1024;
3092
+ const chunks = [];
3093
+ let size = 0;
3094
+ const reader = request.body?.getReader();
3095
+ if (reader)
3096
+ try {
3097
+ for (; ; ) {
3098
+ const item = await reader.read();
3099
+ if (item.done)
3100
+ break;
3101
+ size += item.value.length;
3102
+ if (size > max)
3103
+ return reject(namespace, "body limit exceeded");
3104
+ chunks.push(item.value);
3105
+ }
3106
+ } finally {
3107
+ void reader.cancel().catch(() => {
3108
+ });
3109
+ reader.releaseLock();
3110
+ }
3111
+ const body = new Uint8Array(size);
3112
+ let offset = 0;
3113
+ for (const chunk of chunks) {
3114
+ body.set(chunk, offset);
3115
+ offset += chunk.length;
3116
+ }
3117
+ let method = request.method, headers = new Headers(request.headers);
3118
+ for (let redirects = 0; ; redirects++) {
3119
+ if (!allowed(url))
3120
+ return reject(namespace, "redirect destination not allowed");
3121
+ const response = await send(new Request(url, {
3122
+ method,
3123
+ headers,
3124
+ ...method !== "GET" && method !== "HEAD" ? { body } : {},
3125
+ redirect: "manual",
3126
+ signal: request.signal
3127
+ }));
3128
+ if (![301, 302, 303, 307, 308].includes(response.status))
3129
+ return response;
3130
+ const location = response.headers.get("location");
3131
+ if (!location)
3132
+ return response;
3133
+ await response.body?.cancel();
3134
+ if (redirects >= (policy.maxRedirects ?? 5))
3135
+ return reject(namespace, "redirect limit exceeded");
3136
+ const next = new URL(location, url);
3137
+ if (next.origin !== url.origin) {
3138
+ headers = new Headers(headers);
3139
+ for (const key of ["authorization", "cookie", "proxy-authorization"])
3140
+ headers.delete(key);
3141
+ }
3142
+ if (response.status === 303 || (response.status === 301 || response.status === 302) && method === "POST") {
3143
+ method = "GET";
3144
+ headers.delete("content-length");
3145
+ headers.delete("content-type");
3146
+ }
3147
+ url = next;
3148
+ }
3149
+ };
3150
+ }
3151
+
3067
3152
  // ../core/dist/outbox.js
3068
3153
  var OutboxStore = class {
3069
3154
  items;
@@ -3699,7 +3784,7 @@ var stateAdminRoutes = (routes) => {
3699
3784
  };
3700
3785
 
3701
3786
  // ../core/dist/version.js
3702
- var PACKAGE_VERSION = true ? "3.3.0" : UNRELEASED_VERSION;
3787
+ var PACKAGE_VERSION = true ? "3.5.0" : UNRELEASED_VERSION;
3703
3788
 
3704
3789
  // ../core/dist/signing.js
3705
3790
  var encoder = new TextEncoder();
@@ -3791,13 +3876,35 @@ var createWebhookHub = (options) => {
3791
3876
  const payloads = /* @__PURE__ */ new Map();
3792
3877
  const faults = /* @__PURE__ */ new Map();
3793
3878
  const held = /* @__PURE__ */ new Map();
3879
+ const deliveryObservers = /* @__PURE__ */ new Set();
3880
+ const notify = (delivery) => {
3881
+ for (const listener of deliveryObservers)
3882
+ try {
3883
+ listener({ namespace: delivery.namespace, state: delivery.state, deliveryId: delivery.id });
3884
+ } catch {
3885
+ }
3886
+ };
3887
+ let transport;
3794
3888
  const inFlight = /* @__PURE__ */ new Set();
3889
+ const workNamespace = /* @__PURE__ */ new Map();
3890
+ const activeDeliveries = /* @__PURE__ */ new Map();
3795
3891
  const controllers = /* @__PURE__ */ new Map();
3796
3892
  let immediate = Promise.resolve();
3797
3893
  const snapshots = /* @__PURE__ */ new WeakMap();
3798
- const track = (work) => {
3894
+ const track = (work, delivery) => {
3799
3895
  inFlight.add(work);
3800
- void work.finally(() => inFlight.delete(work));
3896
+ workNamespace.set(work, delivery.namespace);
3897
+ const active = activeDeliveries.get(delivery.id) ?? { namespace: delivery.namespace, count: 0 };
3898
+ active.count++;
3899
+ activeDeliveries.set(delivery.id, active);
3900
+ const finish = () => {
3901
+ inFlight.delete(work);
3902
+ workNamespace.delete(work);
3903
+ active.count--;
3904
+ if (active.count === 0)
3905
+ activeDeliveries.delete(delivery.id);
3906
+ };
3907
+ void work.then(finish, finish);
3801
3908
  };
3802
3909
  const attempt = async (delivery) => {
3803
3910
  const entry = payloads.get(delivery.id);
@@ -3829,7 +3936,8 @@ var createWebhookHub = (options) => {
3829
3936
  type: message.type,
3830
3937
  tags: message.tags
3831
3938
  });
3832
- const response = await send(new Request(endpoint.url, {
3939
+ const ownedSend = (request) => transport ? transport(request, message.namespace, send) : send(request);
3940
+ const response = await ownedSend(new Request(endpoint.url, {
3833
3941
  method: "POST",
3834
3942
  headers: {
3835
3943
  "content-type": message.contentType,
@@ -3856,6 +3964,7 @@ var createWebhookHub = (options) => {
3856
3964
  const index = delivery.attempts.length;
3857
3965
  if (index >= delays.length) {
3858
3966
  delivery.state = "failed";
3967
+ notify(delivery);
3859
3968
  pending.delete(delivery.id);
3860
3969
  return;
3861
3970
  }
@@ -3865,9 +3974,10 @@ var createWebhookHub = (options) => {
3865
3974
  await attempt(delivery).then((ok) => {
3866
3975
  if (!payloads.has(delivery.id))
3867
3976
  return;
3868
- if (ok)
3977
+ if (ok) {
3869
3978
  delivery.state = "delivered";
3870
- else
3979
+ notify(delivery);
3980
+ } else
3871
3981
  schedule(delivery);
3872
3982
  });
3873
3983
  };
@@ -3876,12 +3986,12 @@ var createWebhookHub = (options) => {
3876
3986
  const task = immediate.then(settle);
3877
3987
  immediate = task.catch(() => {
3878
3988
  });
3879
- track(task);
3989
+ track(task, delivery);
3880
3990
  return;
3881
3991
  }
3882
3992
  const timer = scheduleTimer(() => {
3883
3993
  pending.delete(delivery.id);
3884
- track(settle());
3994
+ track(settle(), delivery);
3885
3995
  }, delay);
3886
3996
  unref(timer);
3887
3997
  pending.set(delivery.id, timer);
@@ -3905,6 +4015,8 @@ var createWebhookHub = (options) => {
3905
4015
  payloads.set(delivery.id, { message, endpoint });
3906
4016
  if (state === "pending")
3907
4017
  schedule(delivery);
4018
+ else
4019
+ notify(delivery);
3908
4020
  }
3909
4021
  };
3910
4022
  const takeFault = (namespace) => {
@@ -3926,8 +4038,19 @@ var createWebhookHub = (options) => {
3926
4038
  fanOut(message);
3927
4039
  };
3928
4040
  const hub = {
4041
+ observeDeliveries(listener) {
4042
+ deliveryObservers.add(listener);
4043
+ return () => deliveryObservers.delete(listener);
4044
+ },
4045
+ setTransportPolicy(policy) {
4046
+ transport = policy;
4047
+ },
3929
4048
  pending(namespace) {
3930
- return inFlight.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
4049
+ const ids = new Set([...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").map((d) => d.id));
4050
+ for (const [id2, active] of activeDeliveries)
4051
+ if (active.namespace === namespace)
4052
+ ids.add(id2);
4053
+ return ids.size + (held.get(namespace)?.length ?? 0);
3931
4054
  },
3932
4055
  snapshot(namespace) {
3933
4056
  if (hub.pending(namespace) > 0)
@@ -3943,6 +4066,36 @@ var createWebhookHub = (options) => {
3943
4066
  }));
3944
4067
  return handle;
3945
4068
  },
4069
+ exportSnapshot(handle) {
4070
+ const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
4071
+ if (!point)
4072
+ throw new Error("invalid webhook snapshot");
4073
+ const { namespace: _namespace, ...document2 } = point;
4074
+ return structuredClone(document2);
4075
+ },
4076
+ importSnapshot(namespace, document2) {
4077
+ if (!isRecord4(document2) || !Array.isArray(document2.endpoints) || !Array.isArray(document2.messages) || !Array.isArray(document2.deliveries) || !Array.isArray(document2.payloads) || !Array.isArray(document2.faults))
4078
+ throw new Error("invalid portable webhook snapshot");
4079
+ for (const endpoint of document2.endpoints)
4080
+ if (typeof parseEndpoint(endpoint) === "string")
4081
+ throw new Error("invalid portable webhook endpoint");
4082
+ for (const message of document2.messages)
4083
+ if (!isRecord4(message) || typeof message.id !== "string" || typeof message.body !== "string")
4084
+ throw new Error("invalid portable webhook message");
4085
+ for (const pair of [...document2.deliveries, ...document2.payloads])
4086
+ if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string" || !isRecord4(pair[1]))
4087
+ throw new Error("invalid portable webhook entry");
4088
+ const handle = {};
4089
+ const point = structuredClone(document2);
4090
+ for (const message of point.messages)
4091
+ message.namespace = namespace;
4092
+ for (const [, delivery] of point.deliveries)
4093
+ delivery.namespace = namespace;
4094
+ for (const [, payload] of point.payloads)
4095
+ payload.message.namespace = namespace;
4096
+ snapshots.set(handle, { ...point, namespace });
4097
+ return handle;
4098
+ },
3946
4099
  restore(namespace, handle) {
3947
4100
  const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
3948
4101
  if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
@@ -4003,9 +4156,14 @@ var createWebhookHub = (options) => {
4003
4156
  const delivery = deliveries.get(id2);
4004
4157
  if (!delivery)
4005
4158
  return void 0;
4006
- const ok = await attempt(delivery);
4007
- if (ok)
4008
- delivery.state = "delivered";
4159
+ const work = (async () => {
4160
+ const ok = await attempt(delivery);
4161
+ if (ok)
4162
+ delivery.state = "delivered";
4163
+ notify({ ...delivery, state: ok ? "delivered" : "failed" });
4164
+ })();
4165
+ track(work, delivery);
4166
+ await work;
4009
4167
  return delivery;
4010
4168
  },
4011
4169
  async flush() {
@@ -4022,11 +4180,12 @@ var createWebhookHub = (options) => {
4022
4180
  if (!delivery)
4023
4181
  continue;
4024
4182
  track(attempt(delivery).then((ok) => {
4025
- if (ok)
4183
+ if (ok) {
4026
4184
  delivery.state = "delivered";
4027
- else
4185
+ notify(delivery);
4186
+ } else
4028
4187
  schedule(delivery);
4029
- }));
4188
+ }), delivery);
4030
4189
  }
4031
4190
  await hub.idle();
4032
4191
  const scheduled = [...pending.values()].some((timer) => timer !== void 0);
@@ -4034,9 +4193,13 @@ var createWebhookHub = (options) => {
4034
4193
  return;
4035
4194
  }
4036
4195
  },
4037
- async idle() {
4038
- while (inFlight.size > 0)
4039
- await Promise.allSettled([...inFlight]);
4196
+ async idle(namespace) {
4197
+ for (; ; ) {
4198
+ const selected = [...inFlight].filter((work) => namespace === void 0 || workNamespace.get(work) === namespace);
4199
+ if (!selected.length)
4200
+ return;
4201
+ await Promise.allSettled(selected);
4202
+ }
4040
4203
  },
4041
4204
  fault(namespace, fault) {
4042
4205
  const queue = faults.get(namespace) ?? [];
@@ -4649,7 +4812,8 @@ var createRuntime = (options) => {
4649
4812
  ...point.rngState !== void 0 ? { rng: { state: point.rngState, ...streams } } : {},
4650
4813
  ...rules ? { faults: rules } : {},
4651
4814
  ...waits ? { latency: waits } : {},
4652
- ...settings?.virtualTime !== void 0 ? { virtualTime: settings.virtualTime } : {}
4815
+ ...settings?.virtualTime !== void 0 ? { virtualTime: settings.virtualTime } : {},
4816
+ ...point.webhooks !== void 0 ? { webhooks: options.webhooks?.exportSnapshot(point.webhooks) } : {}
4653
4817
  };
4654
4818
  };
4655
4819
  const position = (value, label) => {
@@ -4734,7 +4898,8 @@ var createRuntime = (options) => {
4734
4898
  records: stored,
4735
4899
  ...document2.clock ? { clock: { now: document2.clock.now, frozen: document2.clock.frozen } } : {},
4736
4900
  ...document2.rng?.state !== void 0 ? { rngState: position(document2.rng.state, "snapshot.rng.state") } : {},
4737
- controls: held
4901
+ controls: held,
4902
+ ...document2.webhooks !== void 0 ? { webhooks: options.webhooks?.importSnapshot(namespace, document2.webhooks) } : {}
4738
4903
  };
4739
4904
  };
4740
4905
  const kept = /* @__PURE__ */ new Map();
@@ -4868,6 +5033,20 @@ var createRuntime = (options) => {
4868
5033
  restoreAll(namespace, point);
4869
5034
  timelines.delete(namespace);
4870
5035
  },
5036
+ fleetExport(handle) {
5037
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
5038
+ if (!point)
5039
+ throw new Error("invalid fleet checkpoint");
5040
+ return toDocument(point);
5041
+ },
5042
+ fleetImport(namespace, document2) {
5043
+ const handle = {};
5044
+ fleetPoints.set(handle, {
5045
+ namespace,
5046
+ ...fromDocument(namespace, assertSnapshotDocument(document2))
5047
+ });
5048
+ return handle;
5049
+ },
4871
5050
  applyPreset: (name2, namespace = DEFAULT_NAMESPACE, overrides = {}) => controls.addFault(namespace, { ...overrides, preset: name2 }),
4872
5051
  instance,
4873
5052
  namespaces: () => [...publicNamespaces].sort(),
@@ -10200,9 +10379,11 @@ export {
10200
10379
  createClock,
10201
10380
  restoreClock,
10202
10381
  parseDuration,
10382
+ normalizeControls,
10203
10383
  normalizeBootControls,
10204
10384
  parseFaultHeader,
10205
10385
  resolveAdminPrefix,
10386
+ ownedTransport,
10206
10387
  document,
10207
10388
  operationIds,
10208
10389
  supportedOperationIds,
@@ -10229,4 +10410,4 @@ export {
10229
10410
  * Copyright (c) 2021 - present, Yusuke Wada and Hono contributors
10230
10411
  * MIT license: see THIRD_PARTY_NOTICES.md in the repository and npm packages.
10231
10412
  */
10232
- //# sourceMappingURL=chunk-55BRX56F.js.map
10413
+ //# sourceMappingURL=chunk-D73EZLNL.js.map