@c9up/rover 0.1.14 → 0.1.16

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.
Files changed (79) hide show
  1. package/dist/Mail.d.ts +8 -4
  2. package/dist/Mail.d.ts.map +1 -1
  3. package/dist/Mail.js +7 -6
  4. package/dist/Mail.js.map +1 -1
  5. package/dist/MessageBuilder.js +1 -1
  6. package/dist/MessageBuilder.js.map +1 -1
  7. package/dist/RoverProvider.d.ts +1 -0
  8. package/dist/RoverProvider.d.ts.map +1 -1
  9. package/dist/RoverProvider.js +9 -3
  10. package/dist/RoverProvider.js.map +1 -1
  11. package/dist/augmentations.d.ts +29 -0
  12. package/dist/augmentations.d.ts.map +1 -0
  13. package/dist/augmentations.js +18 -0
  14. package/dist/augmentations.js.map +1 -0
  15. package/dist/emitSafely.d.ts +18 -0
  16. package/dist/emitSafely.d.ts.map +1 -0
  17. package/dist/emitSafely.js +22 -0
  18. package/dist/emitSafely.js.map +1 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/queue/MemoryMailMessenger.d.ts.map +1 -1
  24. package/dist/queue/MemoryMailMessenger.js +14 -15
  25. package/dist/queue/MemoryMailMessenger.js.map +1 -1
  26. package/dist/retry.d.ts +10 -1
  27. package/dist/retry.d.ts.map +1 -1
  28. package/dist/retry.js +9 -0
  29. package/dist/retry.js.map +1 -1
  30. package/dist/testing/FakeMail.d.ts +4 -2
  31. package/dist/testing/FakeMail.d.ts.map +1 -1
  32. package/dist/testing/FakeMail.js +4 -19
  33. package/dist/testing/FakeMail.js.map +1 -1
  34. package/dist/transports/BrevoTransport.js +6 -3
  35. package/dist/transports/BrevoTransport.js.map +1 -1
  36. package/dist/transports/MailgunTransport.d.ts.map +1 -1
  37. package/dist/transports/MailgunTransport.js +59 -45
  38. package/dist/transports/MailgunTransport.js.map +1 -1
  39. package/dist/transports/SendGridTransport.d.ts.map +1 -1
  40. package/dist/transports/SendGridTransport.js +80 -82
  41. package/dist/transports/SendGridTransport.js.map +1 -1
  42. package/dist/transports/SparkPostTransport.js +6 -3
  43. package/dist/transports/SparkPostTransport.js.map +1 -1
  44. package/dist/webhooks/context.d.ts +9 -1
  45. package/dist/webhooks/context.d.ts.map +1 -1
  46. package/dist/webhooks/mailgun.d.ts.map +1 -1
  47. package/dist/webhooks/mailgun.js +2 -1
  48. package/dist/webhooks/mailgun.js.map +1 -1
  49. package/dist/webhooks/resend.d.ts.map +1 -1
  50. package/dist/webhooks/resend.js +2 -1
  51. package/dist/webhooks/resend.js.map +1 -1
  52. package/dist/webhooks/sendgrid.d.ts.map +1 -1
  53. package/dist/webhooks/sendgrid.js +2 -1
  54. package/dist/webhooks/sendgrid.js.map +1 -1
  55. package/index.darwin-arm64.node +0 -0
  56. package/index.darwin-x64.node +0 -0
  57. package/index.linux-arm64-gnu.node +0 -0
  58. package/index.linux-x64-gnu.node +0 -0
  59. package/index.win32-x64-msvc.node +0 -0
  60. package/package.json +8 -7
  61. package/scripts/build-napi-types.mjs +4 -3
  62. package/scripts/generate-napi-types.mjs +9 -6
  63. package/src/Mail.ts +21 -11
  64. package/src/MessageBuilder.ts +1 -1
  65. package/src/RoverProvider.ts +9 -3
  66. package/src/augmentations.ts +33 -0
  67. package/src/emitSafely.ts +31 -0
  68. package/src/index.ts +2 -0
  69. package/src/queue/MemoryMailMessenger.ts +15 -14
  70. package/src/retry.ts +11 -2
  71. package/src/testing/FakeMail.ts +12 -3
  72. package/src/transports/BrevoTransport.ts +6 -3
  73. package/src/transports/MailgunTransport.ts +64 -56
  74. package/src/transports/SendGridTransport.ts +86 -117
  75. package/src/transports/SparkPostTransport.ts +6 -3
  76. package/src/webhooks/context.ts +9 -1
  77. package/src/webhooks/mailgun.ts +2 -1
  78. package/src/webhooks/resend.ts +2 -1
  79. package/src/webhooks/sendgrid.ts +2 -1
@@ -1,6 +1,3 @@
1
- import formData from "form-data";
2
- // mailgun.js is UMD-bundled; the class lives on `.default` under NodeNext.
3
- import MailgunModule from "mailgun.js";
4
1
  import {
5
2
  type MailMessage,
6
3
  type MailSendOutcome,
@@ -9,6 +6,7 @@ import {
9
6
  } from "../Mail.js";
10
7
  import { attachmentsFor, headerValue } from "../MessageBuilder.js";
11
8
  import { RoverError } from "../RoverError.js";
9
+ import { fetchWithTimeout, wrapFetchNetworkError } from "./fetchError.js";
12
10
 
13
11
  const stripCrlf = (v: string): string => v.replace(/[\r\n]/g, "");
14
12
  const normalizeConfig = (v: string): string => stripCrlf(v).trim();
@@ -23,19 +21,10 @@ const redactSecrets = (s: string): string =>
23
21
  .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, "Bearer [REDACTED]")
24
22
  .replace(/Basic\s+[A-Za-z0-9+/=]+/g, "Basic [REDACTED]");
25
23
 
26
- /** Minimal mailgun.js client surface — matches the subset we need. */
27
- interface MailgunClientLike {
28
- messages: {
29
- create(
30
- domain: string,
31
- data: Record<string, unknown>,
32
- ): Promise<{ id?: string; message?: string; status?: number }>;
33
- };
34
- }
35
-
36
24
  export class MailgunTransport implements MailTransport {
37
- #client: MailgunClientLike;
25
+ #apiKey: string;
38
26
  #domain: string;
27
+ #baseUrl: string;
39
28
 
40
29
  constructor(config: Record<string, unknown>) {
41
30
  const apiKey =
@@ -78,24 +67,11 @@ export class MailgunTransport implements MailTransport {
78
67
  ? "https://api.eu.mailgun.net"
79
68
  : "https://api.mailgun.net";
80
69
 
81
- // Dependency injection for tests: `_client` wins over real SDK. Guarded
82
- // against non-object and non-shape inputs so a typo'd config can't
83
- // silently bypass the real client.
84
- const injected = config._client;
85
- if (
86
- injected &&
87
- typeof injected === "object" &&
88
- "messages" in (injected as object) &&
89
- typeof (injected as MailgunClientLike).messages?.create === "function"
90
- ) {
91
- this.#client = injected as MailgunClientLike;
92
- } else {
93
- // mailgun.js ships a UMD-style default export; the class constructor
94
- // lives on `.default` in the typings (`static get default`).
95
- const MailgunCtor = MailgunModule.default;
96
- const mailgun = new MailgunCtor(formData);
97
- this.#client = mailgun.client({ username: "api", key: apiKey, url });
98
- }
70
+ this.#apiKey = apiKey;
71
+ this.#baseUrl =
72
+ typeof config.baseUrl === "string" && config.baseUrl.length > 0
73
+ ? normalizeConfig(config.baseUrl).replace(/\/+$/, "")
74
+ : url;
99
75
  }
100
76
 
101
77
  async send(message: MailMessage): Promise<MailSendOutcome> {
@@ -134,38 +110,70 @@ export class MailgunTransport implements MailTransport {
134
110
  ? v.map(stripCrlf).join(", ")
135
111
  : stripCrlf(v);
136
112
  }
137
- if (attachmentsFor(message).length > 0) {
138
- data.attachment = attachmentsFor(message).map((att) => {
139
- const entry: { filename: string; data: Buffer; contentType?: string } =
140
- {
141
- filename: stripCrlf(att.filename),
142
- data: Buffer.from(att.content as Buffer | string),
143
- };
144
- if (att.contentType) {
145
- entry.contentType = stripCrlf(att.contentType);
146
- }
147
- return entry;
148
- });
113
+ // Mailgun takes `multipart/form-data`; Node builds it, and `fetch` sets
114
+ // the boundary. Scalars first, then one file part per attachment.
115
+ const form = new FormData();
116
+ for (const [key, value] of Object.entries(data)) {
117
+ form.append(key, Array.isArray(value) ? value.join(", ") : String(value));
118
+ }
119
+ for (const att of attachmentsFor(message)) {
120
+ const bytes = Buffer.from(att.content as Buffer | string);
121
+ const blob = att.contentType
122
+ ? new Blob([bytes], { type: stripCrlf(att.contentType) })
123
+ : new Blob([bytes]);
124
+ form.append("attachment", blob, stripCrlf(att.filename));
149
125
  }
150
126
 
127
+ let res: Response;
151
128
  try {
152
- const res = await this.#client.messages.create(this.#domain, data);
153
- if (res.id) return { providerId: res.id };
154
- return undefined;
129
+ res = await fetchWithTimeout(
130
+ "Mailgun",
131
+ `${this.#baseUrl}/v3/${encodeURIComponent(this.#domain)}/messages`,
132
+ {
133
+ method: "POST",
134
+ headers: {
135
+ // HTTP Basic, the username is literally `api`.
136
+ Authorization: `Basic ${Buffer.from(`api:${this.#apiKey}`).toString("base64")}`,
137
+ Accept: "application/json",
138
+ },
139
+ body: form,
140
+ },
141
+ );
155
142
  } catch (err) {
156
- throw wrapMailgunError(err);
143
+ throw wrapFetchNetworkError("mailgun", err);
144
+ }
145
+
146
+ const raw = await res.text();
147
+ if (!res.ok) {
148
+ throw wrapMailgunError({
149
+ status: res.status,
150
+ message: raw,
151
+ headers: Object.fromEntries(res.headers.entries()),
152
+ });
153
+ }
154
+ // A 200 carries `{ id, message }`; anything unparseable is still a
155
+ // success on the wire, so it is not turned into an error.
156
+ try {
157
+ const parsed: unknown = JSON.parse(raw);
158
+ const id =
159
+ typeof parsed === "object" && parsed !== null
160
+ ? Reflect.get(parsed, "id")
161
+ : undefined;
162
+ return typeof id === "string" ? { providerId: id } : undefined;
163
+ } catch {
164
+ return undefined;
157
165
  }
158
166
  }
159
167
  }
160
168
 
161
169
  /**
162
- * mailgun.js's own error shape is `{ status, details | message, ... }`.
163
- * Map it to our uniform `E_MAIL_PROVIDER_ERROR` so retry + observability
164
- * consumers don't need to branch on provider.
170
+ * Map an upstream refusal onto the uniform `E_MAIL_PROVIDER_ERROR` every
171
+ * transport raises, so retry and observability never branch on the provider.
165
172
  *
166
- * Bare `Error` (no `status`) — typical for network/ECONNRESET failures from
167
- * mailgun.js — surface the original errno (`code`) in context so the retry
168
- * predicate can still classify it as transient.
173
+ * The shape is what the HTTP response gives: a status, the body as the
174
+ * message, and the response headers — `Retry-After` among them, which the
175
+ * backoff honours. A network failure never reaches here; `wrapFetchNetworkError`
176
+ * carries the errno so the retry predicate can still see it.
169
177
  */
170
178
  function wrapMailgunError(err: unknown): RoverError {
171
179
  if (err instanceof RoverError) return err;
@@ -176,8 +184,8 @@ function wrapMailgunError(err: unknown): RoverError {
176
184
  code?: string;
177
185
  headers?: Record<string, string | string[]>;
178
186
  };
179
- // Coerce string-typed status ("401") into number — mailgun.js is
180
- // inconsistent across versions.
187
+ // Coerce a string-typed status into a number: the field is built here, but
188
+ // a caller injecting a fake response may still hand one over as text.
181
189
  const statusNum = Number(anyErr.status);
182
190
  const status = Number.isFinite(statusNum) ? statusNum : 0;
183
191
  const providerMessage = redactSecrets(
@@ -1,7 +1,3 @@
1
- import sgMail, {
2
- type MailDataRequired,
3
- type MailService,
4
- } from "@sendgrid/mail";
5
1
  import {
6
2
  type MailMessage,
7
3
  type MailSendOutcome,
@@ -10,6 +6,7 @@ import {
10
6
  } from "../Mail.js";
11
7
  import { attachmentsFor, headerValue } from "../MessageBuilder.js";
12
8
  import { RoverError } from "../RoverError.js";
9
+ import { fetchWithTimeout, wrapFetchNetworkError } from "./fetchError.js";
13
10
 
14
11
  const stripCrlf = (v: string): string => v.replace(/[\r\n]/g, "");
15
12
  const normalizeConfig = (v: string): string => stripCrlf(v).trim();
@@ -24,26 +21,9 @@ const redactSecrets = (s: string): string =>
24
21
  .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, "Bearer [REDACTED]")
25
22
  .replace(/Basic\s+[A-Za-z0-9+/=]+/g, "Basic [REDACTED]");
26
23
 
27
- /**
28
- * Minimal slice of the SendGrid client we depend on. A per-transport client
29
- * instance is created in the constructor so concurrent transports with
30
- * different API keys cannot race each other — the original module-level
31
- * `sgMail.setApiKey()` would have been a multi-tenant foot-gun.
32
- */
33
- interface SendGridClientLike {
34
- setApiKey(apiKey: string): void;
35
- send(
36
- data: MailDataRequired,
37
- ): Promise<
38
- [
39
- { statusCode: number; headers: Record<string, string | string[]> },
40
- unknown,
41
- ]
42
- >;
43
- }
44
-
45
24
  export class SendGridTransport implements MailTransport {
46
- #client: SendGridClientLike;
25
+ #apiKey: string;
26
+ #baseUrl: string;
47
27
 
48
28
  constructor(config: Record<string, unknown>) {
49
29
  const apiKey =
@@ -56,86 +36,96 @@ export class SendGridTransport implements MailTransport {
56
36
  );
57
37
  }
58
38
 
59
- // Dependency injection for tests — stronger guard than the old version
60
- // (require both `send` AND `setApiKey` to pass through).
61
- const injected = config._client;
62
- if (
63
- injected &&
64
- typeof injected === "object" &&
65
- typeof (injected as SendGridClientLike).send === "function" &&
66
- typeof (injected as SendGridClientLike).setApiKey === "function"
67
- ) {
68
- this.#client = injected as SendGridClientLike;
69
- } else {
70
- // Per-instance MailService (not the shared `sgMail` singleton) so
71
- // `setApiKey` can't race across multiple transports.
72
- this.#client = new (resolveMailServiceCtor(sgMail))();
73
- }
74
- this.#client.setApiKey(apiKey);
39
+ this.#apiKey = apiKey;
40
+ this.#baseUrl =
41
+ typeof config.baseUrl === "string" && config.baseUrl.length > 0
42
+ ? normalizeConfig(config.baseUrl).replace(/\/+$/, "")
43
+ : "https://api.sendgrid.com";
75
44
  }
76
45
 
77
46
  async send(message: MailMessage): Promise<MailSendOutcome> {
78
47
  assertHasRecipients(message);
79
- const content = buildSendGridContent(message);
80
48
 
81
- // CRLF stripping at the wire boundary (defence-in-depth, matches the
82
- // Dev Notes anti-pattern: never trust the SDK to handle it).
83
- const data: MailDataRequired = {
84
- from: stripCrlf(message.from),
85
- to: message.to.map(stripCrlf),
49
+ // CRLF is stripped at the wire boundary regardless of what the provider
50
+ // promises — a header injected through a recipient or a subject is the
51
+ // one thing a transport must never pass on.
52
+ const address = (value: string): { email: string } => ({
53
+ email: stripCrlf(value),
54
+ });
55
+ const personalization: Record<string, unknown> = {
56
+ // `to` may be empty when a message is bcc-only, which SendGrid allows.
57
+ to: message.to.map(address),
58
+ };
59
+ if (message.cc.length) personalization.cc = message.cc.map(address);
60
+ if (message.bcc.length) personalization.bcc = message.bcc.map(address);
61
+
62
+ const body: Record<string, unknown> = {
63
+ // The v3 API groups recipients under `personalizations`; the SDK's
64
+ // flat shape was its own, and this is what actually goes on the wire.
65
+ personalizations: [personalization],
66
+ from: address(message.from),
86
67
  subject: stripCrlf(message.subject),
87
- content,
88
- ...(message.cc.length ? { cc: message.cc.map(stripCrlf) } : {}),
89
- ...(message.bcc.length ? { bcc: message.bcc.map(stripCrlf) } : {}),
90
- ...(message.replyTo ? { replyTo: stripCrlf(message.replyTo) } : {}),
91
- ...(Object.keys(message.headers).length
92
- ? {
93
- headers: Object.fromEntries(
94
- Object.entries(message.headers).map(([k, raw]) => {
95
- const v = headerValue(raw);
96
- return [
97
- stripCrlf(k),
98
- Array.isArray(v) ? v.map(stripCrlf).join(", ") : stripCrlf(v),
99
- ];
100
- }),
101
- ),
102
- }
103
- : {}),
104
- ...(attachmentsFor(message).length
105
- ? {
106
- attachments: attachmentsFor(message).map((att) => {
107
- const entry: {
108
- filename: string;
109
- content: string;
110
- type?: string;
111
- disposition: "attachment";
112
- } = {
113
- filename: stripCrlf(att.filename),
114
- content: Buffer.from(att.content as Buffer | string).toString(
115
- "base64",
116
- ),
117
- disposition: "attachment" as const,
118
- };
119
- if (att.contentType) entry.type = stripCrlf(att.contentType);
120
- return entry;
121
- }),
122
- }
123
- : {}),
68
+ content: buildSendGridContent(message),
124
69
  };
70
+ if (message.replyTo) body.reply_to = address(message.replyTo);
71
+ if (Object.keys(message.headers).length) {
72
+ body.headers = Object.fromEntries(
73
+ Object.entries(message.headers).map(([k, raw]) => {
74
+ const v = headerValue(raw);
75
+ return [
76
+ stripCrlf(k),
77
+ Array.isArray(v) ? v.map(stripCrlf).join(", ") : stripCrlf(v),
78
+ ];
79
+ }),
80
+ );
81
+ }
82
+ const attachments = attachmentsFor(message);
83
+ if (attachments.length > 0) {
84
+ body.attachments = attachments.map((att) => {
85
+ const entry: Record<string, string> = {
86
+ filename: stripCrlf(att.filename),
87
+ content: Buffer.from(att.content as Buffer | string).toString(
88
+ "base64",
89
+ ),
90
+ disposition: "attachment",
91
+ };
92
+ if (att.contentType) entry.type = stripCrlf(att.contentType);
93
+ return entry;
94
+ });
95
+ }
125
96
 
97
+ let res: Response;
126
98
  try {
127
- const result = await this.#client.send(data);
128
- // Guard against non-standard SDK responses: `[]`, `[undefined]`, etc.
129
- const response = Array.isArray(result) ? result[0] : undefined;
130
- const msgId = response?.headers?.["x-message-id"];
131
- const idStr = Array.isArray(msgId) ? msgId[0] : msgId;
132
- if (idStr && typeof idStr === "string" && idStr.length > 0) {
133
- return { providerId: idStr };
134
- }
135
- return undefined;
99
+ res = await fetchWithTimeout(
100
+ "SendGrid",
101
+ `${this.#baseUrl}/v3/mail/send`,
102
+ {
103
+ method: "POST",
104
+ headers: {
105
+ Authorization: `Bearer ${this.#apiKey}`,
106
+ "Content-Type": "application/json",
107
+ },
108
+ body: JSON.stringify(body),
109
+ },
110
+ );
136
111
  } catch (err) {
137
- throw wrapSendGridError(err);
112
+ throw wrapFetchNetworkError("sendgrid", err);
138
113
  }
114
+
115
+ if (!res.ok) {
116
+ // Shaped as the mapper already reads it, so the mapping stays one
117
+ // piece of logic rather than two that can drift.
118
+ throw wrapSendGridError({
119
+ response: {
120
+ statusCode: res.status,
121
+ body: await res.text(),
122
+ headers: Object.fromEntries(res.headers.entries()),
123
+ },
124
+ });
125
+ }
126
+ // A 202 carries no body; the id is in the header.
127
+ const id = res.headers.get("x-message-id");
128
+ return id !== null && id.length > 0 ? { providerId: id } : undefined;
139
129
  }
140
130
  }
141
131
 
@@ -158,7 +148,7 @@ function assertHasRecipients(message: MailMessage): void {
158
148
  /**
159
149
  * Build the SendGrid v3 `content[]`: text/plain when text is set, text/html when
160
150
  * html is set, always at least one entry (SendGrid rejects empty content). The
161
- * non-empty tuple type lets the SDK's MailDataRequired see `content[0]` exists.
151
+ * At least one entry always: SendGrid refuses a message with empty content.
162
152
  */
163
153
  function buildSendGridContent(
164
154
  message: MailMessage,
@@ -177,7 +167,9 @@ function buildSendGridContent(
177
167
 
178
168
  function wrapSendGridError(err: unknown): RoverError {
179
169
  if (err instanceof RoverError) return err;
180
- // @sendgrid/mail throws `{ code, message, response: { body, headers, statusCode } }`
170
+ // Built from the HTTP response here, but the older SDK shape
171
+ // (`{ code, message, response: { body, headers, statusCode } }`) is still
172
+ // accepted so an injected fake or a wrapped error maps the same way.
181
173
  const anyErr = err as {
182
174
  code?: number | string;
183
175
  message?: string;
@@ -224,27 +216,4 @@ function wrapSendGridError(err: unknown): RoverError {
224
216
  );
225
217
  }
226
218
 
227
- /**
228
- * Type-guard resolver for the `MailService` constructor attached to the
229
- * module's default export at runtime. The cerebrum forbids `as unknown as T`;
230
- * here we receive `sgMail` through a parameter typed `unknown`, narrow with
231
- * runtime `typeof` checks, and return a single cast to a precise callable
232
- * type. No double-cast chain, no `as unknown` anchor.
233
- */
234
- function resolveMailServiceCtor(mod: unknown): new () => MailService {
235
- if (mod && typeof mod === "object" && "MailService" in mod) {
236
- const candidate = (mod as { MailService: unknown }).MailService;
237
- if (typeof candidate === "function") {
238
- return candidate as new () => MailService;
239
- }
240
- }
241
- throw new RoverError(
242
- "E_MAIL_PROVIDER_CONFIG",
243
- "@sendgrid/mail runtime does not expose `.MailService` — upgrade to v8+",
244
- {
245
- hint: "Expected `module.exports.MailService` to be the MailService class (index.js attaches it).",
246
- },
247
- );
248
- }
249
-
250
219
  registerTransport("sendgrid", (config) => new SendGridTransport(config));
@@ -177,9 +177,12 @@ export class SparkPostTransport implements MailTransport {
177
177
  function parseAddress(input: string): { email: string; name?: string } {
178
178
  const s = stripCrlf(input).trim();
179
179
  const match = s.match(/^(.*)<([^>]+)>\s*$/);
180
- if (match) {
181
- const email = match[2].trim();
182
- const name = match[1].trim().replace(/^"|"$/g, "").trim();
180
+ // Both groups are required by the pattern, so a match carries both —
181
+ // named rather than indexed, which is what says so.
182
+ const [, rawName, rawEmail] = match ?? [];
183
+ if (rawEmail !== undefined) {
184
+ const email = rawEmail.trim();
185
+ const name = (rawName ?? "").trim().replace(/^"|"$/g, "").trim();
183
186
  return name ? { email, name } : { email };
184
187
  }
185
188
  return { email: s };
@@ -37,5 +37,13 @@ export type WebhookMiddleware = (
37
37
  ) => Promise<void>;
38
38
 
39
39
  export interface WebhookEmitter {
40
- emit(event: string, data: unknown): void;
40
+ /**
41
+ * `unknown`, not `void`. `@adonisjs/events` declares
42
+ * `emit(): Promise<void>` and rethrows when a listener fails and no error
43
+ * handler is registered — and a `void` return ACCEPTS a promise-returning
44
+ * function, so the call site reads as if there were nothing to handle.
45
+ * Duck-typed, so it stays wider than Adonis's own class: a Node
46
+ * EventEmitter returns boolean.
47
+ */
48
+ emit(event: string, data: unknown): unknown;
41
49
  }
@@ -1,5 +1,6 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
+ import { emitSafely } from "../emitSafely.js";
3
4
  import type {
4
5
  WebhookEmitter,
5
6
  WebhookHttpContext,
@@ -94,7 +95,7 @@ export function createMailgunWebhookHandler(
94
95
  const eventName = EVENT_MAP[data.event];
95
96
  if (eventName) {
96
97
  try {
97
- emitter.emit(eventName, {
98
+ emitSafely(emitter, eventName, {
98
99
  messageId: data.message?.headers?.["message-id"] ?? "",
99
100
  to: data.recipient ?? "",
100
101
  reason: data.reason,
@@ -1,5 +1,6 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
+ import { emitSafely } from "../emitSafely.js";
3
4
  import type {
4
5
  WebhookEmitter,
5
6
  WebhookHttpContext,
@@ -176,7 +177,7 @@ function emitMappedEvent(
176
177
  ? (payload.data.to[0] ?? "")
177
178
  : (payload.data?.to ?? "");
178
179
  try {
179
- emitter.emit(mapped, {
180
+ emitSafely(emitter, mapped, {
180
181
  messageId: payload.data?.email_id ?? "",
181
182
  to,
182
183
  reason: payload.data?.reason,
@@ -1,5 +1,6 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { createPublicKey, verify } from "node:crypto";
3
+ import { emitSafely } from "../emitSafely.js";
3
4
  import type {
4
5
  WebhookEmitter,
5
6
  WebhookHttpContext,
@@ -101,7 +102,7 @@ export function createSendGridWebhookHandler(
101
102
  const mapped = EVENT_MAP[ev.event];
102
103
  if (!mapped) continue;
103
104
  try {
104
- emitter.emit(mapped, {
105
+ emitSafely(emitter, mapped, {
105
106
  messageId: ev.sg_message_id ?? "",
106
107
  to: ev.email ?? "",
107
108
  reason: ev.reason,