@crvouga/mockingbird-service-resend 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-resend
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
 
@@ -1438,7 +1438,7 @@ var assertSnapshotDocument = (value, label = "snapshot") => {
1438
1438
  });
1439
1439
  }
1440
1440
  if (value.clock !== void 0) {
1441
- if (!isRecord2(value.clock) || typeof value.clock.now !== "number" || typeof value.clock.frozen !== "boolean")
1441
+ if (!isRecord2(value.clock) || typeof value.clock.now !== "number" || !Number.isFinite(value.clock.now) || typeof value.clock.frozen !== "boolean")
1442
1442
  throw new ControlError(`${label}.clock: expected {now, frozen}`);
1443
1443
  }
1444
1444
  return value;
@@ -3378,6 +3378,91 @@ var createMetrics = (partition = true) => {
3378
3378
  };
3379
3379
  };
3380
3380
 
3381
+ // ../core/dist/outbound.js
3382
+ function ownedTransport(policy) {
3383
+ if (!["offline", "allowlist"].includes(policy.mode) || policy.origins?.some((origin) => {
3384
+ try {
3385
+ return new URL(origin).origin !== origin;
3386
+ } catch {
3387
+ return true;
3388
+ }
3389
+ }))
3390
+ throw new Error("invalid outbound transport policy");
3391
+ const reject = (namespace, reason) => {
3392
+ try {
3393
+ policy.onReject?.({ namespace, policy: policy.mode, reason });
3394
+ } catch {
3395
+ }
3396
+ throw new Error(`outbound policy ${policy.mode}: ${reason}`);
3397
+ };
3398
+ return async (request, namespace, send) => {
3399
+ if (policy.mode === "offline")
3400
+ return reject(namespace, "delivery disabled");
3401
+ const allowed = (url2) => ["http:", "https:"].includes(url2.protocol) && !url2.username && !url2.password && (policy.origins ?? []).includes(url2.origin);
3402
+ let url = new URL(request.url);
3403
+ if (!allowed(url))
3404
+ return reject(namespace, "destination not allowed");
3405
+ const max = policy.maxBodyBytes ?? 1024 * 1024;
3406
+ const chunks = [];
3407
+ let size = 0;
3408
+ const reader = request.body?.getReader();
3409
+ if (reader)
3410
+ try {
3411
+ for (; ; ) {
3412
+ const item = await reader.read();
3413
+ if (item.done)
3414
+ break;
3415
+ size += item.value.length;
3416
+ if (size > max)
3417
+ return reject(namespace, "body limit exceeded");
3418
+ chunks.push(item.value);
3419
+ }
3420
+ } finally {
3421
+ void reader.cancel().catch(() => {
3422
+ });
3423
+ reader.releaseLock();
3424
+ }
3425
+ const body = new Uint8Array(size);
3426
+ let offset = 0;
3427
+ for (const chunk of chunks) {
3428
+ body.set(chunk, offset);
3429
+ offset += chunk.length;
3430
+ }
3431
+ let method = request.method, headers = new Headers(request.headers);
3432
+ for (let redirects = 0; ; redirects++) {
3433
+ if (!allowed(url))
3434
+ return reject(namespace, "redirect destination not allowed");
3435
+ const response = await send(new Request(url, {
3436
+ method,
3437
+ headers,
3438
+ ...method !== "GET" && method !== "HEAD" ? { body } : {},
3439
+ redirect: "manual",
3440
+ signal: request.signal
3441
+ }));
3442
+ if (![301, 302, 303, 307, 308].includes(response.status))
3443
+ return response;
3444
+ const location = response.headers.get("location");
3445
+ if (!location)
3446
+ return response;
3447
+ await response.body?.cancel();
3448
+ if (redirects >= (policy.maxRedirects ?? 5))
3449
+ return reject(namespace, "redirect limit exceeded");
3450
+ const next = new URL(location, url);
3451
+ if (next.origin !== url.origin) {
3452
+ headers = new Headers(headers);
3453
+ for (const key of ["authorization", "cookie", "proxy-authorization"])
3454
+ headers.delete(key);
3455
+ }
3456
+ if (response.status === 303 || (response.status === 301 || response.status === 302) && method === "POST") {
3457
+ method = "GET";
3458
+ headers.delete("content-length");
3459
+ headers.delete("content-type");
3460
+ }
3461
+ url = next;
3462
+ }
3463
+ };
3464
+ }
3465
+
3381
3466
  // ../core/dist/outbox.js
3382
3467
  var OutboxStore = class {
3383
3468
  items;
@@ -4023,7 +4108,7 @@ var stateAdminRoutes = (routes) => {
4023
4108
  };
4024
4109
 
4025
4110
  // ../core/dist/version.js
4026
- var PACKAGE_VERSION = true ? "3.3.0" : UNRELEASED_VERSION;
4111
+ var PACKAGE_VERSION = true ? "3.5.0" : UNRELEASED_VERSION;
4027
4112
 
4028
4113
  // ../core/dist/signing.js
4029
4114
  var encoder = new TextEncoder();
@@ -4115,13 +4200,35 @@ var createWebhookHub = (options) => {
4115
4200
  const payloads = /* @__PURE__ */ new Map();
4116
4201
  const faults = /* @__PURE__ */ new Map();
4117
4202
  const held = /* @__PURE__ */ new Map();
4203
+ const deliveryObservers = /* @__PURE__ */ new Set();
4204
+ const notify = (delivery) => {
4205
+ for (const listener of deliveryObservers)
4206
+ try {
4207
+ listener({ namespace: delivery.namespace, state: delivery.state, deliveryId: delivery.id });
4208
+ } catch {
4209
+ }
4210
+ };
4211
+ let transport;
4118
4212
  const inFlight2 = /* @__PURE__ */ new Set();
4213
+ const workNamespace = /* @__PURE__ */ new Map();
4214
+ const activeDeliveries = /* @__PURE__ */ new Map();
4119
4215
  const controllers = /* @__PURE__ */ new Map();
4120
4216
  let immediate = Promise.resolve();
4121
4217
  const snapshots = /* @__PURE__ */ new WeakMap();
4122
- const track = (work) => {
4218
+ const track = (work, delivery) => {
4123
4219
  inFlight2.add(work);
4124
- void work.finally(() => inFlight2.delete(work));
4220
+ workNamespace.set(work, delivery.namespace);
4221
+ const active = activeDeliveries.get(delivery.id) ?? { namespace: delivery.namespace, count: 0 };
4222
+ active.count++;
4223
+ activeDeliveries.set(delivery.id, active);
4224
+ const finish = () => {
4225
+ inFlight2.delete(work);
4226
+ workNamespace.delete(work);
4227
+ active.count--;
4228
+ if (active.count === 0)
4229
+ activeDeliveries.delete(delivery.id);
4230
+ };
4231
+ void work.then(finish, finish);
4125
4232
  };
4126
4233
  const attempt = async (delivery) => {
4127
4234
  const entry = payloads.get(delivery.id);
@@ -4153,7 +4260,8 @@ var createWebhookHub = (options) => {
4153
4260
  type: message.type,
4154
4261
  tags: message.tags
4155
4262
  });
4156
- const response = await send(new Request(endpoint.url, {
4263
+ const ownedSend = (request) => transport ? transport(request, message.namespace, send) : send(request);
4264
+ const response = await ownedSend(new Request(endpoint.url, {
4157
4265
  method: "POST",
4158
4266
  headers: {
4159
4267
  "content-type": message.contentType,
@@ -4180,6 +4288,7 @@ var createWebhookHub = (options) => {
4180
4288
  const index = delivery.attempts.length;
4181
4289
  if (index >= delays.length) {
4182
4290
  delivery.state = "failed";
4291
+ notify(delivery);
4183
4292
  pending.delete(delivery.id);
4184
4293
  return;
4185
4294
  }
@@ -4189,9 +4298,10 @@ var createWebhookHub = (options) => {
4189
4298
  await attempt(delivery).then((ok) => {
4190
4299
  if (!payloads.has(delivery.id))
4191
4300
  return;
4192
- if (ok)
4301
+ if (ok) {
4193
4302
  delivery.state = "delivered";
4194
- else
4303
+ notify(delivery);
4304
+ } else
4195
4305
  schedule(delivery);
4196
4306
  });
4197
4307
  };
@@ -4200,12 +4310,12 @@ var createWebhookHub = (options) => {
4200
4310
  const task = immediate.then(settle);
4201
4311
  immediate = task.catch(() => {
4202
4312
  });
4203
- track(task);
4313
+ track(task, delivery);
4204
4314
  return;
4205
4315
  }
4206
4316
  const timer = scheduleTimer(() => {
4207
4317
  pending.delete(delivery.id);
4208
- track(settle());
4318
+ track(settle(), delivery);
4209
4319
  }, delay);
4210
4320
  unref(timer);
4211
4321
  pending.set(delivery.id, timer);
@@ -4229,6 +4339,8 @@ var createWebhookHub = (options) => {
4229
4339
  payloads.set(delivery.id, { message, endpoint });
4230
4340
  if (state === "pending")
4231
4341
  schedule(delivery);
4342
+ else
4343
+ notify(delivery);
4232
4344
  }
4233
4345
  };
4234
4346
  const takeFault = (namespace) => {
@@ -4250,8 +4362,19 @@ var createWebhookHub = (options) => {
4250
4362
  fanOut(message);
4251
4363
  };
4252
4364
  const hub = {
4365
+ observeDeliveries(listener) {
4366
+ deliveryObservers.add(listener);
4367
+ return () => deliveryObservers.delete(listener);
4368
+ },
4369
+ setTransportPolicy(policy) {
4370
+ transport = policy;
4371
+ },
4253
4372
  pending(namespace) {
4254
- return inFlight2.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
4373
+ const ids = new Set([...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").map((d) => d.id));
4374
+ for (const [id2, active] of activeDeliveries)
4375
+ if (active.namespace === namespace)
4376
+ ids.add(id2);
4377
+ return ids.size + (held.get(namespace)?.length ?? 0);
4255
4378
  },
4256
4379
  snapshot(namespace) {
4257
4380
  if (hub.pending(namespace) > 0)
@@ -4267,6 +4390,36 @@ var createWebhookHub = (options) => {
4267
4390
  }));
4268
4391
  return handle;
4269
4392
  },
4393
+ exportSnapshot(handle) {
4394
+ const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
4395
+ if (!point)
4396
+ throw new Error("invalid webhook snapshot");
4397
+ const { namespace: _namespace, ...document2 } = point;
4398
+ return structuredClone(document2);
4399
+ },
4400
+ importSnapshot(namespace, document2) {
4401
+ if (!isRecord4(document2) || !Array.isArray(document2.endpoints) || !Array.isArray(document2.messages) || !Array.isArray(document2.deliveries) || !Array.isArray(document2.payloads) || !Array.isArray(document2.faults))
4402
+ throw new Error("invalid portable webhook snapshot");
4403
+ for (const endpoint of document2.endpoints)
4404
+ if (typeof parseEndpoint(endpoint) === "string")
4405
+ throw new Error("invalid portable webhook endpoint");
4406
+ for (const message of document2.messages)
4407
+ if (!isRecord4(message) || typeof message.id !== "string" || typeof message.body !== "string")
4408
+ throw new Error("invalid portable webhook message");
4409
+ for (const pair of [...document2.deliveries, ...document2.payloads])
4410
+ if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string" || !isRecord4(pair[1]))
4411
+ throw new Error("invalid portable webhook entry");
4412
+ const handle = {};
4413
+ const point = structuredClone(document2);
4414
+ for (const message of point.messages)
4415
+ message.namespace = namespace;
4416
+ for (const [, delivery] of point.deliveries)
4417
+ delivery.namespace = namespace;
4418
+ for (const [, payload] of point.payloads)
4419
+ payload.message.namespace = namespace;
4420
+ snapshots.set(handle, { ...point, namespace });
4421
+ return handle;
4422
+ },
4270
4423
  restore(namespace, handle) {
4271
4424
  const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
4272
4425
  if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
@@ -4327,9 +4480,14 @@ var createWebhookHub = (options) => {
4327
4480
  const delivery = deliveries.get(id2);
4328
4481
  if (!delivery)
4329
4482
  return void 0;
4330
- const ok = await attempt(delivery);
4331
- if (ok)
4332
- delivery.state = "delivered";
4483
+ const work = (async () => {
4484
+ const ok = await attempt(delivery);
4485
+ if (ok)
4486
+ delivery.state = "delivered";
4487
+ notify({ ...delivery, state: ok ? "delivered" : "failed" });
4488
+ })();
4489
+ track(work, delivery);
4490
+ await work;
4333
4491
  return delivery;
4334
4492
  },
4335
4493
  async flush() {
@@ -4346,11 +4504,12 @@ var createWebhookHub = (options) => {
4346
4504
  if (!delivery)
4347
4505
  continue;
4348
4506
  track(attempt(delivery).then((ok) => {
4349
- if (ok)
4507
+ if (ok) {
4350
4508
  delivery.state = "delivered";
4351
- else
4509
+ notify(delivery);
4510
+ } else
4352
4511
  schedule(delivery);
4353
- }));
4512
+ }), delivery);
4354
4513
  }
4355
4514
  await hub.idle();
4356
4515
  const scheduled = [...pending.values()].some((timer) => timer !== void 0);
@@ -4358,9 +4517,13 @@ var createWebhookHub = (options) => {
4358
4517
  return;
4359
4518
  }
4360
4519
  },
4361
- async idle() {
4362
- while (inFlight2.size > 0)
4363
- await Promise.allSettled([...inFlight2]);
4520
+ async idle(namespace) {
4521
+ for (; ; ) {
4522
+ const selected = [...inFlight2].filter((work) => namespace === void 0 || workNamespace.get(work) === namespace);
4523
+ if (!selected.length)
4524
+ return;
4525
+ await Promise.allSettled(selected);
4526
+ }
4364
4527
  },
4365
4528
  fault(namespace, fault) {
4366
4529
  const queue = faults.get(namespace) ?? [];
@@ -4973,7 +5136,8 @@ var createRuntime = (options) => {
4973
5136
  ...point.rngState !== void 0 ? { rng: { state: point.rngState, ...streams } } : {},
4974
5137
  ...rules ? { faults: rules } : {},
4975
5138
  ...waits ? { latency: waits } : {},
4976
- ...settings?.virtualTime !== void 0 ? { virtualTime: settings.virtualTime } : {}
5139
+ ...settings?.virtualTime !== void 0 ? { virtualTime: settings.virtualTime } : {},
5140
+ ...point.webhooks !== void 0 ? { webhooks: options.webhooks?.exportSnapshot(point.webhooks) } : {}
4977
5141
  };
4978
5142
  };
4979
5143
  const position = (value, label) => {
@@ -5058,7 +5222,8 @@ var createRuntime = (options) => {
5058
5222
  records: stored,
5059
5223
  ...document2.clock ? { clock: { now: document2.clock.now, frozen: document2.clock.frozen } } : {},
5060
5224
  ...document2.rng?.state !== void 0 ? { rngState: position(document2.rng.state, "snapshot.rng.state") } : {},
5061
- controls: held
5225
+ controls: held,
5226
+ ...document2.webhooks !== void 0 ? { webhooks: options.webhooks?.importSnapshot(namespace, document2.webhooks) } : {}
5062
5227
  };
5063
5228
  };
5064
5229
  const kept = /* @__PURE__ */ new Map();
@@ -5192,6 +5357,20 @@ var createRuntime = (options) => {
5192
5357
  restoreAll(namespace, point);
5193
5358
  timelines.delete(namespace);
5194
5359
  },
5360
+ fleetExport(handle) {
5361
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
5362
+ if (!point)
5363
+ throw new Error("invalid fleet checkpoint");
5364
+ return toDocument(point);
5365
+ },
5366
+ fleetImport(namespace, document2) {
5367
+ const handle = {};
5368
+ fleetPoints.set(handle, {
5369
+ namespace,
5370
+ ...fromDocument(namespace, assertSnapshotDocument(document2))
5371
+ });
5372
+ return handle;
5373
+ },
5195
5374
  applyPreset: (name2, namespace = DEFAULT_NAMESPACE, overrides = {}) => controls.addFault(namespace, { ...overrides, preset: name2 }),
5196
5375
  instance,
5197
5376
  namespaces: () => [...publicNamespaces].sort(),
@@ -8515,9 +8694,11 @@ export {
8515
8694
  createClock,
8516
8695
  restoreClock,
8517
8696
  parseDuration,
8697
+ normalizeControls,
8518
8698
  normalizeBootControls,
8519
8699
  parseFaultHeader,
8520
8700
  resolveAdminPrefix,
8701
+ ownedTransport,
8521
8702
  document,
8522
8703
  operationIds,
8523
8704
  supportedOperationIds,
@@ -8536,4 +8717,4 @@ export {
8536
8717
  * Copyright (c) 2021 - present, Yusuke Wada and Hono contributors
8537
8718
  * MIT license: see THIRD_PARTY_NOTICES.md in the repository and npm packages.
8538
8719
  */
8539
- //# sourceMappingURL=chunk-YJDLINUI.js.map
8720
+ //# sourceMappingURL=chunk-KPX5JPKL.js.map