@agentisend/sdk-node 0.1.0 → 0.4.0

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.
@@ -8,42 +8,128 @@
8
8
  *
9
9
  * So the surface is a declared table (`RESEND_SDK_SURFACE`), the shim is built
10
10
  * from it, and `sdk_compat_covers_every_resend_sdk_method` fails if the two
11
- * ever disagree. Where AgentiSend is deliberately stricter, the shim WARNS
12
- * rather than silently changing behaviour — a migration warning is a feature.
11
+ * ever disagree. `compat_resend_covers_every_resend_sdk_member` goes further
12
+ * and walks a real `resend` package instance: every member it has, ours has
13
+ * too — either wired to the matching AgentiSend endpoint, or a `not_supported`
14
+ * error naming what to use instead, never `undefined`. Where AgentiSend is
15
+ * deliberately stricter, the shim WARNS rather than silently changing
16
+ * behaviour — a migration warning is a feature.
13
17
  *
14
- * Mapped against research/10 §A.1 (Resend OpenAPI, 113 operations, fetched
18
+ * Mapped against resend@6.30.0's own type definitions (RESENDCOMPAT-6,
19
+ * CLAIMS-11) and research/10 §A.1 (Resend OpenAPI, 113 operations, fetched
15
20
  * 2026-09-04) and research/01.
16
21
  */
17
22
  /* eslint-disable no-console -- the shim's migration warnings are the feature */
18
23
  import { AgentiSend } from './client.js';
19
- import { AgentiSendError } from './errors.js';
24
+ import { AgentiSendError, toResendName } from './errors.js';
25
+ import { verifySvixSignature, SvixSignatureVerificationError } from './webhook-signing.js';
26
+ /** `string | {id, ...rest}` — Resend's own-object-carries-its-id call shape, normalized. */
27
+ function splitIdPayload(first, second) {
28
+ if (typeof first === 'string')
29
+ return [first, (second ?? {})];
30
+ const { id, ...rest } = first;
31
+ return [id, rest];
32
+ }
20
33
  /**
21
34
  * Every method the Resend Node SDK exposes that AgentiSend answers, as data.
22
35
  *
23
36
  * `audiences` is absent on purpose: Resend marks it Deprecated in their own
24
- * spec, and building a shim for a surface its author is removing would tie a
25
- * migration to something with no future.
37
+ * spec (an alias of `segments`), and building a shim for a surface its author
38
+ * is removing would tie a migration to something with no future.
26
39
  */
27
40
  export const RESEND_SDK_SURFACE = {
28
- emails: ['send', 'get', 'list', 'update', 'cancel'],
29
- 'emails.batch': ['send'],
41
+ emails: ['send', 'create', 'get', 'list', 'update', 'cancel', 'metrics'],
42
+ 'emails.batch': ['send', 'create'],
43
+ 'emails.attachments': ['get', 'list'],
44
+ 'emails.receiving': ['get', 'list', 'forward'],
45
+ 'emails.receiving.attachments': ['get', 'list'],
46
+ batch: ['send', 'create'],
30
47
  domains: ['create', 'list', 'get', 'update', 'verify', 'remove'],
31
48
  apiKeys: ['create', 'list', 'update', 'remove'],
32
49
  contacts: ['create', 'list', 'get', 'update', 'remove'],
50
+ 'contacts.topics': ['list', 'update'],
51
+ 'contacts.segments': ['list', 'add', 'remove'],
52
+ 'contacts.imports': ['create', 'list', 'get'],
33
53
  topics: ['create', 'list', 'get', 'update', 'remove'],
34
54
  segments: ['create', 'list', 'get', 'update', 'remove'],
35
- broadcasts: ['create', 'list', 'get', 'update', 'send', 'remove'],
36
- templates: ['create', 'list', 'get', 'update', 'publish', 'render', 'duplicate'],
37
- suppressions: ['create', 'list', 'remove'],
55
+ broadcasts: ['create', 'list', 'get', 'update', 'send', 'remove', 'cancel', 'duplicate'],
56
+ templates: ['create', 'list', 'get', 'update', 'publish', 'render', 'duplicate', 'remove'],
57
+ suppressions: ['add', 'list', 'get', 'remove'],
38
58
  'suppressions.batch': ['add', 'remove'],
39
- automations: ['create', 'list', 'get', 'update', 'enable', 'disable'],
40
- webhooks: ['create', 'list', 'get', 'update', 'remove'],
41
- events: ['send'],
59
+ automations: ['create', 'list', 'get', 'update', 'enable', 'disable', 'remove', 'duplicate', 'stop'],
60
+ 'automations.runs': ['get', 'list'],
61
+ webhooks: ['create', 'list', 'get', 'update', 'remove', 'verify', 'rotateSigningSecret'],
62
+ 'webhooks.events': ['get', 'list', 'replay'],
63
+ events: ['send', 'create', 'get', 'list', 'update', 'remove'],
64
+ contactProperties: ['create', 'list', 'get', 'update', 'remove'],
65
+ logs: ['get', 'list'],
66
+ usage: ['get'],
67
+ oauthGrants: ['list', 'revoke'],
42
68
  };
69
+ /** A local refusal (never reaches the API), 422 unless stated (RESENDCOMPAT-3). */
70
+ class LocalRefusal extends Error {
71
+ code;
72
+ statusCode;
73
+ constructor(code, message, statusCode = 422) {
74
+ super(message);
75
+ this.code = code;
76
+ this.statusCode = statusCode;
77
+ }
78
+ }
79
+ /** `{data:null, error:{name:'not_supported', statusCode:501, message}}`, never `undefined`. */
80
+ function notSupported(message) {
81
+ return { data: null, error: { message, name: 'not_supported', statusCode: 501, code: 'not_supported' } };
82
+ }
83
+ /**
84
+ * A callable stand-in for one path under a not-supported resource. Calling it
85
+ * answers `not_supported`; reading any further property off it (Resend's own
86
+ * surface nests namespaces arbitrarily deep — `webhooks.events.attempts.list`)
87
+ * returns another one of these, so a real member at any depth still resolves
88
+ * to a function rather than `undefined`.
89
+ */
90
+ function notSupportedNode(path, hint) {
91
+ const fn = async () => notSupported(`${path} is not available. ${hint}`);
92
+ return new Proxy(fn, {
93
+ get(target, prop, receiver) {
94
+ if (typeof prop === 'symbol' || prop in target)
95
+ return Reflect.get(target, prop, receiver);
96
+ return notSupportedNode(`${path}.${String(prop)}`, hint);
97
+ },
98
+ });
99
+ }
100
+ /**
101
+ * A namespace for a Resend resource we do not answer at all (`contactProperties`,
102
+ * `logs`, `usage`, `oauthGrants`) or only partly (`events`, `contacts.imports`,
103
+ * `automations`). Any member, at any depth, reaches a `not_supported` stub
104
+ * instead of `undefined` — `compat_resend_covers_every_resend_sdk_member`
105
+ * walks the real SDK and checks every member resolves to a function or a
106
+ * namespace, this way.
107
+ */
108
+ function notSupportedNamespace(resource, hint) {
109
+ return new Proxy({}, {
110
+ get(_target, prop) {
111
+ if (typeof prop !== 'string')
112
+ return undefined;
113
+ return notSupportedNode(`${resource}.${prop}`, hint);
114
+ },
115
+ });
116
+ }
117
+ /** As {@link notSupportedNamespace}, but falls back only for members `target` does not itself answer. */
118
+ function withNotSupportedFallback(target, resource, hint) {
119
+ return new Proxy(target, {
120
+ get(obj, prop, receiver) {
121
+ if (prop in obj || typeof prop === 'symbol')
122
+ return Reflect.get(obj, prop, receiver);
123
+ return notSupportedNode(`${resource}.${String(prop)}`, hint);
124
+ },
125
+ });
126
+ }
43
127
  export class Resend {
44
128
  client;
45
129
  warn;
46
130
  emails;
131
+ /** Resend's top-level `batch` — the same calls as `emails.batch` (RESENDCOMPAT-6/CLAIMS-11). */
132
+ batch;
47
133
  domains;
48
134
  apiKeys;
49
135
  contacts;
@@ -55,58 +141,80 @@ export class Resend {
55
141
  automations;
56
142
  webhooks;
57
143
  events;
58
- constructor(apiKey, baseUrl) {
59
- this.client = new AgentiSend(apiKey, baseUrl !== undefined ? { baseUrl } : {});
144
+ /** Resend's `contactProperties`, `logs`, `usage`, `oauthGrants`: no AgentiSend counterpart today. */
145
+ contactProperties;
146
+ logs;
147
+ usage;
148
+ oauthGrants;
149
+ constructor(key = process.env.RESEND_API_KEY ?? process.env.AGENTISEND_API_KEY, options) {
150
+ const resolved = typeof options === 'string' ? { baseUrl: options } : options ?? {};
151
+ this.client = new AgentiSend(key, resolved);
60
152
  // A migration warning is a feature: it names exactly where AgentiSend is
61
153
  // stricter than Resend instead of silently changing behaviour.
62
154
  this.warn = (message) => console.warn(`[agentisend/compat-resend] ${message}`);
155
+ const sendOne = async (payload, options) => asResend(async () => {
156
+ const resolved = await this.resolveReact(payload);
157
+ const result = await this.client.emails.send(this.toSendBody(resolved), options);
158
+ return { id: result.id };
159
+ });
160
+ const sendBatch = async (payloads, options) => asResend(async () => {
161
+ const resolved = await Promise.all(payloads.map((p) => this.resolveReact(p)));
162
+ const bodies = resolved.map((p) => this.toSendBody(p));
163
+ const mode = options?.batchValidation ?? 'strict';
164
+ const requestOptions = {
165
+ ...(options?.idempotencyKey !== undefined ? { idempotencyKey: options.idempotencyKey } : {}),
166
+ ...(mode === 'native' ? {} : { headers: { 'x-batch-validation': mode } }),
167
+ };
168
+ const result = (await this.client.emails.batch(bodies, requestOptions));
169
+ return { data: batchResultItems(mode, result) };
170
+ });
63
171
  this.emails = {
64
- send: async (payload, options) => asResend(async () => {
65
- if (!payload.html && !payload.text) {
66
- throw new Error('Provide html or text.');
67
- }
68
- const result = await this.client.emails.send(this.toSendBody(payload), options);
69
- return { id: result.id };
70
- }),
172
+ send: sendOne,
173
+ create: sendOne,
71
174
  get: async (id) => asResend(async () => row(await this.client.emails.get(id))),
72
175
  list: async (query = {}) => asResend(async () => rowsOf(await this.client.emails.list(query))),
73
- update: async (id, payload) => {
176
+ update: async (idOrPayload, payload) => {
177
+ const [id, body] = splitIdPayload(idOrPayload, payload);
74
178
  // Their `update` only ever moved the schedule; ours is the same act
75
179
  // under a name that says so.
76
- return asResend(async () => row(await this.client.emails.reschedule(id, payload.scheduledAt)));
180
+ return asResend(async () => row(await this.client.emails.reschedule(id, body.scheduledAt)));
77
181
  },
78
182
  cancel: async (id) => asResend(async () => row(await this.client.emails.cancel(id))),
79
- batch: {
80
- send: async (payloads, options) => asResend(async () => {
81
- const results = await this.client.emails.batch(payloads.map((p) => this.toSendBody(p)), options);
82
- return results.data.map((item) => {
83
- if (item.status === 'accepted' && item.id !== undefined)
84
- return { id: item.id };
85
- return {
86
- id: null,
87
- error: {
88
- message: item.error?.message ?? 'This item was not accepted.',
89
- name: item.error?.code ?? 'validation_error',
90
- statusCode: 422,
91
- },
92
- };
93
- });
94
- }),
183
+ metrics: async (query = {}) => asResend(async () => row(await this.client.emails.metrics(query))),
184
+ batch: { send: sendBatch, create: sendBatch },
185
+ attachments: {
186
+ get: async ({ emailId, id }) => asResend(async () => ({ content: await this.client.emails.attachment(emailId, id) })),
187
+ list: async ({ emailId }) => asResend(async () => rowsOf(await this.client.emails.attachments(emailId))),
188
+ },
189
+ receiving: {
190
+ get: async (id) => asResend(async () => row(await this.client.receiving.get(id))),
191
+ list: async (query = {}) => asResend(async () => rowsOf(await this.client.receiving.list(query))),
192
+ forward: async () => notSupported('emails.receiving.forward is not available. Reply with emails.receiving.reply-shaped POST /emails/receiving/:id/reply through client.request(), or in the console.'),
193
+ attachments: {
194
+ get: async ({ emailId, id }) => asResend(async () => ({ content: await this.client.receiving.attachment(emailId, id) })),
195
+ list: async ({ emailId }) => asResend(async () => rowsOf(await this.client.receiving.attachments(emailId))),
196
+ },
95
197
  },
96
198
  };
199
+ this.batch = this.emails.batch;
200
+ this.emails = withNotSupportedFallback(this.emails, 'emails', 'attachments, receiving and share have no counterpart beyond what is wired above.');
97
201
  this.domains = {
98
202
  create: async (input) => {
99
203
  if (typeof input.region !== 'string' || input.region.length === 0) {
100
- this.warn('Resend defaulted a domain region silently; AgentiSend requires an explicit region (F13). Pass region explicitly.');
204
+ this.warn('Resend defaulted a domain region silently; omitting it here stores this domain as us (Oregon) — pass an explicit region to choose eu.');
101
205
  }
102
- return asResend(async () => row(await this.client.domains.create(input)));
206
+ return asResend(async () => row(await this.client.domains.create(resendDomainCreate(input))));
103
207
  },
104
208
  list: async () => asResend(async () => rowsOf(await this.client.domains.list())),
105
209
  get: async (id) => asResend(async () => row(await this.client.domains.get(id))),
106
- update: async (id, payload) => asResend(async () => row(await this.client.domains.update(id, payload))),
210
+ update: async (idOrPayload, maybe) => {
211
+ const [id, raw] = splitIdPayload(idOrPayload, maybe);
212
+ return asResend(async () => row(await this.client.domains.update(id, resendDomainUpdate(raw))));
213
+ },
107
214
  verify: async (id) => asResend(async () => row(await this.client.domains.verify(id))),
108
215
  remove: async (id) => asResend(async () => row(await this.client.domains.delete(id))),
109
216
  };
217
+ this.domains = withNotSupportedFallback({ ...this.domains, claims: notSupportedNamespace('domains.claims', 'Domain claims are not available today.') }, 'domains', 'Not available today.');
110
218
  this.apiKeys = {
111
219
  create: async (input) => asResend(async () => row(await this.client.apiKeys.create({ name: input.name, permission: input.permission }))),
112
220
  list: async () => asResend(async () => rowsOf(await this.client.apiKeys.list())),
@@ -114,17 +222,100 @@ export class Resend {
114
222
  remove: async (id) => asResend(async () => row(await this.client.apiKeys.delete(id))),
115
223
  };
116
224
  this.contacts = {
117
- create: async (input) => asResend(async () => row(await this.client.contacts.create(input))),
118
- list: async (query = {}) => asResend(async () => rowsOf(await this.client.contacts.list(query))),
119
- get: async (id) => asResend(async () => row(await this.client.contacts.get(id))),
120
- update: async (id, input) => asResend(async () => row(await this.client.contacts.update(id, input))),
121
- remove: async (id) => asResend(async () => row(await this.client.contacts.delete(id))),
225
+ create: async (input) => asResend(async () => row(await this.client.request({
226
+ method: 'POST',
227
+ path: input.audienceId ? `/audiences/${input.audienceId}/contacts` : '/contacts',
228
+ body: contactWireBody(input),
229
+ }))),
230
+ list: async (query = {}) => asResend(async () => {
231
+ const segmentId = query.segmentId ?? query.audienceId;
232
+ if (segmentId) {
233
+ const page = await this.client.request({
234
+ method: 'GET',
235
+ path: `/segments/${segmentId}/contacts`,
236
+ ...(query.email !== undefined ? { query: { email: query.email } } : {}),
237
+ });
238
+ return rowsOf(page);
239
+ }
240
+ return rowsOf(await this.client.contacts.list(query));
241
+ }),
242
+ get: async (idOrOptions) => {
243
+ const ref = contactRef(idOrOptions);
244
+ if (!ref.ok)
245
+ return ref.result;
246
+ return asResend(async () => row(await this.client.request({ method: 'GET', path: contactPath(ref.audienceId, ref.key) })));
247
+ },
248
+ update: async (idOrOptions, input) => {
249
+ if (typeof idOrOptions === 'string') {
250
+ return asResend(async () => row(await this.client.request({
251
+ method: 'PATCH',
252
+ path: `/contacts/${idOrOptions}`,
253
+ body: contactWireBody(input ?? {}),
254
+ })));
255
+ }
256
+ const ref = contactRef(idOrOptions);
257
+ if (!ref.ok)
258
+ return ref.result;
259
+ return asResend(async () => row(await this.client.request({
260
+ method: 'PATCH',
261
+ path: contactPath(ref.audienceId, ref.key),
262
+ body: contactWireBody(idOrOptions),
263
+ })));
264
+ },
265
+ remove: async (idOrOptions) => {
266
+ const ref = contactRef(idOrOptions);
267
+ if (!ref.ok)
268
+ return ref.result;
269
+ return asResend(async () => row(await this.client.request({ method: 'DELETE', path: contactPath(ref.audienceId, ref.key) })));
270
+ },
271
+ topics: {
272
+ list: async ({ id, email, ...query }) => {
273
+ const key = id ?? email;
274
+ if (!key)
275
+ return notSupportedRefusal('contacts.topics.list needs id or email.');
276
+ return asResend(async () => rowsOf(await this.client.request({
277
+ method: 'GET',
278
+ path: `/contacts/${key}/topics`,
279
+ query: query,
280
+ })));
281
+ },
282
+ update: async ({ id, email, topics }) => {
283
+ const key = id ?? email;
284
+ if (!key)
285
+ return notSupportedRefusal('contacts.topics.update needs id or email.');
286
+ return asResend(async () => row(await this.client.request({ method: 'PATCH', path: `/contacts/${key}/topics`, body: topics })));
287
+ },
288
+ },
289
+ segments: {
290
+ list: async ({ contactId, email }) => {
291
+ const key = contactId ?? email;
292
+ if (!key)
293
+ return notSupportedRefusal('contacts.segments.list needs contactId or email.');
294
+ return asResend(async () => rowsOf(await this.client.contacts.segments(key)));
295
+ },
296
+ add: async ({ contactId, email, segmentId }) => {
297
+ const key = contactId ?? email;
298
+ if (!key)
299
+ return notSupportedRefusal('contacts.segments.add needs contactId or email.');
300
+ return asResend(async () => row(await this.client.contacts.addSegment(key, segmentId)));
301
+ },
302
+ remove: async ({ contactId, email, segmentId }) => {
303
+ const key = contactId ?? email;
304
+ if (!key)
305
+ return notSupportedRefusal('contacts.segments.remove needs contactId or email.');
306
+ return asResend(async () => row(await this.client.contacts.removeSegment(key, segmentId)));
307
+ },
308
+ },
309
+ imports: notSupportedNamespace('contacts.imports', 'Send a CSV through client.request({method:"POST", path:"/contacts/imports", body:{csv}}) instead.'),
122
310
  };
123
311
  this.topics = {
124
312
  create: async (input) => asResend(async () => row(await this.client.topics.create(input))),
125
313
  list: async () => asResend(async () => rowsOf(await this.client.topics.list())),
126
314
  get: async (id) => asResend(async () => row(await this.client.topics.get(id))),
127
- update: async (id, input) => asResend(async () => row(await this.client.topics.update(id, input))),
315
+ update: async (idOrPayload, input) => {
316
+ const [id, payload] = splitIdPayload(idOrPayload, input);
317
+ return asResend(async () => row(await this.client.topics.update(id, payload)));
318
+ },
128
319
  remove: async (id) => asResend(async () => row(await this.client.topics.delete(id))),
129
320
  };
130
321
  this.segments = {
@@ -139,12 +330,15 @@ export class Resend {
139
330
  list: async () => asResend(async () => rowsOf(await this.client.broadcasts.list())),
140
331
  get: async (id) => asResend(async () => row(await this.client.broadcasts.get(id))),
141
332
  update: async (id, payload) => asResend(async () => row(await this.client.broadcasts.update(id, payload))),
142
- send: async (id) => asResend(async () => row(await this.client.broadcasts.send(id))),
333
+ send: async (id, payload) => asResend(async () => row(await this.client.broadcasts.send(id, payload))),
334
+ cancel: async (id) => asResend(async () => row(await this.client.broadcasts.cancel(id))),
143
335
  remove: async (id) => {
144
336
  this.warn('Resend deletes a broadcast; AgentiSend archives it, so what was sent stays auditable (F14). This archived it.');
145
337
  return asResend(async () => row(await this.client.broadcasts.archive(id)));
146
338
  },
339
+ duplicate: async () => notSupported('broadcasts.duplicate is not available. Read it with broadcasts.get and broadcasts.create a copy.'),
147
340
  };
341
+ this.broadcasts = withNotSupportedFallback(this.broadcasts, 'broadcasts', 'Per-recipient and per-link engagement rows are not available; read GET /broadcasts/:id/stats through client.request().');
148
342
  this.templates = {
149
343
  create: async (payload) => asResend(async () => row(await this.client.templates.create(payload))),
150
344
  list: async () => asResend(async () => rowsOf(await this.client.templates.list())),
@@ -153,69 +347,297 @@ export class Resend {
153
347
  publish: async (id) => asResend(async () => row(await this.client.templates.publish(id))),
154
348
  render: async (id, values = {}) => asResend(async () => await this.client.templates.render(id, values)),
155
349
  duplicate: async (id, name) => asResend(async () => row(await this.client.templates.duplicate(id, name !== undefined ? { name } : {}))),
350
+ remove: async (id) => asResend(async () => row(await this.client.templates.delete(id))),
156
351
  };
157
352
  this.suppressions = {
158
- create: async (input) => asResend(async () => row(await this.client.suppressions.add(input))),
353
+ add: async (input) => {
354
+ const email = typeof input === 'string' ? input : input.email;
355
+ const level = typeof input === 'string' ? undefined : input.level;
356
+ return asResend(async () => row(await this.client.suppressions.add({ email, ...(level ? { level } : {}) })));
357
+ },
159
358
  list: async () => asResend(async () => rowsOf(await this.client.suppressions.list())),
160
- remove: async (id) => asResend(async () => row(await this.client.suppressions.remove(id))),
359
+ get: async (idOrEmail) => asResend(async () => row(await this.client.suppressions.get(idOrEmail))),
360
+ remove: async (idOrEmail) => asResend(async () => row(await this.client.suppressions.remove(idOrEmail))),
161
361
  batch: {
162
- add: async (emails) => asResend(async () => rowsOf(await this.client.suppressions.batchAdd(emails))),
163
- remove: async (emails) => asResend(async () => rowsOf(await this.client.suppressions.batchRemove(emails))),
362
+ add: async (input) => {
363
+ const emails = Array.isArray(input) ? input : input.emails;
364
+ return asResend(async () => rowsOf(await this.client.suppressions.batchAdd(emails)));
365
+ },
366
+ remove: async (input) => {
367
+ const emails = Array.isArray(input) ? input : (input.emails ?? []);
368
+ return asResend(async () => rowsOf(await this.client.suppressions.batchRemove(emails)));
369
+ },
164
370
  },
165
371
  };
166
- this.automations = {
372
+ const automations = {
167
373
  create: async (payload) => asResend(async () => row(await this.client.automations.create(payload))),
168
374
  list: async () => asResend(async () => rowsOf(await this.client.automations.list())),
169
375
  get: async (id) => asResend(async () => row(await this.client.automations.get(id))),
170
376
  update: async (id, payload) => asResend(async () => row(await this.client.automations.update(id, payload))),
171
377
  enable: async (id) => asResend(async () => row(await this.client.automations.enable(id))),
172
378
  disable: async (id) => asResend(async () => row(await this.client.automations.disable(id))),
379
+ runs: {
380
+ list: async ({ automationId, ...query }) => asResend(async () => rowsOf(await this.client.automations.runs(automationId, query))),
381
+ get: async ({ automationId, runId }) => asResend(async () => row(await this.client.automations.run(automationId, runId))),
382
+ },
173
383
  };
174
- this.webhooks = {
175
- create: async (input) => asResend(async () => row(await this.client.webhooks.create({
176
- url: input.endpoint,
384
+ this.automations = withNotSupportedFallback(automations, 'automations', 'There is no remove, duplicate or stop for automations today; disable it instead.');
385
+ const webhooks = {
386
+ create: async (input) => asResend(async () => webhookRow(await this.client.webhooks.create({
387
+ url: (input.endpoint ?? input.url),
177
388
  events: input.events.includes('*') ? ['*'] : input.events,
389
+ // Always on: the shim is the bridge for a receiver that kept
390
+ // its Svix verifier and Resend's own envelope, whether or not
391
+ // the caller asked for either by name (RESENDCOMPAT-7).
392
+ svix_compat: true,
393
+ payload_format: 'resend',
178
394
  }))),
179
395
  list: async () => asResend(async () => rowsOf(await this.client.webhooks.list())),
180
- get: async (id) => asResend(async () => row(await this.client.webhooks.get(id))),
181
- update: async (id, payload) => asResend(async () => row(await this.client.webhooks.update(id, payload))),
396
+ get: async (id) => asResend(async () => webhookRow(await this.client.webhooks.get(id))),
397
+ update: async (id, payload) => asResend(async () => webhookRow(await this.client.webhooks.update(id, payload))),
182
398
  remove: async (id) => asResend(async () => row(await this.client.webhooks.delete(id))),
399
+ rotateSigningSecret: async (id) => asResend(async () => {
400
+ const rotated = await this.client.webhooks.rotateSecret(id);
401
+ return { ...rotated, signing_secret: rotated.secret };
402
+ }),
403
+ verify: (payload) => verifySvixSignature(payload.webhookSecret, payload.payload, payload.headers),
404
+ // Delivery-attempt history: not available through the shim today.
405
+ events: notSupportedNamespace('webhooks.events', 'Read GET /webhooks/:id/deliveries through client.request() instead.'),
183
406
  };
184
- this.events = {
185
- send: async (payload) => asResend(async () => row(await this.client.events.ingest(payload))),
407
+ this.webhooks = withNotSupportedFallback(webhooks, 'webhooks', 'events (delivery attempt history) is not available; use GET /webhooks/:id/deliveries through client.request().');
408
+ const events = {
409
+ send: async (payload) => asResend(async () => {
410
+ const name = payload.event ?? payload.name;
411
+ const contactId = payload.contactId ?? payload.contact_id;
412
+ if (!name)
413
+ throw new LocalRefusal('missing_required_field', 'Provide event (or name).');
414
+ if (payload.email !== undefined && contactId === undefined) {
415
+ throw new LocalRefusal('not_supported', 'events.send by email is not available; resolve a contact_id first (contacts.list({email})).');
416
+ }
417
+ return row(await this.client.events.ingest({
418
+ name,
419
+ ...(contactId !== undefined ? { contact_id: contactId } : {}),
420
+ data: payload.payload ?? payload.data ?? {},
421
+ }));
422
+ }),
186
423
  };
424
+ this.events = withNotSupportedFallback(events, 'events', 'Custom event definitions (create/get/list/update/remove) are not available; events.send registers one on first use.');
425
+ this.contactProperties = notSupportedNamespace('contactProperties', 'Store extra fields on the contact itself with contacts.update({properties}).');
426
+ this.logs = notSupportedNamespace('logs', 'Read GET /emails/:id through client.request(), or the console.');
427
+ this.usage = notSupportedNamespace('usage', 'Read GET /account through client.request(), or the console.');
428
+ this.oauthGrants = notSupportedNamespace('oauthGrants', 'Manage MCP OAuth grants in the console.');
429
+ }
430
+ /** react → html (and text), via a dynamically imported, optional renderer (RESENDCOMPAT-3). */
431
+ async resolveReact(payload) {
432
+ if (payload.react === undefined)
433
+ return payload;
434
+ // Never a dependency of this package: a caller who wants react installs
435
+ // it, same as with the real Resend SDK. The specifier is read from a
436
+ // variable so a bundler / tsc does not try to resolve it eagerly.
437
+ const specifier = '@react-email/render';
438
+ let renderer;
439
+ try {
440
+ renderer = (await import(specifier));
441
+ }
442
+ catch {
443
+ throw new LocalRefusal('validation_error', 'Install @react-email/render to send react, or pass html.');
444
+ }
445
+ const { react, ...rest } = payload;
446
+ const html = await renderer.render(react);
447
+ let text;
448
+ try {
449
+ text = await renderer.render(react, { plainText: true });
450
+ }
451
+ catch {
452
+ text = undefined;
453
+ }
454
+ return { ...rest, html, ...(text !== undefined ? { text } : {}) };
187
455
  }
456
+ /**
457
+ * Forward every key `sendEmailBodySchema` accepts (RESENDCOMPAT-2, -3). The
458
+ * API already folds `replyTo`, `scheduledAt`, `topicId` and `template`, so
459
+ * only `to`/`cc`/`bcc` (to arrays), `tags` (list to map) and attachment
460
+ * content (Buffer/Uint8Array to base64) need transforming here — everything
461
+ * else is spread through unchanged, so an unknown key reaches the API's zod
462
+ * schema and gets a clear 400 instead of silently vanishing.
463
+ */
188
464
  toSendBody(payload) {
465
+ const { to, cc, bcc, reply_to, replyTo, tags, attachments, topicId, topic_id, react: _react, ...rest } = payload;
466
+ void _react; // stripped by resolveReact before this runs; never forwarded.
467
+ const topic = topicId ?? topic_id;
468
+ const replies = reply_to ?? replyTo;
189
469
  return {
190
- from: payload.from,
191
- to: toArray(payload.to),
192
- subject: payload.subject,
193
- ...(payload.html !== undefined ? { html: payload.html } : {}),
194
- ...(payload.text !== undefined ? { text: payload.text } : {}),
195
- ...(payload.cc !== undefined ? { cc: toArray(payload.cc) } : {}),
196
- ...(payload.bcc !== undefined ? { bcc: toArray(payload.bcc) } : {}),
197
- ...(payload.reply_to !== undefined ? { reply_to: toArray(payload.reply_to) } : {}),
198
- ...(payload.headers !== undefined ? { headers: payload.headers } : {}),
199
- ...(payload.tags !== undefined
200
- ? { tags: Object.fromEntries(payload.tags.map((t) => [t.name, t.value])) }
470
+ ...rest,
471
+ to: toArray(to),
472
+ ...(cc !== undefined ? { cc: toArray(cc) } : {}),
473
+ ...(bcc !== undefined ? { bcc: toArray(bcc) } : {}),
474
+ ...(replies !== undefined ? { reply_to: toArray(replies) } : {}),
475
+ ...(tags !== undefined
476
+ ? { tags: Array.isArray(tags) ? Object.fromEntries(tags.map((t) => [t.name, t.value])) : tags }
201
477
  : {}),
202
- ...(payload.attachments !== undefined ? { attachments: payload.attachments } : {}),
203
- ...(payload.scheduledAt !== undefined ? { scheduled_at: payload.scheduledAt } : {}),
478
+ ...(attachments !== undefined
479
+ ? { attachments: attachments.map((a) => ({ ...a, content: attachmentContentToWire(a.content) })) }
480
+ : {}),
481
+ ...(topic !== undefined && topic !== null ? { topic_id: topic } : {}),
204
482
  };
205
483
  }
206
484
  }
485
+ /** Resend field names on domains.create, written as the fields POST /domains stores. */
486
+ export function resendDomainCreate(input) {
487
+ const returnPath = input.return_path_subdomain ?? input.custom_return_path ?? input.customReturnPath;
488
+ const tls = input.tls_mode ?? input.tls;
489
+ const open = input.open_tracking ?? input.openTracking;
490
+ const click = input.click_tracking ?? input.clickTracking;
491
+ const trackingSubdomain = input.tracking_subdomain ?? input.trackingSubdomain;
492
+ return {
493
+ name: input.name,
494
+ ...(input.region !== undefined ? { region: input.region } : {}),
495
+ ...(returnPath !== undefined ? { return_path_subdomain: returnPath } : {}),
496
+ ...(tls !== undefined ? { tls_mode: tls } : {}),
497
+ ...(open !== undefined ? { open_tracking: open } : {}),
498
+ ...(click !== undefined ? { click_tracking: click } : {}),
499
+ ...(trackingSubdomain !== undefined ? { tracking_subdomain: trackingSubdomain } : {}),
500
+ };
501
+ }
502
+ /** Resend field names on domains.update (RESENDCOMPAT-6): camelCase or snake_case, either wins consistently. */
503
+ export function resendDomainUpdate(input) {
504
+ const click = input.click_tracking ?? input.clickTracking;
505
+ const open = input.open_tracking ?? input.openTracking;
506
+ const tls = input.tls_mode ?? input.tls;
507
+ const trackingSubdomain = input.tracking_subdomain ?? input.trackingSubdomain;
508
+ return {
509
+ ...(click !== undefined ? { click_tracking: click } : {}),
510
+ ...(open !== undefined ? { open_tracking: open } : {}),
511
+ ...(tls !== undefined ? { tls_mode: tls } : {}),
512
+ ...(trackingSubdomain !== undefined ? { tracking_subdomain: trackingSubdomain } : {}),
513
+ };
514
+ }
207
515
  function toArray(value) {
208
516
  return Array.isArray(value) ? value : [value];
209
517
  }
518
+ /** A base64 string, a Node Buffer, or any `Uint8Array` — all become base64 (RESENDCOMPAT-12). */
519
+ function attachmentContentToWire(content) {
520
+ if (content === undefined || typeof content === 'string')
521
+ return content;
522
+ return Buffer.from(content).toString('base64');
523
+ }
524
+ function contactWireBody(input) {
525
+ const src = input;
526
+ const first = src.first_name !== undefined ? src.first_name : src.firstName;
527
+ const last = src.last_name !== undefined ? src.last_name : src.lastName;
528
+ return {
529
+ ...(src.email !== undefined ? { email: src.email } : {}),
530
+ ...(first !== undefined ? { first_name: first } : {}),
531
+ ...(last !== undefined ? { last_name: last } : {}),
532
+ ...(src.unsubscribed !== undefined ? { unsubscribed: src.unsubscribed } : {}),
533
+ ...(src.status !== undefined ? { status: src.status } : {}),
534
+ ...(src.properties !== undefined ? { properties: src.properties } : {}),
535
+ };
536
+ }
537
+ function contactPath(audienceId, key) {
538
+ return audienceId ? `/audiences/${audienceId}/contacts/${key}` : `/contacts/${key}`;
539
+ }
540
+ function contactRef(idOrOptions) {
541
+ if (typeof idOrOptions === 'string')
542
+ return { ok: true, key: idOrOptions };
543
+ const key = idOrOptions.email ?? idOrOptions.id;
544
+ if (!key) {
545
+ return {
546
+ ok: false,
547
+ result: {
548
+ data: null,
549
+ error: {
550
+ message: 'Missing `id` or `email` field.',
551
+ name: 'missing_required_field',
552
+ statusCode: 422,
553
+ },
554
+ },
555
+ };
556
+ }
557
+ return {
558
+ ok: true,
559
+ key,
560
+ ...(idOrOptions.audienceId !== undefined ? { audienceId: idOrOptions.audienceId } : {}),
561
+ };
562
+ }
563
+ function notSupportedRefusal(message) {
564
+ return { data: null, error: { message, name: 'missing_required_field', statusCode: 422 } };
565
+ }
210
566
  function row(value) {
211
567
  return value;
212
568
  }
569
+ /** `object:'webhook'` beside the existing `secret`/`signing_secret` (RESENDCOMPAT-7). */
570
+ function webhookRow(value) {
571
+ return { object: 'webhook', ...value };
572
+ }
213
573
  function rowsOf(result) {
214
- return result.data;
574
+ return { object: 'list', data: result.data, has_more: result.has_more ?? false };
575
+ }
576
+ /** `native` mode's own `BatchItemResult[]` (`{index,status,id?,error?}`) → Resend's `{id}`/`{id:null,error}`. */
577
+ function nativeBatchItems(items) {
578
+ return items.map((item) => {
579
+ if (item.status === 'accepted' && item.id !== undefined)
580
+ return { id: item.id };
581
+ return {
582
+ id: null,
583
+ error: {
584
+ message: item.error?.message ?? 'This item was not accepted.',
585
+ name: toResendName(item.error?.code ?? 'validation_error', item.error?.message ?? ''),
586
+ statusCode: 422,
587
+ ...(item.error?.code !== undefined ? { code: item.error.code } : {}),
588
+ },
589
+ };
590
+ });
591
+ }
592
+ /**
593
+ * `strict`/`permissive` share Resend's own shape: `{id}` for every accepted
594
+ * item, in order, plus an `errors: [{index, message}]` side list for
595
+ * `permissive` (strict either has none, or the whole call already threw).
596
+ * Reconstructed here into one index-aligned array, so a caller's `data[i]`
597
+ * still lines up with `payloads[i]` the way `native` mode always has.
598
+ */
599
+ function shapedBatchItems(result) {
600
+ const errorsByIndex = new Map((result.errors ?? []).map((e) => [e.index, e]));
601
+ const total = result.data.length + errorsByIndex.size;
602
+ const accepted = [...result.data];
603
+ const out = [];
604
+ for (let i = 0; i < total; i++) {
605
+ const err = errorsByIndex.get(i);
606
+ if (err) {
607
+ out.push({ id: null, error: { message: err.message, name: toResendName('validation_error', err.message), statusCode: 422 } });
608
+ continue;
609
+ }
610
+ const item = accepted.shift();
611
+ out.push({ id: item ? item.id : null });
612
+ }
613
+ return out;
614
+ }
615
+ function batchResultItems(mode, result) {
616
+ if (mode === 'native') {
617
+ return nativeBatchItems(result.data);
618
+ }
619
+ return shapedBatchItems(result);
215
620
  }
216
621
  function toResendError(caught) {
217
622
  if (caught instanceof AgentiSendError) {
218
- return { message: caught.message, name: caught.code, statusCode: caught.status };
623
+ return {
624
+ message: caught.message,
625
+ name: toResendName(caught.code, caught.message),
626
+ statusCode: caught.status,
627
+ code: caught.code,
628
+ fix: caught.fix,
629
+ };
630
+ }
631
+ if (caught instanceof LocalRefusal) {
632
+ return {
633
+ message: caught.message,
634
+ name: toResendName(caught.code, caught.message),
635
+ statusCode: caught.statusCode,
636
+ code: caught.code,
637
+ };
638
+ }
639
+ if (caught instanceof SvixSignatureVerificationError) {
640
+ return { message: caught.message, name: 'validation_error', statusCode: 401 };
219
641
  }
220
642
  if (caught instanceof Error) {
221
643
  return { message: caught.message, name: 'application_error', statusCode: 500 };