@alimzen/simulator 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alimzen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,65 @@
1
- # Temporary Holding Version
1
+ # @alimzen/simulator
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Driver for **Relayo Sim**, the delivery simulator. Swap it in for your real provider in tests and CI, send
4
+ to scenario addresses, and the simulator answers like the real world: hard bounces, greylisting, blocklists,
5
+ full mailboxes, complaints, delays... delivered to your webhook as ordinary Relayo events. Your
6
+ `relayo.on(...)` handlers run exactly as they would in production.
7
+
8
+ ```sh
9
+ npm install -D @alimzen/core @alimzen/simulator
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```ts
15
+ import { createRelayo } from '@alimzen/core';
16
+ import { scenarioAddress, simulator } from '@alimzen/simulator';
17
+
18
+ const relayo = createRelayo({
19
+ email: process.env.CI
20
+ ? simulator({
21
+ apiKey: process.env.RELAYO_SIM_API_KEY!,
22
+ baseUrl: process.env.RELAYO_SIM_URL!,
23
+ webhook: { secret: process.env.RELAYO_SIM_WEBHOOK_SECRET! },
24
+ })
25
+ : ses({ region: 'eu-west-1' }),
26
+ });
27
+
28
+ await relayo.email.send({ ...invoiceEmail, to: { address: scenarioAddress('hard-bounce') } });
29
+ // → your relayo.on('bounced') handler gets { bounceType: 'hard', reason: 'user_unknown', ... }
30
+ ```
31
+
32
+ Webhooks from the simulator go through `relayo.handleWebhook('email', req)` like any provider's (they are
33
+ signed: `Relayo-Signature: t=...,v1=<HMAC-SHA256>`).
34
+
35
+ ## Scenarios
36
+
37
+ Pick one with the recipient address (`name[+param]@sim.relayo.dev`, or `scenarioAddress(name, param)`) or
38
+ with an `X-Sim-Scenario` header that applies to every recipient. Any other address is delivered normally.
39
+
40
+ | Scenario | What happens |
41
+ | --- | --- |
42
+ | `hard-bounce` | 550 5.1.1: the mailbox does not exist → `bounced` hard / `user_unknown` |
43
+ | `no-mx` | 550 5.1.2: the domain cannot receive mail → `bounced` hard / `domain_invalid` |
44
+ | `mailbox-full` | 452 4.2.2: deferred twice, then `bounced` soft / `mailbox_full` |
45
+ | `greylist+N` | 451 4.7.1 until attempt N (default 2), then delivered |
46
+ | `blocked` | 550 5.7.1 policy → `bounced` block / `policy_block` |
47
+ | `blocklisted` | 554 5.7.1 Spamhaus → `bounced` block / `reputation_block` |
48
+ | `dmarc` | 550 5.7.26 → `bounced` block / `auth_failure` |
49
+ | `too-large` | 552 5.3.4 → `bounced` block / `message_too_large` |
50
+ | `rate-limit` | 421 4.7.28: deferred, then delivered |
51
+ | `timeout` | No answer (SMTP); deferred and soft-bounced (API) |
52
+ | `delay+30m` | Delivered after the given time (s, m or h) |
53
+ | `complaint` | Delivered, then marked as spam |
54
+ | `unsubscribe` | Delivered, then unsubscribed |
55
+ | `engaged` | Delivered, opened, clicked |
56
+ | `suppressed` | `rejected` / `suppressed`, never attempted |
57
+
58
+ The simulator also has an SMTP endpoint (password: your API key), so apps sending through
59
+ `@alimzen/smtp`, or any other language, get the same scenarios as live SMTP answers.
60
+
61
+ Over SMTP, refusing scenarios answer during the session. Add `+async` (`hard-bounce+async@`) to have the
62
+ simulator accept the message and bounce it later, mailing a DSN to the envelope sender, as a relay would: that
63
+ is how you test reading your bounce mailbox with [`@alimzen/imap`](../imap). Complaints come back as ARF reports.
64
+
65
+ `baseUrl` is required: the driver never sends your API key to a default host.
package/dist/index.cjs ADDED
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_DOMAIN: () => DEFAULT_DOMAIN,
24
+ SIGNATURE_HEADER: () => SIGNATURE_HEADER,
25
+ scenarioAddress: () => scenarioAddress,
26
+ simulator: () => simulator,
27
+ verifySimulatorSignature: () => verifySimulatorSignature
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/simulator.ts
32
+ var import_node_crypto = require("crypto");
33
+ var import_core = require("@alimzen/core");
34
+ var SIGNATURE_HEADER = "relayo-signature";
35
+ var DEFAULT_DOMAIN = "sim.relayo.dev";
36
+ function simulator(options) {
37
+ if (!options.apiKey) throw new import_core.ValidationError("simulator() needs an apiKey");
38
+ if (!options.baseUrl) throw new import_core.ValidationError("simulator() needs the baseUrl of your Relayo Sim");
39
+ const name = options.name ?? "simulator";
40
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
41
+ const doFetch = options.fetch ?? globalThis.fetch;
42
+ const webhook = options.webhook ?? {};
43
+ return {
44
+ name,
45
+ channel: "email",
46
+ capabilities: { batch: false, attachments: true, scheduling: false, tracking: true, webhooks: true },
47
+ async send(message, context) {
48
+ const to = Array.isArray(message.to) ? message.to : [message.to];
49
+ const body = {
50
+ from: message.from,
51
+ to,
52
+ ...message.cc && { cc: message.cc },
53
+ ...message.bcc && { bcc: message.bcc },
54
+ ...message.replyTo && { replyTo: message.replyTo },
55
+ subject: message.subject,
56
+ ...message.html !== void 0 && { html: message.html },
57
+ ...message.text !== void 0 && { text: message.text },
58
+ ...message.headers && { headers: message.headers },
59
+ ...message.attachments?.length && {
60
+ attachments: message.attachments.map((a) => ({
61
+ filename: a.filename,
62
+ content: (typeof a.content === "string" ? Buffer.from(a.content, "utf8") : Buffer.from(a.content)).toString("base64"),
63
+ ...a.contentType && { contentType: a.contentType },
64
+ ...a.cid && { cid: a.cid }
65
+ }))
66
+ },
67
+ ...message.metadata && { metadata: message.metadata },
68
+ ...message.tags && { tags: message.tags },
69
+ relayoMessageId: context.messageId
70
+ };
71
+ let response;
72
+ try {
73
+ response = await doFetch(`${baseUrl}/v1/messages`, {
74
+ method: "POST",
75
+ headers: { Authorization: `Bearer ${options.apiKey}`, "Content-Type": "application/json" },
76
+ body: JSON.stringify(body),
77
+ signal: AbortSignal.timeout(options.timeoutMs ?? 3e4)
78
+ });
79
+ } catch (error) {
80
+ throw new import_core.ProviderUnavailableError(`Relayo Sim request failed: ${error.message}`, { provider: name, cause: error });
81
+ }
82
+ if (!response.ok) throw await errorFromResponse(response, name);
83
+ const { id } = await response.json();
84
+ const result = { messageId: context.messageId, provider: name, status: "queued" };
85
+ if (id) result.providerMessageId = id;
86
+ return result;
87
+ },
88
+ verifyWebhook(req) {
89
+ if (webhook.verifySignature === false) return true;
90
+ if (!webhook.secret) {
91
+ throw new import_core.ValidationError(
92
+ `Driver "${name}" has no webhook.secret; add the project's webhook secret or set webhook.verifySignature to false`,
93
+ { provider: name }
94
+ );
95
+ }
96
+ return verifySimulatorSignature(req, webhook.secret, webhook.toleranceSeconds ?? 300);
97
+ },
98
+ parseWebhook(req) {
99
+ const event = readBody(req);
100
+ if (!event || typeof event.type !== "string" || typeof event.id !== "string") {
101
+ throw new import_core.ValidationError("Unrecognized Relayo Sim webhook payload", { provider: name, raw: event });
102
+ }
103
+ return [{ ...event, timestamp: new Date(event.timestamp) }];
104
+ }
105
+ };
106
+ }
107
+ function scenarioAddress(scenario, param, domain = DEFAULT_DOMAIN) {
108
+ return `${scenario}${param !== void 0 ? `+${param}` : ""}@${domain}`;
109
+ }
110
+ function verifySimulatorSignature(req, secret, toleranceSeconds = 300) {
111
+ const header = Object.entries(req.headers).find(([k]) => k.toLowerCase() === SIGNATURE_HEADER)?.[1];
112
+ const value = Array.isArray(header) ? header[0] : header;
113
+ if (!value || req.rawBody === void 0) return false;
114
+ const parts = Object.fromEntries(value.split(",").map((p) => p.split("=")));
115
+ const timestamp = Number(parts.t);
116
+ if (!timestamp || !parts.v1) return false;
117
+ if (toleranceSeconds > 0 && Math.abs(Date.now() / 1e3 - timestamp) > toleranceSeconds) return false;
118
+ const body = typeof req.rawBody === "string" ? req.rawBody : Buffer.from(req.rawBody).toString("utf8");
119
+ const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(`${timestamp}.${body}`).digest();
120
+ const given = Buffer.from(parts.v1, "hex");
121
+ return given.length === expected.length && (0, import_node_crypto.timingSafeEqual)(given, expected);
122
+ }
123
+ async function errorFromResponse(response, provider) {
124
+ const body = await response.json().catch(() => ({}));
125
+ const detail = Array.isArray(body.message) ? body.message.join("; ") : body.message ?? response.statusText;
126
+ const message = `Relayo Sim ${response.status}: ${detail}`;
127
+ const options = { provider, raw: { status: response.status, message: detail } };
128
+ if (response.status === 401 || response.status === 403) return new import_core.AuthenticationError(message, options);
129
+ if (response.status === 429) return new import_core.RateLimitError(message, options);
130
+ if (response.status >= 500) return new import_core.ProviderUnavailableError(message, options);
131
+ return new import_core.ValidationError(message, options);
132
+ }
133
+ function readBody(req) {
134
+ const source = req.rawBody ?? req.body;
135
+ if (typeof source === "string") return JSON.parse(source);
136
+ if (source instanceof Uint8Array) return JSON.parse(Buffer.from(source).toString("utf8"));
137
+ return source;
138
+ }
139
+ // Annotate the CommonJS export names for ESM import in node:
140
+ 0 && (module.exports = {
141
+ DEFAULT_DOMAIN,
142
+ SIGNATURE_HEADER,
143
+ scenarioAddress,
144
+ simulator,
145
+ verifySimulatorSignature
146
+ });
147
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/simulator.ts"],"sourcesContent":["export { DEFAULT_DOMAIN, scenarioAddress, SIGNATURE_HEADER, simulator, verifySimulatorSignature } from './simulator.js';\nexport type { SimulatorDriverOptions, SimulatorWebhookOptions } from './simulator.js';\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport {\n AuthenticationError,\n ProviderUnavailableError,\n RateLimitError,\n ValidationError,\n type Driver,\n type EmailMessage,\n type RelayoError,\n type RelayoEvent,\n type SendResult,\n type WebhookRequest,\n} from '@alimzen/core';\n\nexport const SIGNATURE_HEADER = 'relayo-signature';\nexport const DEFAULT_DOMAIN = 'sim.relayo.dev';\n\nexport interface SimulatorWebhookOptions {\n /** The project's webhook secret (`whsec_...`). */\n secret?: string;\n /** Set to false to accept unsigned webhooks. Default true. */\n verifySignature?: boolean;\n /** Reject signatures older than this many seconds. Default 300; `0` turns the check off. */\n toleranceSeconds?: number;\n}\n\nexport interface SimulatorDriverOptions {\n /** Project API key (`sim_live_...`). */\n apiKey: string;\n /** Base URL of the Relayo Sim API, e.g. `http://localhost:3000`. */\n baseUrl: string;\n timeoutMs?: number;\n /** Provider name reported in results and errors. Defaults to `\"simulator\"`. */\n name?: string;\n webhook?: SimulatorWebhookOptions;\n fetch?: typeof fetch;\n}\n\n/**\n * Relayo Sim driver: a drop-in replacement for your real provider in tests and CI. Send to scenario\n * addresses (see `scenarioAddress`) and the simulator plays the scenario back as Relayo events through\n * your webhook: bounces, deferrals, complaints, opens, clicks...\n */\nexport function simulator(options: SimulatorDriverOptions): Driver<EmailMessage> {\n if (!options.apiKey) throw new ValidationError('simulator() needs an apiKey');\n if (!options.baseUrl) throw new ValidationError('simulator() needs the baseUrl of your Relayo Sim');\n const name = options.name ?? 'simulator';\n const baseUrl = options.baseUrl.replace(/\\/$/, '');\n const doFetch = options.fetch ?? globalThis.fetch;\n const webhook = options.webhook ?? {};\n\n return {\n name,\n channel: 'email',\n capabilities: { batch: false, attachments: true, scheduling: false, tracking: true, webhooks: true },\n\n async send(message, context) {\n const to = Array.isArray(message.to) ? message.to : [message.to];\n const body = {\n from: message.from,\n to,\n ...(message.cc && { cc: message.cc }),\n ...(message.bcc && { bcc: message.bcc }),\n ...(message.replyTo && { replyTo: message.replyTo }),\n subject: message.subject,\n ...(message.html !== undefined && { html: message.html }),\n ...(message.text !== undefined && { text: message.text }),\n ...(message.headers && { headers: message.headers }),\n ...(message.attachments?.length && {\n attachments: message.attachments.map((a) => ({\n filename: a.filename,\n content: (typeof a.content === 'string' ? Buffer.from(a.content, 'utf8') : Buffer.from(a.content)).toString('base64'),\n ...(a.contentType && { contentType: a.contentType }),\n ...(a.cid && { cid: a.cid }),\n })),\n }),\n ...(message.metadata && { metadata: message.metadata }),\n ...(message.tags && { tags: message.tags }),\n relayoMessageId: context.messageId,\n };\n\n let response: Response;\n try {\n response = await doFetch(`${baseUrl}/v1/messages`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${options.apiKey}`, 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),\n });\n } catch (error) {\n throw new ProviderUnavailableError(`Relayo Sim request failed: ${(error as Error).message}`, { provider: name, cause: error });\n }\n if (!response.ok) throw await errorFromResponse(response, name);\n\n const { id } = (await response.json()) as { id: string };\n const result: SendResult = { messageId: context.messageId, provider: name, status: 'queued' };\n if (id) result.providerMessageId = id;\n return result;\n },\n\n verifyWebhook(req) {\n if (webhook.verifySignature === false) return true;\n if (!webhook.secret) {\n throw new ValidationError(\n `Driver \"${name}\" has no webhook.secret; add the project's webhook secret or set webhook.verifySignature to false`,\n { provider: name },\n );\n }\n return verifySimulatorSignature(req, webhook.secret, webhook.toleranceSeconds ?? 300);\n },\n\n parseWebhook(req) {\n const event = readBody(req) as RelayoEvent;\n if (!event || typeof event.type !== 'string' || typeof event.id !== 'string') {\n throw new ValidationError('Unrecognized Relayo Sim webhook payload', { provider: name, raw: event });\n }\n // The simulator already sends normalized Relayo events.\n return [{ ...event, timestamp: new Date(event.timestamp) }];\n },\n };\n}\n\n/** `scenarioAddress('greylist', 3)` → `greylist+3@sim.relayo.dev`. */\nexport function scenarioAddress(scenario: string, param?: string | number, domain = DEFAULT_DOMAIN): string {\n return `${scenario}${param !== undefined ? `+${param}` : ''}@${domain}`;\n}\n\n/** Checks `Relayo-Signature: t=<unix>,v1=<hex HMAC-SHA256 of \"t.body\">`. */\nexport function verifySimulatorSignature(req: WebhookRequest, secret: string, toleranceSeconds = 300): boolean {\n const header = Object.entries(req.headers).find(([k]) => k.toLowerCase() === SIGNATURE_HEADER)?.[1];\n const value = Array.isArray(header) ? header[0] : header;\n if (!value || req.rawBody === undefined) return false;\n const parts = Object.fromEntries(value.split(',').map((p) => p.split('=') as [string, string]));\n const timestamp = Number(parts.t);\n if (!timestamp || !parts.v1) return false;\n if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;\n\n const body = typeof req.rawBody === 'string' ? req.rawBody : Buffer.from(req.rawBody).toString('utf8');\n const expected = createHmac('sha256', secret).update(`${timestamp}.${body}`).digest();\n const given = Buffer.from(parts.v1, 'hex');\n return given.length === expected.length && timingSafeEqual(given, expected);\n}\n\nasync function errorFromResponse(response: Response, provider: string): Promise<RelayoError> {\n const body = (await response.json().catch(() => ({}))) as { message?: string | string[] };\n const detail = Array.isArray(body.message) ? body.message.join('; ') : (body.message ?? response.statusText);\n const message = `Relayo Sim ${response.status}: ${detail}`;\n const options = { provider, raw: { status: response.status, message: detail } };\n if (response.status === 401 || response.status === 403) return new AuthenticationError(message, options);\n if (response.status === 429) return new RateLimitError(message, options);\n if (response.status >= 500) return new ProviderUnavailableError(message, options);\n return new ValidationError(message, options);\n}\n\nfunction readBody(req: WebhookRequest): unknown {\n const source = req.rawBody ?? req.body;\n if (typeof source === 'string') return JSON.parse(source);\n if (source instanceof Uint8Array) return JSON.parse(Buffer.from(source).toString('utf8'));\n return source;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,35 @@
1
+ import { Driver, EmailMessage, WebhookRequest } from '@alimzen/core';
2
+
3
+ declare const SIGNATURE_HEADER = "relayo-signature";
4
+ declare const DEFAULT_DOMAIN = "sim.relayo.dev";
5
+ interface SimulatorWebhookOptions {
6
+ /** The project's webhook secret (`whsec_...`). */
7
+ secret?: string;
8
+ /** Set to false to accept unsigned webhooks. Default true. */
9
+ verifySignature?: boolean;
10
+ /** Reject signatures older than this many seconds. Default 300; `0` turns the check off. */
11
+ toleranceSeconds?: number;
12
+ }
13
+ interface SimulatorDriverOptions {
14
+ /** Project API key (`sim_live_...`). */
15
+ apiKey: string;
16
+ /** Base URL of the Relayo Sim API, e.g. `http://localhost:3000`. */
17
+ baseUrl: string;
18
+ timeoutMs?: number;
19
+ /** Provider name reported in results and errors. Defaults to `"simulator"`. */
20
+ name?: string;
21
+ webhook?: SimulatorWebhookOptions;
22
+ fetch?: typeof fetch;
23
+ }
24
+ /**
25
+ * Relayo Sim driver: a drop-in replacement for your real provider in tests and CI. Send to scenario
26
+ * addresses (see `scenarioAddress`) and the simulator plays the scenario back as Relayo events through
27
+ * your webhook: bounces, deferrals, complaints, opens, clicks...
28
+ */
29
+ declare function simulator(options: SimulatorDriverOptions): Driver<EmailMessage>;
30
+ /** `scenarioAddress('greylist', 3)` → `greylist+3@sim.relayo.dev`. */
31
+ declare function scenarioAddress(scenario: string, param?: string | number, domain?: string): string;
32
+ /** Checks `Relayo-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">`. */
33
+ declare function verifySimulatorSignature(req: WebhookRequest, secret: string, toleranceSeconds?: number): boolean;
34
+
35
+ export { DEFAULT_DOMAIN, SIGNATURE_HEADER, type SimulatorDriverOptions, type SimulatorWebhookOptions, scenarioAddress, simulator, verifySimulatorSignature };
@@ -0,0 +1,35 @@
1
+ import { Driver, EmailMessage, WebhookRequest } from '@alimzen/core';
2
+
3
+ declare const SIGNATURE_HEADER = "relayo-signature";
4
+ declare const DEFAULT_DOMAIN = "sim.relayo.dev";
5
+ interface SimulatorWebhookOptions {
6
+ /** The project's webhook secret (`whsec_...`). */
7
+ secret?: string;
8
+ /** Set to false to accept unsigned webhooks. Default true. */
9
+ verifySignature?: boolean;
10
+ /** Reject signatures older than this many seconds. Default 300; `0` turns the check off. */
11
+ toleranceSeconds?: number;
12
+ }
13
+ interface SimulatorDriverOptions {
14
+ /** Project API key (`sim_live_...`). */
15
+ apiKey: string;
16
+ /** Base URL of the Relayo Sim API, e.g. `http://localhost:3000`. */
17
+ baseUrl: string;
18
+ timeoutMs?: number;
19
+ /** Provider name reported in results and errors. Defaults to `"simulator"`. */
20
+ name?: string;
21
+ webhook?: SimulatorWebhookOptions;
22
+ fetch?: typeof fetch;
23
+ }
24
+ /**
25
+ * Relayo Sim driver: a drop-in replacement for your real provider in tests and CI. Send to scenario
26
+ * addresses (see `scenarioAddress`) and the simulator plays the scenario back as Relayo events through
27
+ * your webhook: bounces, deferrals, complaints, opens, clicks...
28
+ */
29
+ declare function simulator(options: SimulatorDriverOptions): Driver<EmailMessage>;
30
+ /** `scenarioAddress('greylist', 3)` → `greylist+3@sim.relayo.dev`. */
31
+ declare function scenarioAddress(scenario: string, param?: string | number, domain?: string): string;
32
+ /** Checks `Relayo-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">`. */
33
+ declare function verifySimulatorSignature(req: WebhookRequest, secret: string, toleranceSeconds?: number): boolean;
34
+
35
+ export { DEFAULT_DOMAIN, SIGNATURE_HEADER, type SimulatorDriverOptions, type SimulatorWebhookOptions, scenarioAddress, simulator, verifySimulatorSignature };
package/dist/index.js ADDED
@@ -0,0 +1,121 @@
1
+ // src/simulator.ts
2
+ import { createHmac, timingSafeEqual } from "crypto";
3
+ import {
4
+ AuthenticationError,
5
+ ProviderUnavailableError,
6
+ RateLimitError,
7
+ ValidationError
8
+ } from "@alimzen/core";
9
+ var SIGNATURE_HEADER = "relayo-signature";
10
+ var DEFAULT_DOMAIN = "sim.relayo.dev";
11
+ function simulator(options) {
12
+ if (!options.apiKey) throw new ValidationError("simulator() needs an apiKey");
13
+ if (!options.baseUrl) throw new ValidationError("simulator() needs the baseUrl of your Relayo Sim");
14
+ const name = options.name ?? "simulator";
15
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
16
+ const doFetch = options.fetch ?? globalThis.fetch;
17
+ const webhook = options.webhook ?? {};
18
+ return {
19
+ name,
20
+ channel: "email",
21
+ capabilities: { batch: false, attachments: true, scheduling: false, tracking: true, webhooks: true },
22
+ async send(message, context) {
23
+ const to = Array.isArray(message.to) ? message.to : [message.to];
24
+ const body = {
25
+ from: message.from,
26
+ to,
27
+ ...message.cc && { cc: message.cc },
28
+ ...message.bcc && { bcc: message.bcc },
29
+ ...message.replyTo && { replyTo: message.replyTo },
30
+ subject: message.subject,
31
+ ...message.html !== void 0 && { html: message.html },
32
+ ...message.text !== void 0 && { text: message.text },
33
+ ...message.headers && { headers: message.headers },
34
+ ...message.attachments?.length && {
35
+ attachments: message.attachments.map((a) => ({
36
+ filename: a.filename,
37
+ content: (typeof a.content === "string" ? Buffer.from(a.content, "utf8") : Buffer.from(a.content)).toString("base64"),
38
+ ...a.contentType && { contentType: a.contentType },
39
+ ...a.cid && { cid: a.cid }
40
+ }))
41
+ },
42
+ ...message.metadata && { metadata: message.metadata },
43
+ ...message.tags && { tags: message.tags },
44
+ relayoMessageId: context.messageId
45
+ };
46
+ let response;
47
+ try {
48
+ response = await doFetch(`${baseUrl}/v1/messages`, {
49
+ method: "POST",
50
+ headers: { Authorization: `Bearer ${options.apiKey}`, "Content-Type": "application/json" },
51
+ body: JSON.stringify(body),
52
+ signal: AbortSignal.timeout(options.timeoutMs ?? 3e4)
53
+ });
54
+ } catch (error) {
55
+ throw new ProviderUnavailableError(`Relayo Sim request failed: ${error.message}`, { provider: name, cause: error });
56
+ }
57
+ if (!response.ok) throw await errorFromResponse(response, name);
58
+ const { id } = await response.json();
59
+ const result = { messageId: context.messageId, provider: name, status: "queued" };
60
+ if (id) result.providerMessageId = id;
61
+ return result;
62
+ },
63
+ verifyWebhook(req) {
64
+ if (webhook.verifySignature === false) return true;
65
+ if (!webhook.secret) {
66
+ throw new ValidationError(
67
+ `Driver "${name}" has no webhook.secret; add the project's webhook secret or set webhook.verifySignature to false`,
68
+ { provider: name }
69
+ );
70
+ }
71
+ return verifySimulatorSignature(req, webhook.secret, webhook.toleranceSeconds ?? 300);
72
+ },
73
+ parseWebhook(req) {
74
+ const event = readBody(req);
75
+ if (!event || typeof event.type !== "string" || typeof event.id !== "string") {
76
+ throw new ValidationError("Unrecognized Relayo Sim webhook payload", { provider: name, raw: event });
77
+ }
78
+ return [{ ...event, timestamp: new Date(event.timestamp) }];
79
+ }
80
+ };
81
+ }
82
+ function scenarioAddress(scenario, param, domain = DEFAULT_DOMAIN) {
83
+ return `${scenario}${param !== void 0 ? `+${param}` : ""}@${domain}`;
84
+ }
85
+ function verifySimulatorSignature(req, secret, toleranceSeconds = 300) {
86
+ const header = Object.entries(req.headers).find(([k]) => k.toLowerCase() === SIGNATURE_HEADER)?.[1];
87
+ const value = Array.isArray(header) ? header[0] : header;
88
+ if (!value || req.rawBody === void 0) return false;
89
+ const parts = Object.fromEntries(value.split(",").map((p) => p.split("=")));
90
+ const timestamp = Number(parts.t);
91
+ if (!timestamp || !parts.v1) return false;
92
+ if (toleranceSeconds > 0 && Math.abs(Date.now() / 1e3 - timestamp) > toleranceSeconds) return false;
93
+ const body = typeof req.rawBody === "string" ? req.rawBody : Buffer.from(req.rawBody).toString("utf8");
94
+ const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest();
95
+ const given = Buffer.from(parts.v1, "hex");
96
+ return given.length === expected.length && timingSafeEqual(given, expected);
97
+ }
98
+ async function errorFromResponse(response, provider) {
99
+ const body = await response.json().catch(() => ({}));
100
+ const detail = Array.isArray(body.message) ? body.message.join("; ") : body.message ?? response.statusText;
101
+ const message = `Relayo Sim ${response.status}: ${detail}`;
102
+ const options = { provider, raw: { status: response.status, message: detail } };
103
+ if (response.status === 401 || response.status === 403) return new AuthenticationError(message, options);
104
+ if (response.status === 429) return new RateLimitError(message, options);
105
+ if (response.status >= 500) return new ProviderUnavailableError(message, options);
106
+ return new ValidationError(message, options);
107
+ }
108
+ function readBody(req) {
109
+ const source = req.rawBody ?? req.body;
110
+ if (typeof source === "string") return JSON.parse(source);
111
+ if (source instanceof Uint8Array) return JSON.parse(Buffer.from(source).toString("utf8"));
112
+ return source;
113
+ }
114
+ export {
115
+ DEFAULT_DOMAIN,
116
+ SIGNATURE_HEADER,
117
+ scenarioAddress,
118
+ simulator,
119
+ verifySimulatorSignature
120
+ };
121
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/simulator.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\nimport {\n AuthenticationError,\n ProviderUnavailableError,\n RateLimitError,\n ValidationError,\n type Driver,\n type EmailMessage,\n type RelayoError,\n type RelayoEvent,\n type SendResult,\n type WebhookRequest,\n} from '@alimzen/core';\n\nexport const SIGNATURE_HEADER = 'relayo-signature';\nexport const DEFAULT_DOMAIN = 'sim.relayo.dev';\n\nexport interface SimulatorWebhookOptions {\n /** The project's webhook secret (`whsec_...`). */\n secret?: string;\n /** Set to false to accept unsigned webhooks. Default true. */\n verifySignature?: boolean;\n /** Reject signatures older than this many seconds. Default 300; `0` turns the check off. */\n toleranceSeconds?: number;\n}\n\nexport interface SimulatorDriverOptions {\n /** Project API key (`sim_live_...`). */\n apiKey: string;\n /** Base URL of the Relayo Sim API, e.g. `http://localhost:3000`. */\n baseUrl: string;\n timeoutMs?: number;\n /** Provider name reported in results and errors. Defaults to `\"simulator\"`. */\n name?: string;\n webhook?: SimulatorWebhookOptions;\n fetch?: typeof fetch;\n}\n\n/**\n * Relayo Sim driver: a drop-in replacement for your real provider in tests and CI. Send to scenario\n * addresses (see `scenarioAddress`) and the simulator plays the scenario back as Relayo events through\n * your webhook: bounces, deferrals, complaints, opens, clicks...\n */\nexport function simulator(options: SimulatorDriverOptions): Driver<EmailMessage> {\n if (!options.apiKey) throw new ValidationError('simulator() needs an apiKey');\n if (!options.baseUrl) throw new ValidationError('simulator() needs the baseUrl of your Relayo Sim');\n const name = options.name ?? 'simulator';\n const baseUrl = options.baseUrl.replace(/\\/$/, '');\n const doFetch = options.fetch ?? globalThis.fetch;\n const webhook = options.webhook ?? {};\n\n return {\n name,\n channel: 'email',\n capabilities: { batch: false, attachments: true, scheduling: false, tracking: true, webhooks: true },\n\n async send(message, context) {\n const to = Array.isArray(message.to) ? message.to : [message.to];\n const body = {\n from: message.from,\n to,\n ...(message.cc && { cc: message.cc }),\n ...(message.bcc && { bcc: message.bcc }),\n ...(message.replyTo && { replyTo: message.replyTo }),\n subject: message.subject,\n ...(message.html !== undefined && { html: message.html }),\n ...(message.text !== undefined && { text: message.text }),\n ...(message.headers && { headers: message.headers }),\n ...(message.attachments?.length && {\n attachments: message.attachments.map((a) => ({\n filename: a.filename,\n content: (typeof a.content === 'string' ? Buffer.from(a.content, 'utf8') : Buffer.from(a.content)).toString('base64'),\n ...(a.contentType && { contentType: a.contentType }),\n ...(a.cid && { cid: a.cid }),\n })),\n }),\n ...(message.metadata && { metadata: message.metadata }),\n ...(message.tags && { tags: message.tags }),\n relayoMessageId: context.messageId,\n };\n\n let response: Response;\n try {\n response = await doFetch(`${baseUrl}/v1/messages`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${options.apiKey}`, 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),\n });\n } catch (error) {\n throw new ProviderUnavailableError(`Relayo Sim request failed: ${(error as Error).message}`, { provider: name, cause: error });\n }\n if (!response.ok) throw await errorFromResponse(response, name);\n\n const { id } = (await response.json()) as { id: string };\n const result: SendResult = { messageId: context.messageId, provider: name, status: 'queued' };\n if (id) result.providerMessageId = id;\n return result;\n },\n\n verifyWebhook(req) {\n if (webhook.verifySignature === false) return true;\n if (!webhook.secret) {\n throw new ValidationError(\n `Driver \"${name}\" has no webhook.secret; add the project's webhook secret or set webhook.verifySignature to false`,\n { provider: name },\n );\n }\n return verifySimulatorSignature(req, webhook.secret, webhook.toleranceSeconds ?? 300);\n },\n\n parseWebhook(req) {\n const event = readBody(req) as RelayoEvent;\n if (!event || typeof event.type !== 'string' || typeof event.id !== 'string') {\n throw new ValidationError('Unrecognized Relayo Sim webhook payload', { provider: name, raw: event });\n }\n // The simulator already sends normalized Relayo events.\n return [{ ...event, timestamp: new Date(event.timestamp) }];\n },\n };\n}\n\n/** `scenarioAddress('greylist', 3)` → `greylist+3@sim.relayo.dev`. */\nexport function scenarioAddress(scenario: string, param?: string | number, domain = DEFAULT_DOMAIN): string {\n return `${scenario}${param !== undefined ? `+${param}` : ''}@${domain}`;\n}\n\n/** Checks `Relayo-Signature: t=<unix>,v1=<hex HMAC-SHA256 of \"t.body\">`. */\nexport function verifySimulatorSignature(req: WebhookRequest, secret: string, toleranceSeconds = 300): boolean {\n const header = Object.entries(req.headers).find(([k]) => k.toLowerCase() === SIGNATURE_HEADER)?.[1];\n const value = Array.isArray(header) ? header[0] : header;\n if (!value || req.rawBody === undefined) return false;\n const parts = Object.fromEntries(value.split(',').map((p) => p.split('=') as [string, string]));\n const timestamp = Number(parts.t);\n if (!timestamp || !parts.v1) return false;\n if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;\n\n const body = typeof req.rawBody === 'string' ? req.rawBody : Buffer.from(req.rawBody).toString('utf8');\n const expected = createHmac('sha256', secret).update(`${timestamp}.${body}`).digest();\n const given = Buffer.from(parts.v1, 'hex');\n return given.length === expected.length && timingSafeEqual(given, expected);\n}\n\nasync function errorFromResponse(response: Response, provider: string): Promise<RelayoError> {\n const body = (await response.json().catch(() => ({}))) as { message?: string | string[] };\n const detail = Array.isArray(body.message) ? body.message.join('; ') : (body.message ?? response.statusText);\n const message = `Relayo Sim ${response.status}: ${detail}`;\n const options = { provider, raw: { status: response.status, message: detail } };\n if (response.status === 401 || response.status === 403) return new AuthenticationError(message, options);\n if (response.status === 429) return new RateLimitError(message, options);\n if (response.status >= 500) return new ProviderUnavailableError(message, options);\n return new ValidationError(message, options);\n}\n\nfunction readBody(req: WebhookRequest): unknown {\n const source = req.rawBody ?? req.body;\n if (typeof source === 'string') return JSON.parse(source);\n if (source instanceof Uint8Array) return JSON.parse(Buffer.from(source).toString('utf8'));\n return source;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@alimzen/simulator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Relayo Sim driver: send to the delivery simulator and get realistic bounces, deferrals, complaints and more as Relayo events.",
5
+ "license": "MIT",
6
+ "author": "Alimzen",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Alimzen/relayo.git",
10
+ "directory": "packages/simulator"
11
+ },
12
+ "homepage": "https://github.com/Alimzen/relayo/tree/main/packages/simulator#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/Alimzen/relayo/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "main": "./dist/index.cjs",
22
+ "module": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "import": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "require": {
31
+ "types": "./dist/index.d.cts",
32
+ "default": "./dist/index.cjs"
33
+ }
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "keywords": [
41
+ "relayo",
42
+ "simulator",
43
+ "testing",
44
+ "email",
45
+ "bounces",
46
+ "ci"
47
+ ],
48
+ "peerDependencies": {
49
+ "@alimzen/core": ">=0.1.0 <1.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@alimzen/core": "^0.2.1"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "typecheck": "tsc --noEmit"
62
+ }
6
63
  }