@alimzen/simulator 0.1.0 → 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/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -51,10 +51,19 @@ function simulator(options) {
|
|
|
51
51
|
to,
|
|
52
52
|
...message.cc && { cc: message.cc },
|
|
53
53
|
...message.bcc && { bcc: message.bcc },
|
|
54
|
+
...message.replyTo && { replyTo: message.replyTo },
|
|
54
55
|
subject: message.subject,
|
|
55
56
|
...message.html !== void 0 && { html: message.html },
|
|
56
57
|
...message.text !== void 0 && { text: message.text },
|
|
57
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
|
+
},
|
|
58
67
|
...message.metadata && { metadata: message.metadata },
|
|
59
68
|
...message.tags && { tags: message.tags },
|
|
60
69
|
relayoMessageId: context.messageId
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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 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.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":[]}
|
|
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":[]}
|
package/dist/index.js
CHANGED
|
@@ -26,10 +26,19 @@ function simulator(options) {
|
|
|
26
26
|
to,
|
|
27
27
|
...message.cc && { cc: message.cc },
|
|
28
28
|
...message.bcc && { bcc: message.bcc },
|
|
29
|
+
...message.replyTo && { replyTo: message.replyTo },
|
|
29
30
|
subject: message.subject,
|
|
30
31
|
...message.html !== void 0 && { html: message.html },
|
|
31
32
|
...message.text !== void 0 && { text: message.text },
|
|
32
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
|
+
},
|
|
33
42
|
...message.metadata && { metadata: message.metadata },
|
|
34
43
|
...message.tags && { tags: message.tags },
|
|
35
44
|
relayoMessageId: context.messageId
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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 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.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":[]}
|
|
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