@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 +15 -4
- package/README.md +9 -4
- package/dist/{chunk-OIK3EYGO.js → chunk-FVK4CBNZ.js} +3 -2
- package/dist/chunk-FVK4CBNZ.js.map +7 -0
- package/dist/{chunk-FSFGDVP6.js → chunk-RUK2K5Y6.js} +179 -16
- package/dist/chunk-RUK2K5Y6.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +16 -1
- package/dist/index.js +1 -1
- package/dist/server.d.ts +26 -9
- package/dist/server.js +2 -2
- package/package.json +2 -1
- package/dist/chunk-FSFGDVP6.js.map +0 -7
- package/dist/chunk-OIK3EYGO.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-resend
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.6.1 (2026-09-30)
|
|
4
4
|
|
|
5
5
|
### Fixes and improvements
|
|
6
6
|
|
|
7
|
-
- keep
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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-
|
|
5674
|
+
//# sourceMappingURL=chunk-RUK2K5Y6.js.map
|