@crvouga/mockingbird-service-resend 1.0.0 → 1.0.1

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,15 +1,18 @@
1
1
  # Changelog — @crvouga/mockingbird-service-resend
2
2
 
3
+ ## 1.0.1 (2026-09-26)
4
+
5
+ ### Fixes and improvements
6
+
7
+ - take the rejected-request byte count from content-length, never by reading the body ([a0e5fb2](https://github.com/crvouga/mockingbird/commit/a0e5fb2fc80ab725c3552b612f3f091fd6c857fb))
8
+ - reject a present body by media type, and log what a rejected request sent ([12e351a](https://github.com/crvouga/mockingbird/commit/12e351aba53cfb7b41e31a7809419455fc2902eb))
9
+
3
10
  ## 1.0.0 (2026-09-24)
4
11
 
5
12
  ### ⚠️ Breaking changes
6
13
 
7
14
  - vendor branding for every service; purge internal mocks and consumer names ([f689534](https://github.com/crvouga/mockingbird/commit/f6895343543550dafcb7d76b426cae7635e03226))
8
15
 
9
- ### Dependencies
10
-
11
- - `@crvouga/mockingbird-service-sqlite`
12
-
13
16
  ## 0.2.0 (2026-09-23)
14
17
 
15
18
  ### Features
@@ -1503,6 +1503,90 @@ var operationMetadata = (operation) => {
1503
1503
 
1504
1504
  // ../core/dist/service.js
1505
1505
  import { Hono } from "hono";
1506
+
1507
+ // ../core/dist/validation.js
1508
+ var requestBodyOf = (context) => {
1509
+ const requestBody = context.operation.operation.requestBody;
1510
+ return requestBody ? deref(context.document, requestBody) : void 0;
1511
+ };
1512
+ var mediaTypeMatches = (accepted, actual) => {
1513
+ const pattern = mediaTypeOf(accepted) ?? accepted.toLowerCase();
1514
+ if (pattern === actual || pattern === "*/*")
1515
+ return true;
1516
+ const [type, subtype] = pattern.split("/");
1517
+ const [actualType, actualSubtype] = actual.split("/");
1518
+ if (type !== actualType && type !== "*")
1519
+ return false;
1520
+ if (subtype === void 0 || subtype === "*")
1521
+ return true;
1522
+ if (subtype.startsWith("*+"))
1523
+ return actualSubtype?.endsWith(subtype.slice(1)) === true;
1524
+ return false;
1525
+ };
1526
+ var acceptedKeyFor = (content, mediaType) => {
1527
+ if (!content || mediaType === void 0)
1528
+ return void 0;
1529
+ const keys = Object.keys(content);
1530
+ return keys.find((key) => mediaTypeOf(key) === mediaType) ?? keys.find((key) => mediaTypeMatches(key, mediaType));
1531
+ };
1532
+ var unsupportedMediaType = (context, options = {}) => {
1533
+ if (context.body.kind === "empty" && options.includeEmpty !== true)
1534
+ return void 0;
1535
+ const content = requestBodyOf(context)?.content;
1536
+ if (!content || Object.keys(content).length === 0)
1537
+ return void 0;
1538
+ const mediaType = mediaTypeOf(context.request.headers.get("content-type"));
1539
+ if (acceptedKeyFor(content, mediaType) !== void 0)
1540
+ return void 0;
1541
+ return { mediaType: mediaType ?? null, accepted: Object.keys(content) };
1542
+ };
1543
+ var recorded = /* @__PURE__ */ new WeakMap();
1544
+ var recordedIssues = (request) => recorded.get(request);
1545
+ var bodyIssues = (context, contentType = "application/json") => {
1546
+ const issues = findBodyIssues(context, contentType);
1547
+ if (issues.length > 0)
1548
+ recorded.set(context.request, issues);
1549
+ return issues;
1550
+ };
1551
+ var findBodyIssues = (context, contentType) => {
1552
+ const resolved = requestBodyOf(context);
1553
+ if (!resolved)
1554
+ return [];
1555
+ const unsupported = unsupportedMediaType(context);
1556
+ if (unsupported) {
1557
+ return [
1558
+ {
1559
+ path: "",
1560
+ kind: "media_type",
1561
+ message: unsupported.mediaType === null ? "request body has no content-type" : `request body media type ${unsupported.mediaType} is not supported`
1562
+ }
1563
+ ];
1564
+ }
1565
+ const key = acceptedKeyFor(resolved.content, mediaTypeOf(context.request.headers.get("content-type"))) ?? contentType;
1566
+ const schema = resolved.content?.[key]?.schema;
1567
+ if (!schema)
1568
+ return [];
1569
+ if (context.body.kind === "invalid") {
1570
+ return [
1571
+ {
1572
+ path: "",
1573
+ kind: "syntax",
1574
+ message: `request body is not valid ${context.body.mediaType}`
1575
+ }
1576
+ ];
1577
+ }
1578
+ const value = context.body.kind === "json" || context.body.kind === "form" || context.body.kind === "text" ? context.body.value : void 0;
1579
+ if (value === void 0) {
1580
+ return resolved.required && context.body.kind === "empty" ? [{ path: "", kind: "required", message: "request body is required" }] : [];
1581
+ }
1582
+ return validateValue(context.document, resolveSchema(context.document, schema), value).map((issue) => ({ path: issue.path.join("."), kind: "schema", message: issue.message }));
1583
+ };
1584
+
1585
+ // ../core/dist/service.js
1586
+ var withIssues = (request, response) => {
1587
+ const issues = response.status >= 400 ? recordedIssues(request) : void 0;
1588
+ return issues && issues.length > 0 ? annotateResponse(response, { issues }) : response;
1589
+ };
1506
1590
  var defineOperations = (handlers) => handlers;
1507
1591
  var OperationRegistryError = class extends Error {
1508
1592
  problems;
@@ -1588,9 +1672,7 @@ var createService = (options) => {
1588
1672
  now
1589
1673
  };
1590
1674
  const short = await options.before?.(context);
1591
- if (short)
1592
- return short;
1593
- return handler(context);
1675
+ return withIssues(request, short ?? await handler(context));
1594
1676
  };
1595
1677
  app.on(operation.method.toUpperCase(), honoPath(operation.path), route);
1596
1678
  }
@@ -1627,7 +1709,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
1627
1709
  };
1628
1710
 
1629
1711
  // ../core/dist/version.js
1630
- var PACKAGE_VERSION = true ? "1.0.0" : UNRELEASED_VERSION;
1712
+ var PACKAGE_VERSION = true ? "1.0.1" : UNRELEASED_VERSION;
1631
1713
 
1632
1714
  // ../core/dist/signing.js
1633
1715
  var encoder = new TextEncoder();
@@ -2385,8 +2467,15 @@ var createRuntime = (options) => {
2385
2467
  const started = monotonicNow();
2386
2468
  const url = new URL(request.url);
2387
2469
  const operationId = operationIdFor(request, url.pathname);
2470
+ const contentLength = request.headers.get("content-length");
2471
+ const received = {
2472
+ contentType: request.headers.get("content-type"),
2473
+ bodyBytes: contentLength !== null && /^\d+$/.test(contentLength) ? Number(contentLength) : null,
2474
+ transferEncoding: request.headers.get("transfer-encoding")
2475
+ };
2388
2476
  const log = (status, faultId, response2) => {
2389
2477
  const noted = response2 ? responseNotes(response2) : void 0;
2478
+ const rejected = status >= 400 && faultId === void 0;
2390
2479
  const entry = {
2391
2480
  service: options.name,
2392
2481
  namespace,
@@ -2398,7 +2487,9 @@ var createRuntime = (options) => {
2398
2487
  unmatched: options.document !== void 0 && operationId === void 0,
2399
2488
  ...faultId !== void 0 ? { faultId } : {},
2400
2489
  ...noted?.ids && Object.keys(noted.ids).length > 0 ? { ids: noted.ids } : {},
2401
- ...noted?.adopted ? { adopted: true } : {}
2490
+ ...noted?.adopted ? { adopted: true } : {},
2491
+ ...rejected ? { request: { ...received } } : {},
2492
+ ...rejected && noted?.issues && noted.issues.length > 0 ? { issues: noted.issues.map((issue) => ({ ...issue })) } : {}
2402
2493
  };
2403
2494
  metrics.record(entry);
2404
2495
  journal.record({ ...entry, at: new Date(clock.now()).toISOString() });
@@ -2605,25 +2696,6 @@ var presetRoutes = (presets, runtime) => ({
2605
2696
  }
2606
2697
  });
2607
2698
 
2608
- // ../core/dist/validation.js
2609
- var bodyIssues = (context, contentType = "application/json") => {
2610
- const requestBody = context.operation.operation.requestBody;
2611
- if (!requestBody)
2612
- return [];
2613
- const resolved = deref(context.document, requestBody);
2614
- const schema = resolved.content?.[contentType]?.schema;
2615
- if (!schema)
2616
- return [];
2617
- const value = context.body.kind === "json" || context.body.kind === "form" ? context.body.value : void 0;
2618
- if (context.body.kind === "invalid") {
2619
- return [{ path: "", message: `request body is not valid ${context.body.mediaType}` }];
2620
- }
2621
- if (value === void 0) {
2622
- return resolved.required ? [{ path: "", message: "request body is required" }] : [];
2623
- }
2624
- return validateValue(context.document, resolveSchema(context.document, schema), value).map((issue) => ({ path: issue.path.join("."), message: issue.message }));
2625
- };
2626
-
2627
2699
  // src/generated/openapi.ts
2628
2700
  var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Resend API (Mockingbird subset)","description":"Stateful mock subset of the Resend email API: send (with \`Idempotency-Key\`), retrieve a sent\\nemail, and the received-email (inbound) endpoints our care-chat hydration reads, including\\nthe attachment download URL. Trimmed from Resend's published API reference to what\\n\`resend@4.8.0\` and our backend call.\\n","version":"1","x-mockingbird-upstream":{"note":"Trimmed from https://resend.com/docs/api-reference (emails/send-email, emails/retrieve-email, receiving/retrieve-received-email, receiving/list-attachments); request/response shapes cross-checked against resend@4.8.0 (dist/index.mjs) and our consumer (send-emails.service.ts, notification-dispatcher email.ts, resend-inbound-email.client.ts)."}},"servers":[{"url":"https://api.resend.com"}],"security":[{"bearerAuth":[]}],"paths":{"/emails":{"post":{"operationId":"SendEmail","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"Idempotency-Key","in":"header","description":"Replays the first response for the same key and payload (1\u2013256 characters).","schema":{"type":"string","pattern":"^[!-~]{1,256}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendEmailBody"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","x-mockingbird-resource":{"type":"email","identity":true},"x-mockingbird-volatile":{"kind":"id"}}}}}}},"400":{"description":"Invalid idempotency key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency key reused with another payload, or still in flight","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/emails/{email_id}":{"parameters":[{"name":"email_id","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"email","missing":"00000000-0000-4000-8000-000000000000"}}}],"get":{"operationId":"GetEmail","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The sent email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Email"}}}},"401":{"description":"Missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/emails/receiving/{email_id}":{"parameters":[{"$ref":"#/components/parameters/ReceivedEmailId"}],"get":{"operationId":"GetReceivedEmail","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The received email, with its content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceivedEmail"}}}},"401":{"description":"Missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/emails/receiving/{email_id}/attachments":{"parameters":[{"$ref":"#/components/parameters/ReceivedEmailId"}],"get":{"operationId":"ListReceivedEmailAttachments","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Attachment metadata with short-lived download URLs","content":{"application/json":{"schema":{"type":"object","required":["object","data"],"properties":{"object":{"type":"string","enum":["list"]},"has_more":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ReceivedAttachment"}}}}}}},"401":{"description":"Missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/downloads/inbound/{attachment_id}":{"get":{"operationId":"DownloadReceivedAttachment","description":"The \`download_url\` of a received attachment: unauthenticated (it stands in for Resend's signed CDN URL), the raw bytes with their content type and length.","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"received_attachment","missing":"00000000-0000-4000-8000-000000000000"}}}],"responses":{"200":{"description":"The attachment bytes","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Unknown attachment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"\`Authorization: Bearer re_\u2026\`"}},"parameters":{"ReceivedEmailId":{"name":"email_id","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"received_email","missing":"00000000-0000-4000-8000-000000000000"}}}},"schemas":{"Address":{"type":"string","description":"\`email@example.com\` or \`Name <email@example.com>\`","pattern":"^([^<>@]* <[A-Za-z0-9._+-]+@[A-Za-z0-9-]+(\\\\.[A-Za-z0-9-]+)+>|[A-Za-z0-9._+-]+@[A-Za-z0-9-]+(\\\\.[A-Za-z0-9-]+)+)$","maxLength":120},"Recipients":{"oneOf":[{"$ref":"#/components/schemas/Address"},{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/Address"}}]},"SendEmailBody":{"type":"object","required":["from","to","subject"],"properties":{"from":{"$ref":"#/components/schemas/Address"},"to":{"$ref":"#/components/schemas/Recipients"},"subject":{"type":"string","minLength":1,"maxLength":200},"html":{"type":"string","maxLength":5000},"text":{"type":"string","maxLength":5000},"cc":{"$ref":"#/components/schemas/Recipients"},"bcc":{"$ref":"#/components/schemas/Recipients"},"reply_to":{"$ref":"#/components/schemas/Recipients"},"scheduled_at":{"type":"string","maxLength":60},"headers":{"type":"object","additionalProperties":{"type":"string","maxLength":500}},"tags":{"type":"array","maxItems":10,"items":{"type":"object","required":["name","value"],"properties":{"name":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,256}$"},"value":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,256}$"}}}},"attachments":{"type":"array","maxItems":5,"items":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":100},"content":{"description":"Base64 bytes (or a JSON-serialised Node Buffer).","oneOf":[{"type":"string","maxLength":10000},{"type":"object","required":["type","data"],"properties":{"type":{"type":"string","enum":["Buffer"]},"data":{"type":"array","maxItems":10000,"items":{"type":"integer","minimum":0,"maximum":255}}}}]},"path":{"type":"string","maxLength":500},"content_type":{"type":"string","maxLength":100},"inline_content_id":{"type":"string","maxLength":100}}}}}},"Email":{"type":"object","required":["object","id","to","from","created_at","subject","html","text","bcc","cc","reply_to","last_event"],"properties":{"object":{"type":"string","enum":["email"]},"id":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"to":{"type":"array","items":{"type":"string"}},"from":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"subject":{"type":"string"},"html":{"type":["string","null"]},"text":{"type":["string","null"]},"bcc":{"type":["array","null"],"items":{"type":"string"}},"cc":{"type":["array","null"],"items":{"type":"string"}},"reply_to":{"type":["array","null"],"items":{"type":"string"}},"last_event":{"type":"string","enum":["delivered","scheduled"]},"scheduled_at":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}}}},"ReceivedAttachment":{"type":"object","required":["id","filename","content_type","size","download_url"],"properties":{"id":{"type":"string","x-mockingbird-resource":{"type":"received_attachment","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"filename":{"type":"string"},"content_type":{"type":"string"},"content_disposition":{"type":["string","null"]},"content_id":{"type":["string","null"]},"size":{"type":"integer"},"download_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"expires_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"ReceivedEmail":{"type":"object","required":["object","id","to","from","created_at","subject","html","text","message_id","attachments"],"properties":{"object":{"type":"string","enum":["email"]},"id":{"type":"string","x-mockingbird-resource":{"type":"received_email","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"to":{"type":"array","items":{"type":"string"}},"from":{"type":"string"},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"reply_to":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"subject":{"type":["string","null"]},"html":{"type":["string","null"]},"text":{"type":["string","null"]},"headers":{"type":"object","additionalProperties":{"type":"string"}},"message_id":{"type":["string","null"]},"attachments":{"type":"array","items":{"type":"object","required":["id","filename","content_type"],"properties":{"id":{"type":"string"},"filename":{"type":"string"},"content_type":{"type":"string"},"content_disposition":{"type":["string","null"]},"content_id":{"type":["string","null"]}}}}}},"Error":{"type":"object","description":"Resend's error body; the SDK returns it verbatim as \`error\`.","required":["statusCode","name","message"],"properties":{"statusCode":{"type":"integer"},"name":{"type":"string"},"message":{"type":"string"}}}}}}`);
2629
2701
  var operationIds = ["SendEmail", "GetEmail", "GetReceivedEmail", "ListReceivedEmailAttachments", "DownloadReceivedAttachment"];
@@ -3230,4 +3302,4 @@ export {
3230
3302
  bareAddress,
3231
3303
  ResendAPI
3232
3304
  };
3233
- //# sourceMappingURL=chunk-MAW2TLUJ.js.map
3305
+ //# sourceMappingURL=chunk-C4R6D4IN.js.map