@crvouga/mockingbird-service-resend 1.5.1 → 1.6.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,14 +1,25 @@
1
1
  # Changelog — @crvouga/mockingbird-service-resend
2
2
 
3
- ## 1.5.1 (2026-09-30)
3
+ ## 1.6.1 (2026-09-30)
4
4
 
5
5
  ### Fixes and improvements
6
6
 
7
- - keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
7
+ - keep admin buttons pressed until the action finishes ([0aabfcb](https://github.com/crvouga/mockingbird/commit/0aabfcb5f392aa51e193f9c3f817d5877d92bc6a))
8
+ - size admin select fields like the text inputs beside them ([0fb3ef2](https://github.com/crvouga/mockingbird/commit/0fb3ef2ddbf43376c93a2ae6d87c6c9bf890dafc))
9
+ - keep admin section tabs wired when boot is renamed ([4eda25e](https://github.com/crvouga/mockingbird/commit/4eda25eacd59da048d196f7cc8369d7a02b47bfc))
10
+
11
+ ## 1.6.0 (2026-09-29)
12
+
13
+ ### Features
8
14
 
9
- ### Dependencies
15
+ - boot Bolt over Socket Mode ([01322df](https://github.com/crvouga/mockingbird/commit/01322df315c66b1f2f5b53094cbbeeeef6d3ae6c))
16
+ - accept a send before dropping its response ([77c619d](https://github.com/crvouga/mockingbird/commit/77c619d6a54e7fdeef22b713d29aee0130d84f19))
10
17
 
11
- - `@crvouga/mockingbird-service-sqlite`
18
+ ## 1.5.1 (2026-09-29)
19
+
20
+ ### Fixes and improvements
21
+
22
+ - keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
12
23
 
13
24
  ## 1.5.0 (2026-09-29)
14
25
 
package/README.md CHANGED
@@ -76,17 +76,20 @@ await mock.close()
76
76
  | Route | Behaviour |
77
77
  | --- | --- |
78
78
  | `POST /emails` | `{from, to, subject, html?, text?, cc?, bcc?, reply_to?, headers?, tags?, attachments?, scheduled_at?}` → `{id}` (a UUID). The SDK renders `react` to HTML before sending, so the mock sees HTML. Violations answer Resend's body `{statusCode, name, message}`: 422 `missing_required_field` (`Missing \`to\` field.`, or no `html`/`text`), 422 `validation_error` for a malformed address (`Invalid \`from\` field. The email address needs to follow …`), a tag outside `[A-Za-z0-9_-]`, or any other contract violation. `Idempotency-Key`: the same key and payload replay the first 200 byte for byte (`idempotent-replayed: true`); another payload is 409 `invalid_idempotent_request`; a key still in flight is 409 `concurrent_idempotent_requests`; a key outside 1–256 characters is 400 `invalid_idempotency_key`. Only 200s are remembered. |
79
- | `GET /emails/{id}` | `emails.get`: `{object: "email", id, to, from, created_at, subject, html, text, cc, bcc, reply_to, last_event: "delivered" \| "scheduled", scheduled_at, tags}`. |
79
+ | `GET /emails/{id}` | `emails.get`: `{object: "email", id, to, from, created_at, subject, html, text, cc, bcc, reply_to, last_event: "delivered" \| "scheduled", scheduled_at, headers, tags}`. `headers` keeps the names and values from the send; `to` keeps the original address form and order. |
80
80
  | `GET /emails/receiving/{id}` | A received email: `{object: "email", id, to, from, cc, bcc, reply_to, created_at, subject, html, text, headers, message_id, attachments[{id, filename, content_type, content_disposition, content_id}]}`. |
81
81
  | `GET /emails/receiving/{id}/attachments` | `{object: "list", has_more: false, data: [{id, filename, content_type, content_disposition, content_id, size, download_url, expires_at}]}`. |
82
82
  | `GET /downloads/inbound/{attachment_id}` | The `download_url`: unauthenticated, the bytes with `content-type` and `content-length`. In a non-default namespace the URL carries `/ns/<name>`. |
83
83
 
84
84
  Auth is `Authorization: Bearer <key>`; any key works; none is 401 `missing_api_key`.
85
85
 
86
- **SDK error mapping** (`resend@4.8.0` never throws): a non-2xx JSON body comes back verbatim as
86
+ **SDK error mapping.** `resend@4.8.0` never throws: a non-2xx JSON body comes back verbatim as
87
87
  `{data: null, error: {statusCode, name, message}}`; a non-JSON body becomes
88
88
  `{name: "application_error", message: "Internal server error. …"}`; a dropped connection
89
89
  becomes `{name: "application_error", message: "Unable to fetch data. The request could not be resolved."}`.
90
+ `resend@2.1.0` also reads `RESEND_BASE_URL` at import and returns that JSON error object, but its
91
+ client does not catch a non-JSON body or a dropped socket (those reject). It also has no
92
+ `idempotencyKey` option; send the `Idempotency-Key` header yourself, as `resend@4.8.0` does.
90
93
 
91
94
  ### Webhooks
92
95
 
@@ -110,6 +113,7 @@ official `svix` `Webhook.verify` accepts them. Non-2xx answers are retried (imme
110
113
 
111
114
  | Route | Effect |
112
115
  | --- | --- |
116
+ | `GET /__admin/outcomes` | `{accepted, lost, replayed}` for sends whose response was dropped after acceptance, those lost responses, and idempotent replays. |
113
117
  | `GET /__admin/outbox?to=&tag=<name>:<value>&since=&limit=` | Sent emails, oldest first: `{id, from, to (bare, lower-cased), toHeader, cc, bcc, replyTo, subject, html, text, tags, headers, attachments (filename, contentType, size), idempotencyKey, scheduledAt, createdAt}`. `tag=category` matches any value. `GET /__admin/outbox/:id` returns one. |
114
118
  | `GET /__admin/outbox/:id/links` | `{id, links}`: every `href` in the HTML, entity-decoded (every URL in the text when there is no HTML). |
115
119
  | `POST /__admin/inbound` | `{from, to, cc?, bcc?, replyTo?, subject?, text?, html?, headers?, messageId?, attachments?: [{filename, content (base64), contentType?, contentId?, contentDisposition?}], inline?}` → 201 `{id, webhook, event}`. |
@@ -125,7 +129,8 @@ Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`; `GET /_
125
129
  `send_422` (`{statusCode: 422, name: "validation_error", message}`), `send_429`
126
130
  (`rate_limit_exceeded` with `retry-after` and `ratelimit-*` headers), `send_500`
127
131
  (`internal_server_error`), `non_json_500` (an HTML 500 page), `network_drop` (the connection
128
- dies before an answer), `receiving_500` (the received-email routes answer 500),
132
+ dies before acceptance, so nothing is stored), `accepted_then_network_drop` (the send is stored,
133
+ then the connection dies before any response bytes; the same key replays), `receiving_500` (the received-email routes answer 500),
129
134
  `webhook_duplicate`, `webhook_reorder`, `webhook_drop`.
130
135
 
131
136
  ### Namespaces
@@ -148,7 +153,7 @@ dies before an answer), `receiving_500` (the received-email routes answer 500),
148
153
 
149
154
  | Export | Kind | Description |
150
155
  | --- | --- | --- |
151
- | `ResendAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `sent()`, `inbound()`, `receive(input, origin)`, `state`. Options: `sqlite`, `now`, `namespace`, `publicNamespace`, `onSent`. |
156
+ | `ResendAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `sent()`, `sendOutcomes()`, `inbound()`, `receive(input, origin)`, `state`. Options: `sqlite`, `now`, `namespace`, `publicNamespace`, `onSent`, `onOutcome`. |
152
157
  | `createRuntime` | function | The mock with the full service contract (health, admin, namespaces, credentials, presets, Svix webhooks, outbox). Options: `webhooks: {url, secret, retryDelaysMs?, fetch?}`, `forwardToInbox: {url, adminKey?, timeoutMs?, fetch?}`, `clock`, `seed`, `adminKey`, `onLog`, `sqlite`. |
153
158
  | `forwardToInbox` | function | Copy one sent email into a Mailosaur mock's ingest route. |
154
159
  | `RESEND_PRESETS` | object | Every named fault preset. |
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createRuntime
3
- } from "./chunk-FSFGDVP6.js";
3
+ } from "./chunk-RUK2K5Y6.js";
4
4
 
5
5
  // ../../adapters/node/dist/cli.js
6
6
  import { readFile } from "node:fs/promises";
@@ -235,6 +235,7 @@ var start = async (target, values, config) => {
235
235
  onLog: formatLog(config.log)
236
236
  });
237
237
  const listening = await listen(runtime, { port: config.port, host: config.host });
238
+ target.listening?.(listening);
238
239
  console.log(`${target.name} mock listening on ${listening.url}`);
239
240
  console.log(`${target.name} health: GET ${listening.url}/health`);
240
241
  console.log(`${target.name} admin: ${listening.url}/__admin (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
@@ -369,4 +370,4 @@ export {
369
370
  createServer2 as createServer,
370
371
  serveTarget
371
372
  };
372
- //# sourceMappingURL=chunk-OIK3EYGO.js.map
373
+ //# sourceMappingURL=chunk-FVK4CBNZ.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../adapters/node/src/cli.ts", "../../../adapters/node/src/serve.ts", "../../../adapters/node/src/listen.ts", "../src/server.ts"],
4
+ "sourcesContent": ["import { readFile } from \"node:fs/promises\"\nimport { type ParseArgsConfig, parseArgs } from \"node:util\"\nimport type { RequestLog, ServiceInstance, ServiceRuntime } from \"@crvouga/mockingbird-service\"\nimport { type Listening, listen } from \"./listen.js\"\n\nexport type CliOption = {\n type: \"string\" | \"boolean\"\n description: string\n /** Shown in help; the value placeholder, e.g. `<port>`. */\n value?: string\n default?: string | boolean\n}\n\nexport type CliValues = Record<string, string | boolean | undefined>\n\nexport type CliCommand = {\n summary: string\n usage?: string\n options?: Record<string, CliOption>\n /** Resolves to an exit code; a server command resolves only when it stops. */\n run(values: CliValues, positionals: string[]): Promise<number>\n}\n\nexport type CliSpec = {\n bin: string\n description: string\n commands: Record<string, CliCommand>\n}\n\nconst optionHelp = (options: Record<string, CliOption>): string[] =>\n Object.entries(options).map(([name, option]) => {\n const flag = `--${name}${option.type === \"string\" ? ` ${option.value ?? \"<value>\"}` : \"\"}`\n const fallback = option.default !== undefined ? ` (default: ${String(option.default)})` : \"\"\n return ` ${flag.padEnd(30)} ${option.description}${fallback}`\n })\n\nconst help = (spec: CliSpec): string =>\n [\n `${spec.bin} \u2014 ${spec.description}`,\n \"\",\n \"Usage:\",\n ` ${spec.bin} <command> [options]`,\n \"\",\n \"Commands:\",\n ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),\n \"\",\n `Run \\`${spec.bin} <command> --help\\` for a command's options.`,\n ].join(\"\\n\")\n\nconst commandHelp = (spec: CliSpec, name: string, command: CliCommand): string =>\n [\n `${spec.bin} ${name} \u2014 ${command.summary}`,\n \"\",\n \"Usage:\",\n ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,\n ...(command.options ? [\"\", \"Options:\", ...optionHelp(command.options)] : []),\n ].join(\"\\n\")\n\n/** Parse `argv` against `spec` and run the chosen command. Resolves to an exit code. */\nexport const runCli = async (spec: CliSpec, argv: string[]): Promise<number> => {\n // Two-word commands (`corpus pull`) win over one-word ones.\n const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : undefined\n const words = pair !== undefined && spec.commands[pair] ? 2 : 1\n const name = words === 2 ? pair : argv[0]\n const rest = argv.slice(words)\n if (name === undefined || name === \"--help\" || name === \"-h\" || name === \"help\") {\n console.log(help(spec))\n return 0\n }\n const command = spec.commands[name]\n if (!command) {\n console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}\\n\\n${help(spec)}`)\n return 2\n }\n if (rest.includes(\"--help\") || rest.includes(\"-h\")) {\n console.log(commandHelp(spec, name, command))\n return 0\n }\n let parsed: { values: CliValues; positionals: string[] }\n try {\n parsed = parseArgs({\n args: rest,\n allowPositionals: true,\n strict: true,\n options: Object.fromEntries(\n Object.entries(command.options ?? {}).map(([key, option]) => [\n key,\n {\n type: option.type,\n ...(option.default !== undefined ? { default: option.default } : {}),\n },\n ]),\n ) as ParseArgsConfig[\"options\"],\n }) as { values: CliValues; positionals: string[] }\n } catch (error) {\n console.error(\n `${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}\\n\\n${commandHelp(spec, name, command)}`,\n )\n return 2\n }\n return command.run(parsed.values, parsed.positionals)\n}\n\n// \u2500\u2500 serve \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type LogFormat = \"pretty\" | \"json\" | \"off\"\n\nexport type CommonServeOptions = {\n adminKey: string | undefined\n seed: string | undefined\n onLog: ((entry: RequestLog) => void) | undefined\n}\n\n/**\n * What a service contributes to `serve`: how to build its runtime from CLI flags,\n * and what to say at startup. Every service's `./server` entry exports one as\n * `serveTarget`, which is also how `serve --config` finds services by name.\n */\nexport type ServeTarget = {\n name: string\n defaultPort: number\n /** Serve flags beyond the common ones. */\n options?: Record<string, CliOption>\n create(\n values: CliValues,\n common: CommonServeOptions,\n ): Promise<ServiceRuntime<ServiceInstance>> | ServiceRuntime<ServiceInstance>\n /** Startup lines after the listen address, e.g. the loaded corpus version. */\n banner?(runtime: ServiceRuntime<ServiceInstance>): string[]\n /** After the HTTP server is bound. A service that speaks more than HTTP attaches it here. */\n listening?(server: Listening): void\n}\n\nconst COMMON_SERVE_OPTIONS: Record<string, CliOption> = {\n port: { type: \"string\", value: \"<port>\", description: \"Port to listen on\" },\n host: { type: \"string\", value: \"<host>\", description: \"Interface to bind\", default: \"127.0.0.1\" },\n \"admin-key\": {\n type: \"string\",\n value: \"<key>\",\n description: \"Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)\",\n },\n seed: { type: \"string\", value: \"<seed>\", description: \"Seed for every random choice\" },\n log: {\n type: \"string\",\n value: \"<pretty|json|off>\",\n description: \"Request log format\",\n default: \"pretty\",\n },\n \"log-requests\": {\n type: \"boolean\",\n description:\n \"One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json\",\n },\n config: {\n type: \"string\",\n value: \"<file>\",\n description: \"Serve every service in a mockingbird.json config instead\",\n },\n}\n\nconst formatLog = (format: LogFormat) => {\n if (format === \"off\") return undefined\n if (format === \"json\") return (entry: RequestLog) => console.log(JSON.stringify(entry))\n return (entry: RequestLog) => {\n const op = entry.operationId ?? (entry.unmatched ? \"UNMATCHED\" : \"-\")\n const ns = entry.namespace === \"default\" ? \"\" : ` [${entry.namespace}]`\n const fault = entry.faultId ? ` fault=${entry.faultId}` : \"\"\n const adopted = entry.adopted ? \" adopted\" : \"\"\n // A rejection says what arrived (never the body) and why the service refused it.\n const received = entry.request\n ? ` received=${entry.request.bodyBytes === null ? \"?\" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? \"-\"}${\n entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : \"\"\n }`\n : \"\"\n const issues = entry.issues?.length\n ? ` issues=${entry.issues\n .slice(0, 3)\n .map((issue) => `${issue.path || \"$\"}: ${issue.message}`)\n .join(\"; \")}${entry.issues.length > 3 ? ` (+${entry.issues.length - 3})` : \"\"}`\n : \"\"\n console.log(\n `${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`,\n )\n }\n}\n\nconst asString = (value: string | boolean | undefined): string | undefined =>\n typeof value === \"string\" ? value : undefined\n\n/** A service entry in `mockingbird.json`. */\nexport type ConfigService = {\n port?: number\n host?: string\n adminKey?: string\n seed?: string\n /** Service-specific serve flags, by long name: `{ \"webhook-url\": \"\u2026\" }`. */\n options?: Record<string, string | boolean>\n}\n\nexport type MockingbirdConfig = {\n /** Keyed by service name: `junction` loads `@crvouga/mockingbird-service-junction`. */\n services: Record<string, ConfigService>\n log?: LogFormat\n}\n\nconst loadTarget = async (name: string, own: ServeTarget): Promise<ServeTarget> => {\n if (name === own.name) return own\n const specifier = `@crvouga/mockingbird-service-${name}/server`\n try {\n const mod = (await import(specifier)) as { serveTarget?: ServeTarget }\n if (!mod.serveTarget) throw new Error(`${specifier} exports no serveTarget`)\n return mod.serveTarget\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n throw new Error(\n `cannot load service \"${name}\": ${reason}. Install @crvouga/mockingbird-service-${name}.`,\n )\n }\n}\n\nconst start = async (\n target: ServeTarget,\n values: CliValues,\n config: { port: number; host: string; adminKey?: string; seed?: string; log: LogFormat },\n): Promise<Listening> => {\n const runtime = await target.create(values, {\n adminKey: config.adminKey,\n seed: config.seed,\n onLog: formatLog(config.log),\n })\n const listening = await listen(runtime, { port: config.port, host: config.host })\n target.listening?.(listening)\n console.log(`${target.name} mock listening on ${listening.url}`)\n console.log(`${target.name} health: GET ${listening.url}/health`)\n console.log(\n `${target.name} admin: ${listening.url}/__admin (${config.adminKey ? \"x-mockingbird-admin-key required\" : \"open \u2014 pass --admin-key to lock\"})`,\n )\n console.log(`${target.name} admin ui: ${listening.url}/__admin/ui`)\n for (const line of target.banner?.(runtime) ?? []) console.log(`${target.name} ${line}`)\n return listening\n}\n\nconst untilSignal = async (servers: Listening[]): Promise<number> =>\n new Promise((resolve) => {\n const stop = async () => {\n await Promise.allSettled(servers.map((s) => s.close()))\n resolve(0)\n }\n process.once(\"SIGINT\", stop)\n process.once(\"SIGTERM\", stop)\n })\n\n/** The standard `serve` command for a service, including multi-service `--config`. */\nexport const serveCommand = (target: ServeTarget): CliCommand => ({\n summary: `Serve the ${target.name} mock over HTTP`,\n options: { ...COMMON_SERVE_OPTIONS, ...target.options },\n async run(values) {\n const log = (\n values[\"log-requests\"] === true ? \"json\" : (asString(values.log) ?? \"pretty\")\n ) as LogFormat\n if (![\"pretty\", \"json\", \"off\"].includes(log)) {\n console.error(`--log must be pretty, json or off (got ${log})`)\n return 2\n }\n const configPath = asString(values.config)\n if (configPath !== undefined) {\n const config = JSON.parse(await readFile(configPath, \"utf8\")) as MockingbirdConfig\n const servers: Listening[] = []\n try {\n for (const [name, entry] of Object.entries(config.services ?? {})) {\n const each = await loadTarget(name, target)\n servers.push(\n await start(each, entry.options ?? {}, {\n port: entry.port ?? each.defaultPort,\n host: entry.host ?? \"127.0.0.1\",\n ...(entry.adminKey !== undefined ? { adminKey: entry.adminKey } : {}),\n ...(entry.seed !== undefined ? { seed: entry.seed } : {}),\n log: config.log ?? log,\n }),\n )\n }\n } catch (error) {\n await Promise.allSettled(servers.map((s) => s.close()))\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal(servers)\n }\n const port = asString(values.port)\n const adminKey = asString(values[\"admin-key\"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY\n const seed = asString(values.seed)\n let listening: Listening\n try {\n listening = await start(target, values, {\n port: port === undefined ? target.defaultPort : Number.parseInt(port, 10),\n host: asString(values.host) ?? \"127.0.0.1\",\n ...(adminKey !== undefined ? { adminKey } : {}),\n ...(seed !== undefined ? { seed } : {}),\n log,\n })\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal([listening])\n },\n})\n", "import { createServer } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\n\n/** Options for {@link serve}. */\nexport type NodeServeOptions = {\n port?: number\n host?: string\n}\n\n/**\n * Serve any Mockingbird {@link FetchAPI} over `node:http`.\n * Port defaults to `0`, so the OS assigns an ephemeral port (read from `server.address()`).\n */\nexport const serve = async (api: FetchAPI, options: NodeServeOptions = {}) => {\n const server = createServer(async (req, res) => {\n const chunks: Buffer[] = []\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk))\n }\n const body = Buffer.concat(chunks)\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : undefined\n const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`\n const raw = req.url ?? \"/\"\n const url = new URL(raw.replace(/^\\/+/, \"/\"), base)\n const method = req.method ?? \"GET\"\n const init: RequestInit = { method, headers: req.headers as Record<string, string> }\n if (method !== \"GET\" && method !== \"HEAD\" && body.length > 0) {\n init.body = body\n }\n const aborted = new AbortController()\n res.once(\"close\", () => {\n if (!res.writableFinished) aborted.abort()\n })\n init.signal = aborted.signal\n const request = new Request(url, init)\n let response: Response\n try {\n response = await api.fetch(request)\n } catch (error) {\n // A `drop` fault: destroy the socket so the client sees the connection die.\n if ((error as { code?: string }).code === \"MOCKINGBIRD_DROP\") {\n req.socket.destroy()\n return\n }\n res.writeHead(500, { \"content-type\": \"application/json\" })\n res.end(\n JSON.stringify({\n error: {\n type: \"mockingbird_internal\",\n message: error instanceof Error ? error.message : String(error),\n },\n }),\n )\n return\n }\n // Headers#entries() joins repeated headers; Set-Cookie must stay one header per cookie.\n const headers: Record<string, string | string[]> = Object.fromEntries(response.headers)\n const cookies = response.headers.getSetCookie()\n if (cookies.length > 0) headers[\"set-cookie\"] = cookies\n if (!response.body) {\n res.writeHead(response.status, headers)\n res.end()\n return\n }\n // Stream the body chunk by chunk: event streams and long-polls must not be buffered.\n res.writeHead(response.status, headers)\n res.flushHeaders()\n const reader = response.body.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!res.write(value)) await new Promise<void>((resolve) => res.once(\"drain\", resolve))\n }\n res.end()\n } catch {\n res.destroy()\n } finally {\n reader.releaseLock()\n }\n })\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, options.host, resolve)\n })\n return server\n}\n", "import type { Server } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\nimport { serve } from \"./serve.js\"\n\nexport type ListenOptions = {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`: a mock should not be reachable off the machine by accident. */\n host?: string\n}\n\n/** A running server, with the address it actually bound. */\nexport type Listening = {\n url: string\n port: number\n host: string\n server: Server\n close(): Promise<void>\n}\n\nexport const listen = async (api: FetchAPI, options: ListenOptions = {}): Promise<Listening> => {\n const host = options.host ?? \"127.0.0.1\"\n const server = await serve(api, { port: options.port ?? 0, host })\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : (options.port ?? 0)\n const shown = host.includes(\":\") ? `[${host}]` : host\n return {\n url: `http://${shown}:${port}`,\n port,\n host,\n server,\n close: () =>\n new Promise<void>((resolve, reject) => {\n // Stop accepting first, then drop keep-alive sockets so close() can finish.\n server.close((error) => {\n if (error && (error as { code?: string }).code !== \"ERR_SERVER_NOT_RUNNING\") reject(error)\n else resolve()\n })\n server.closeAllConnections?.()\n }),\n }\n}\n", "/// <reference types=\"node\" />\nimport { type Listening, listen, type ServeTarget } from \"@crvouga/mockingbird-adapter-node\"\nimport { createRuntime, type ResendRuntime, type ResendRuntimeOptions } from \"./runtime.js\"\n\n/** Port `mockingbird-resend serve` listens on when none is given. */\nexport const DEFAULT_PORT = 8794\n\nexport type ResendServerOptions = ResendRuntimeOptions & {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`. */\n host?: string\n}\n\nexport type ResendServer = Listening & { runtime: ResendRuntime }\n\n/** Serve the Resend mock over `node:http`. Point `RESEND_BASE_URL` at `url` before importing `resend`. */\nexport const createServer = async (options: ResendServerOptions = {}): Promise<ResendServer> => {\n const { port, host, ...rest } = options\n const runtime = createRuntime(rest)\n const listening = await listen(runtime, {\n port: port ?? 0,\n ...(host !== undefined ? { host } : {}),\n })\n return { ...listening, runtime }\n}\n\nconst text = (value: string | boolean | undefined) =>\n typeof value === \"string\" ? value : undefined\n\n/** How `serve` (and `serve --config`) builds the Resend mock from flags. */\nexport const serveTarget: ServeTarget = {\n name: \"resend\",\n defaultPort: DEFAULT_PORT,\n options: {\n \"webhook-url\": {\n type: \"string\",\n value: \"<url>\",\n description:\n \"Deliver email.received webhooks here (e.g. http://127.0.0.1:3000/messaging/inbound/email)\",\n },\n \"webhook-secret\": {\n type: \"string\",\n value: \"<whsec_\u2026>\",\n description: \"Svix signing secret (the app's RESEND_INBOUND_WEBHOOK_SECRET)\",\n },\n \"forward-to-inbox\": {\n type: \"string\",\n value: \"<url>\",\n description:\n \"Copy every sent email into a Mailosaur mock (its POST /__admin/ingest), e.g. http://127.0.0.1:8793\",\n },\n },\n create: (values, common) => {\n const url = text(values[\"webhook-url\"])\n const secret = text(values[\"webhook-secret\"])\n const inbox = text(values[\"forward-to-inbox\"])\n if (secret && !/^whsec_[A-Za-z0-9+/=]+$/.test(secret)) {\n throw new Error(\"--webhook-secret must be whsec_<base64>, as Resend issues it\")\n }\n return createRuntime({\n ...(url ? { webhooks: { url, ...(secret ? { secret } : {}) } } : {}),\n ...(inbox\n ? {\n forwardToInbox: {\n url: inbox,\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n },\n }\n : {}),\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n ...(common.seed !== undefined ? { seed: common.seed } : {}),\n ...(common.onLog ? { onLog: common.onLog } : {}),\n })\n },\n banner: () => [\n \"auth: Authorization: Bearer re_\u2026 (any key); set RESEND_BASE_URL before importing resend\",\n \"outbox: GET /__admin/outbox?to=&tag=category:<v>, GET /__admin/outbox/:id/links\",\n \"inbound: POST /__admin/inbound {from, to, subject, text?, html?, attachments?} \u2192 email.received\",\n \"namespaces: x-mockingbird-namespace, /ns/<name>/\u2026, or PUT /__admin/credentials {<api key>: <ns>}\",\n ],\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["createServer"]
7
+ }
@@ -2442,6 +2442,50 @@ function bootAdmin(config) {
2442
2442
  if (render)
2443
2443
  await render();
2444
2444
  }
2445
+ const delay = (ms) => new Promise((resolve) => {
2446
+ window.setTimeout(resolve, ms);
2447
+ });
2448
+ const confirmAction = async (button, pending, done, work) => {
2449
+ if (button.dataset.pending === "1")
2450
+ return;
2451
+ const label = button.textContent ?? "";
2452
+ const width = button.offsetWidth;
2453
+ button.dataset.pending = "1";
2454
+ button.classList.add("pressed");
2455
+ button.setAttribute("aria-busy", "true");
2456
+ button.style.minWidth = `${width}px`;
2457
+ button.textContent = pending;
2458
+ try {
2459
+ await work();
2460
+ button.textContent = done;
2461
+ await delay(1e3);
2462
+ } catch (error2) {
2463
+ showError(error2);
2464
+ } finally {
2465
+ button.textContent = label;
2466
+ button.style.minWidth = "";
2467
+ button.classList.remove("pressed");
2468
+ button.removeAttribute("aria-busy");
2469
+ delete button.dataset.pending;
2470
+ }
2471
+ };
2472
+ document.addEventListener("click", (event) => {
2473
+ const target = event.target;
2474
+ if (!(target instanceof Element))
2475
+ return;
2476
+ const button = target.closest("button");
2477
+ if (!(button instanceof HTMLButtonElement) || button.dataset.pending === "1")
2478
+ return;
2479
+ button.classList.add("pressed");
2480
+ const token = String(Date.now());
2481
+ button.dataset.press = token;
2482
+ window.setTimeout(() => {
2483
+ if (button.dataset.press !== token || button.dataset.pending === "1")
2484
+ return;
2485
+ button.classList.remove("pressed");
2486
+ delete button.dataset.press;
2487
+ }, 450);
2488
+ });
2445
2489
  el("namespace", HTMLSelectElement).addEventListener("change", () => {
2446
2490
  state.namespace = el("namespace", HTMLSelectElement).value;
2447
2491
  refresh().catch(showError);
@@ -2452,7 +2496,7 @@ function bootAdmin(config) {
2452
2496
  refresh().catch(showError);
2453
2497
  });
2454
2498
  el("refresh", HTMLButtonElement).addEventListener("click", () => {
2455
- refresh().catch(showError);
2499
+ confirmAction(el("refresh", HTMLButtonElement), "Refreshing...", "Updated", () => refresh()).catch(showError);
2456
2500
  });
2457
2501
  el("collections", HTMLElement).addEventListener("click", (event) => {
2458
2502
  const target = event.target;
@@ -2575,10 +2619,10 @@ function bootAdmin(config) {
2575
2619
  }).then(() => renderFaults()).catch(showError);
2576
2620
  });
2577
2621
  el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
2578
- api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
2622
+ confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
2579
2623
  });
2580
2624
  el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
2581
- api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
2625
+ confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
2582
2626
  });
2583
2627
  el("route-list", HTMLElement).addEventListener("click", (event) => {
2584
2628
  const target = event.target;
@@ -2693,9 +2737,13 @@ function bootAdmin(config) {
2693
2737
  var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
2694
2738
  var adminClientSource = (config) => {
2695
2739
  const payload = JSON.stringify(config).replace(/</g, "\\u003c");
2740
+ const entry = bootAdmin.name;
2741
+ if (!/^[A-Za-z_$][\w$]*$/.test(entry)) {
2742
+ throw new Error("admin client cannot call its boot function");
2743
+ }
2696
2744
  return `<script>
2697
2745
  ${clientPrelude}
2698
- bootAdmin(${payload});
2746
+ ${entry}(${payload});
2699
2747
  </script>`;
2700
2748
  };
2701
2749
 
@@ -2909,9 +2957,24 @@ var renderAdminDocument = (service) => {
2909
2957
  .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
2910
2958
  label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
2911
2959
  label.field input, label.field select, .controls input[type="password"] {
2912
- background: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2913
- padding: 7px 8px; min-height: 36px;
2914
- }
2960
+ background-color: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2961
+ padding: 7px 8px; min-height: 36px; line-height: 1.2;
2962
+ }
2963
+ /* A native menu ignores min-height, so Preset rendered about half as tall as
2964
+ the text fields in the same form. Paint it like those fields. */
2965
+ label.field select {
2966
+ -webkit-appearance: none; appearance: none;
2967
+ padding-right: 28px;
2968
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2.5 4.5 6 8l3.5-3.5'/%3E%3C/svg%3E");
2969
+ background-repeat: no-repeat; background-position: right 10px center; background-size: 12px 12px;
2970
+ }
2971
+ .stack > .field > input,
2972
+ .stack > .field > select,
2973
+ .stack > .field > textarea,
2974
+ dialog .field > input,
2975
+ dialog .field > select,
2976
+ dialog .field > textarea { width: 100%; }
2977
+ .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
2915
2978
  .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
2916
2979
  border-radius: 9px; min-height: 36px; padding: 0 12px; border: 1px solid var(--line);
2917
2980
  background: var(--bg-elev);
@@ -2919,6 +2982,14 @@ var renderAdminDocument = (service) => {
2919
2982
  .btn-primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
2920
2983
  .btn-quiet { background: transparent; }
2921
2984
  .btn-danger { color: var(--err); }
2985
+ /* Held after mouseup. Clearing an empty journal changes nothing on screen,
2986
+ so the press itself has to stay visible. */
2987
+ .icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
2988
+ .icon-btn.pressed, .btn.pressed, .btn-quiet.pressed, .btn-danger.pressed, nav.side button.pressed {
2989
+ background: var(--bg-inset); border-color: var(--line-strong); color: var(--ink);
2990
+ }
2991
+ .btn-danger:active, .btn-danger.pressed { background: var(--err-soft); color: var(--err); }
2992
+ .btn-primary:active, .btn-primary.pressed { filter: brightness(0.88); color: var(--accent-ink); }
2922
2993
  /* minmax(0, 1fr): a 1fr column will not shrink below its content, so a long
2923
2994
  record or the full nav row would widen the page past the viewport. */
2924
2995
  .layout {
@@ -3007,8 +3078,16 @@ var renderAdminDocument = (service) => {
3007
3078
  input, select, textarea { font-size: 16px; }
3008
3079
  .brand, .vendor, .controls { width: 100%; }
3009
3080
  label.field, label.field select, label.field input { flex: 1; min-width: 0; }
3010
- .layout { grid-template-columns: minmax(0, 1fr); }
3081
+ /* The layout fills the screen. A single-column grid stretches every auto
3082
+ row, so the tab bar grew with the leftover height and the buttons floated
3083
+ in the middle of it. The bar stays one row; the page below takes the rest. */
3084
+ .layout {
3085
+ grid-template-columns: minmax(0, 1fr);
3086
+ grid-template-rows: auto minmax(0, 1fr);
3087
+ align-content: start;
3088
+ }
3011
3089
  nav.side {
3090
+ align-self: start; height: auto;
3012
3091
  flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
3013
3092
  border-right: 0; border-bottom: 1px solid var(--line);
3014
3093
  padding: 8px;
@@ -3876,7 +3955,7 @@ var stateAdminRoutes = (routes) => {
3876
3955
  };
3877
3956
 
3878
3957
  // ../core/dist/version.js
3879
- var PACKAGE_VERSION = true ? "1.5.1" : UNRELEASED_VERSION;
3958
+ var PACKAGE_VERSION = true ? "1.6.1" : UNRELEASED_VERSION;
3880
3959
 
3881
3960
  // ../core/dist/signing.js
3882
3961
  var encoder = new TextEncoder();
@@ -4341,6 +4420,8 @@ var reuseSorted = (fresh, previous, compare, equal) => {
4341
4420
  }
4342
4421
  return unchanged ? previous : result;
4343
4422
  };
4423
+ var faultEffects = (request) => (effects.get(request) ?? []).filter((e) => e !== void 0);
4424
+ var faultEffect = (request, name) => faultEffects(request).find((e) => e.name === name)?.params;
4344
4425
  var DroppedConnectionError = class extends TypeError {
4345
4426
  code = "MOCKINGBIRD_DROP";
4346
4427
  constructor() {
@@ -4903,7 +4984,7 @@ var presetRoutes = (presets, runtime) => ({
4903
4984
  });
4904
4985
 
4905
4986
  // src/generated/openapi.ts
4906
- var document2 = 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"}}}}}}`);
4987
+ var document2 = 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"]},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Custom headers from the send, with their original names and values."},"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"}}}}}}`);
4907
4988
  var operationIds = ["SendEmail", "GetEmail", "GetReceivedEmail", "ListReceivedEmailAttachments", "DownloadReceivedAttachment"];
4908
4989
  var supportedOperationIds = ["SendEmail", "GetEmail", "GetReceivedEmail", "ListReceivedEmailAttachments", "DownloadReceivedAttachment"];
4909
4990
 
@@ -4992,9 +5073,13 @@ var RESEND_PRESETS = {
4992
5073
  ]
4993
5074
  },
4994
5075
  network_drop: {
4995
- description: "Send drops the connection before answering (the SDK reports 'Unable to fetch data')",
5076
+ description: "Send drops the connection before acceptance (no email, no idempotency record). resend@4.8.0 reports 'Unable to fetch data'; resend@2.1.0 rejects.",
4996
5077
  rules: [{ operationId: "SendEmail", drop: true }]
4997
5078
  },
5079
+ accepted_then_network_drop: {
5080
+ description: "The next valid send is validated and stored (email, idempotency record, outbox), then the connection closes before any response bytes. A retry of the same key replays the original id. resend@4.8.0 reports 'Unable to fetch data'; resend@2.1.0 rejects.",
5081
+ rules: [{ operationId: "SendEmail", effect: "accepted_then_network_drop" }]
5082
+ },
4998
5083
  receiving_500: {
4999
5084
  description: "The received-email endpoints answer 500 (our hydration throws 'could not be retrieved')",
5000
5085
  rules: [
@@ -5091,6 +5176,7 @@ var adminRoutes = (hub) => (runtime) => ({
5091
5176
  );
5092
5177
  }
5093
5178
  ),
5179
+ "GET /outcomes": ({ namespace }) => json6(200, runtime.instance(namespace).sendOutcomes()),
5094
5180
  "GET /outbox/:id/links": ({ params, namespace }) => {
5095
5181
  const email = runtime.instance(namespace).state.outbox.get(params.id);
5096
5182
  if (!email) return adminError4(404, `no email ${params.id}`);
@@ -5121,6 +5207,8 @@ var createRuntime2 = (options = {}) => {
5121
5207
  });
5122
5208
  const stats = { forwarded: 0, failed: 0, lastError: null };
5123
5209
  const target = options.forwardToInbox;
5210
+ let recordOutcome = () => {
5211
+ };
5124
5212
  const runtime = createRuntime({
5125
5213
  name: RESEND_NAMESPACE,
5126
5214
  document: document2,
@@ -5137,6 +5225,7 @@ var createRuntime2 = (options = {}) => {
5137
5225
  namespace,
5138
5226
  publicNamespace,
5139
5227
  now: clock.now,
5228
+ onOutcome: (event) => recordOutcome(event),
5140
5229
  ...target ? {
5141
5230
  onSent: async (email) => {
5142
5231
  try {
@@ -5161,6 +5250,24 @@ var createRuntime2 = (options = {}) => {
5161
5250
  "GET /forwarding": () => json6(200, { target: target?.url ?? null, ...stats })
5162
5251
  })
5163
5252
  });
5253
+ recordOutcome = (event) => {
5254
+ const entry = {
5255
+ service: RESEND_NAMESPACE,
5256
+ namespace: event.namespace,
5257
+ operationId: "SendEmail",
5258
+ method: "POST",
5259
+ path: "/emails",
5260
+ status: event.outcome === "lost" ? 0 : 200,
5261
+ durationMs: 0,
5262
+ unmatched: false,
5263
+ ...event.outcome === "lost" ? { faultId: "accepted_then_network_drop" } : {},
5264
+ ...event.emailId ? { ids: { emailId: event.emailId } } : {},
5265
+ outcome: event.outcome
5266
+ };
5267
+ if (event.outcome === "lost") runtime.metrics.record(entry);
5268
+ runtime.journal.record({ ...entry, at: new Date(runtime.clock.now()).toISOString() });
5269
+ options.onLog?.(entry);
5270
+ };
5164
5271
  return Object.assign(runtime, { webhooks: hub, forwarding: () => ({ ...stats }) });
5165
5272
  };
5166
5273
 
@@ -5193,6 +5300,20 @@ var validationResponse = (issue) => {
5193
5300
  return error(422, "validation_error", `Invalid \`${field}\` field.`);
5194
5301
  };
5195
5302
  var list = (value) => value === void 0 || value === null ? [] : Array.isArray(value) ? value.map(String) : [String(value)];
5303
+ var stringRecord = (value) => {
5304
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
5305
+ const headers = {};
5306
+ for (const [key, entry] of Object.entries(value)) {
5307
+ if (typeof entry === "string") headers[key] = entry;
5308
+ }
5309
+ return headers;
5310
+ };
5311
+ var tagsOf = (value) => Array.isArray(value) ? value.flatMap((item) => {
5312
+ if (typeof item !== "object" || item === null) return [];
5313
+ const name = item.name;
5314
+ const tagValue = item.value;
5315
+ return typeof name === "string" && typeof tagValue === "string" ? [{ name, value: tagValue }] : [];
5316
+ }) : [];
5196
5317
  var bareAddress = (value) => (/<([^<>]+)>\s*$/.exec(value)?.[1] ?? value).trim().toLowerCase();
5197
5318
  var attachmentBytes = (content) => {
5198
5319
  if (typeof content === "string") {
@@ -5216,12 +5337,20 @@ var ResendAPI = class {
5216
5337
  now;
5217
5338
  publicNamespace;
5218
5339
  onSent;
5340
+ onOutcome;
5341
+ /** Counts for the ambiguous-send path. Cleared with `reset`. */
5342
+ outcomeCounts = {
5343
+ accepted: 0,
5344
+ lost: 0,
5345
+ replayed: 0
5346
+ };
5219
5347
  constructor(options = {}) {
5220
5348
  const sqlite = bootSqlite(options.sqlite);
5221
5349
  const namespace = options.namespace ?? RESEND_NAMESPACE;
5222
5350
  this.now = options.now ?? (() => Date.now());
5223
5351
  this.publicNamespace = options.publicNamespace;
5224
5352
  this.onSent = options.onSent;
5353
+ this.onOutcome = options.onOutcome;
5225
5354
  this.state = new ResendState(sqlite, namespace);
5226
5355
  const handlers = defineOperations({
5227
5356
  SendEmail: (context) => this.send(context),
@@ -5256,12 +5385,30 @@ var ResendAPI = class {
5256
5385
  this.app = this.service.app;
5257
5386
  this.sqlite = this.service.sqlite;
5258
5387
  }
5259
- fetch(request) {
5260
- return this.service.fetch(request);
5388
+ async fetch(request) {
5389
+ const response = await this.service.fetch(request);
5390
+ if (response.headers.get("idempotent-replayed") === "true") {
5391
+ await this.recordOutcome("replayed", response);
5392
+ return response;
5393
+ }
5394
+ const path = new URL(request.url).pathname;
5395
+ if (request.method === "POST" && path === "/emails" && response.status === 200 && faultEffect(request, "accepted_then_network_drop") !== void 0) {
5396
+ await this.recordOutcome("accepted", response);
5397
+ await this.recordOutcome("lost", response);
5398
+ throw new DroppedConnectionError();
5399
+ }
5400
+ return response;
5261
5401
  }
5262
5402
  async reset() {
5403
+ this.outcomeCounts.accepted = 0;
5404
+ this.outcomeCounts.lost = 0;
5405
+ this.outcomeCounts.replayed = 0;
5263
5406
  await this.service.reset();
5264
5407
  }
5408
+ /** Accepted sends whose response was lost, those lost responses, and idempotent replays. */
5409
+ sendOutcomes() {
5410
+ return { ...this.outcomeCounts };
5411
+ }
5265
5412
  /** Every sent email, oldest first. */
5266
5413
  sent() {
5267
5414
  return this.state.outbox.list();
@@ -5269,6 +5416,21 @@ var ResendAPI = class {
5269
5416
  iso(offsetMs = 0) {
5270
5417
  return new Date(this.now() + offsetMs).toISOString();
5271
5418
  }
5419
+ async recordOutcome(outcome, response) {
5420
+ this.outcomeCounts[outcome] += 1;
5421
+ let emailId;
5422
+ try {
5423
+ const body = await response.clone().json();
5424
+ if (typeof body.id === "string") emailId = body.id;
5425
+ } catch {
5426
+ emailId = void 0;
5427
+ }
5428
+ this.onOutcome?.({
5429
+ outcome,
5430
+ namespace: this.publicNamespace ?? "default",
5431
+ ...emailId !== void 0 ? { emailId } : {}
5432
+ });
5433
+ }
5272
5434
  send(context) {
5273
5435
  const key = context.request.headers.get("idempotency-key");
5274
5436
  if (key !== null && (key.length < 1 || key.length > 256)) {
@@ -5322,8 +5484,8 @@ var ResendAPI = class {
5322
5484
  subject: String(body.subject),
5323
5485
  html,
5324
5486
  text,
5325
- tags: Array.isArray(body.tags) ? body.tags : [],
5326
- headers: body.headers ?? {},
5487
+ tags: tagsOf(body.tags),
5488
+ headers: stringRecord(body.headers),
5327
5489
  attachments: (Array.isArray(body.attachments) ? body.attachments : []).map((raw) => {
5328
5490
  const attachment = raw;
5329
5491
  return {
@@ -5358,6 +5520,7 @@ var ResendAPI = class {
5358
5520
  reply_to: email.replyTo.length > 0 ? email.replyTo : null,
5359
5521
  last_event: email.scheduledAt ? "scheduled" : "delivered",
5360
5522
  scheduled_at: email.scheduledAt,
5523
+ headers: email.headers,
5361
5524
  tags: email.tags
5362
5525
  }),
5363
5526
  { ids: { emailId: email.id } }
@@ -5508,4 +5671,4 @@ export {
5508
5671
  bareAddress,
5509
5672
  ResendAPI
5510
5673
  };
5511
- //# sourceMappingURL=chunk-FSFGDVP6.js.map
5674
+ //# sourceMappingURL=chunk-RUK2K5Y6.js.map