@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.
- package/README.md +12 -8
- package/dist/cli/commands.js +2 -1
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/main.js +5 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/scheduled-at.d.ts +4 -3
- package/dist/cli/scheduled-at.js +86 -12
- package/dist/cli/scheduled-at.js.map +1 -1
- package/dist/client.d.ts +346 -6
- package/dist/client.js +398 -21
- package/dist/client.js.map +1 -1
- package/dist/compat-resend.d.ts +311 -50
- package/dist/compat-resend.js +500 -78
- package/dist/compat-resend.js.map +1 -1
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -1
- package/dist/types.d.ts +102 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhook-signing.d.ts +37 -1
- package/dist/webhook-signing.js +85 -16
- package/dist/webhook-signing.js.map +1 -1
- package/package.json +15 -10
package/dist/compat-resend.js
CHANGED
|
@@ -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.
|
|
12
|
-
*
|
|
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
|
|
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
|
|
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: ['
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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:
|
|
65
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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;
|
|
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 (
|
|
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.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
163
|
-
|
|
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
|
-
|
|
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.
|
|
175
|
-
|
|
176
|
-
|
|
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 () =>
|
|
181
|
-
update: async (id, payload) => asResend(async () =>
|
|
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.
|
|
185
|
-
|
|
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
|
-
|
|
191
|
-
to: toArray(
|
|
192
|
-
|
|
193
|
-
...(
|
|
194
|
-
...(
|
|
195
|
-
...(
|
|
196
|
-
|
|
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
|
-
...(
|
|
203
|
-
|
|
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 {
|
|
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 };
|