@hogsend/plugin-resend 0.12.1 → 0.12.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hogsend/plugin-resend",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,8 +24,8 @@
24
24
  "dependencies": {
25
25
  "resend": "^6.12.3",
26
26
  "svix": "^1.94.0",
27
- "@hogsend/core": "^0.12.1",
28
- "@hogsend/email": "^0.12.1"
27
+ "@hogsend/core": "^0.12.2",
28
+ "@hogsend/email": "^0.12.2"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.0.0",
@@ -118,6 +118,38 @@ describe("sendEmail", () => {
118
118
  expect(arg.tags).toEqual(tags);
119
119
  });
120
120
 
121
+ it("sanitizes tag names/values to Resend's allowed charset", async () => {
122
+ const sendFn = vi.fn().mockResolvedValue({
123
+ data: { id: "resend_123" },
124
+ error: null,
125
+ });
126
+ const client = mockResendClient({ sendFn });
127
+
128
+ await sendEmail({
129
+ client,
130
+ options: {
131
+ from: "test@hogsend.com",
132
+ to: "user@example.com",
133
+ subject: "Test",
134
+ html: HTML,
135
+ // The engine's neutral tags carry journey names and slashed template
136
+ // keys — Resend only allows [A-Za-z0-9_-].
137
+ tags: [
138
+ { name: "journeyId", value: "Docs Subscriber" },
139
+ { name: "templateKey", value: "docs/welcome" },
140
+ ],
141
+ },
142
+ });
143
+
144
+ const arg = sendFn.mock.calls[0]?.[0] as {
145
+ tags?: Array<{ name: string; value: string }>;
146
+ };
147
+ expect(arg.tags).toEqual([
148
+ { name: "journeyId", value: "Docs-Subscriber" },
149
+ { name: "templateKey", value: "docs-welcome" },
150
+ ]);
151
+ });
152
+
121
153
  it("omits Resend tags when none are set", async () => {
122
154
  const sendFn = vi.fn().mockResolvedValue({
123
155
  data: { id: "resend_123" },
package/src/send.ts CHANGED
@@ -9,6 +9,21 @@ import type { BatchEmailItem, SendEmailOptions, SendResult } from "./types.js";
9
9
 
10
10
  const BATCH_CHUNK_SIZE = 100;
11
11
 
12
+ /**
13
+ * Resend rejects tag names/values outside ASCII letters, numbers, underscores
14
+ * and dashes. The engine's neutral tags carry journey names ("Docs Subscriber")
15
+ * and template keys ("docs/welcome"), so map anything else to "-" rather than
16
+ * failing the whole send.
17
+ */
18
+ function sanitizeTags(
19
+ tags: SendEmailOptions["tags"],
20
+ ): SendEmailOptions["tags"] {
21
+ return tags?.map((tag) => ({
22
+ name: tag.name.replace(/[^a-zA-Z0-9_-]/g, "-"),
23
+ value: tag.value.replace(/[^a-zA-Z0-9_-]/g, "-"),
24
+ }));
25
+ }
26
+
12
27
  function isRetryableStatusCode(statusCode: number): boolean {
13
28
  return statusCode === 429 || statusCode >= 500;
14
29
  }
@@ -109,8 +124,9 @@ export async function sendEmail(args: {
109
124
  cc: options.cc,
110
125
  bcc: options.bcc,
111
126
  scheduledAt: options.scheduledAt,
112
- // Resend accepts neutral `{name,value}[]` tags natively — pass them through.
113
- tags: options.tags,
127
+ // Resend accepts neutral `{name,value}[]` tags natively — sanitized to
128
+ // its allowed charset (see sanitizeTags).
129
+ tags: sanitizeTags(options.tags),
114
130
  headers: options.headers,
115
131
  });
116
132
 
@@ -182,8 +198,8 @@ async function sendBatchChunk(
182
198
  replyTo: email.replyTo,
183
199
  cc: email.cc,
184
200
  bcc: email.bcc,
185
- // Resend accepts neutral `{name,value}[]` tags natively.
186
- tags: email.tags,
201
+ // Resend accepts neutral `{name,value}[]` tags — sanitized to its charset.
202
+ tags: sanitizeTags(email.tags),
187
203
  headers: email.headers,
188
204
  })),
189
205
  );