@agentisend/sdk-node 0.2.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 +327 -6
- package/dist/client.js +381 -21
- package/dist/client.js.map +1 -1
- package/dist/compat-resend.d.ts +310 -50
- package/dist/compat-resend.js +499 -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 +79 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhook-signing.d.ts +28 -1
- package/dist/webhook-signing.js +76 -16
- package/dist/webhook-signing.js.map +1 -1
- package/package.json +8 -2
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', 'metrics'],
|
|
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,59 +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
183
|
metrics: async (query = {}) => asResend(async () => row(await this.client.emails.metrics(query))),
|
|
80
|
-
batch: {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
}),
|
|
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
|
+
},
|
|
96
197
|
},
|
|
97
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.');
|
|
98
201
|
this.domains = {
|
|
99
202
|
create: async (input) => {
|
|
100
203
|
if (typeof input.region !== 'string' || input.region.length === 0) {
|
|
101
|
-
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.');
|
|
102
205
|
}
|
|
103
|
-
return asResend(async () => row(await this.client.domains.create(input)));
|
|
206
|
+
return asResend(async () => row(await this.client.domains.create(resendDomainCreate(input))));
|
|
104
207
|
},
|
|
105
208
|
list: async () => asResend(async () => rowsOf(await this.client.domains.list())),
|
|
106
209
|
get: async (id) => asResend(async () => row(await this.client.domains.get(id))),
|
|
107
|
-
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
|
+
},
|
|
108
214
|
verify: async (id) => asResend(async () => row(await this.client.domains.verify(id))),
|
|
109
215
|
remove: async (id) => asResend(async () => row(await this.client.domains.delete(id))),
|
|
110
216
|
};
|
|
217
|
+
this.domains = withNotSupportedFallback({ ...this.domains, claims: notSupportedNamespace('domains.claims', 'Domain claims are not available today.') }, 'domains', 'Not available today.');
|
|
111
218
|
this.apiKeys = {
|
|
112
219
|
create: async (input) => asResend(async () => row(await this.client.apiKeys.create({ name: input.name, permission: input.permission }))),
|
|
113
220
|
list: async () => asResend(async () => rowsOf(await this.client.apiKeys.list())),
|
|
@@ -115,17 +222,100 @@ export class Resend {
|
|
|
115
222
|
remove: async (id) => asResend(async () => row(await this.client.apiKeys.delete(id))),
|
|
116
223
|
};
|
|
117
224
|
this.contacts = {
|
|
118
|
-
create: async (input) => asResend(async () => row(await this.client.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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.'),
|
|
123
310
|
};
|
|
124
311
|
this.topics = {
|
|
125
312
|
create: async (input) => asResend(async () => row(await this.client.topics.create(input))),
|
|
126
313
|
list: async () => asResend(async () => rowsOf(await this.client.topics.list())),
|
|
127
314
|
get: async (id) => asResend(async () => row(await this.client.topics.get(id))),
|
|
128
|
-
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
|
+
},
|
|
129
319
|
remove: async (id) => asResend(async () => row(await this.client.topics.delete(id))),
|
|
130
320
|
};
|
|
131
321
|
this.segments = {
|
|
@@ -140,12 +330,15 @@ export class Resend {
|
|
|
140
330
|
list: async () => asResend(async () => rowsOf(await this.client.broadcasts.list())),
|
|
141
331
|
get: async (id) => asResend(async () => row(await this.client.broadcasts.get(id))),
|
|
142
332
|
update: async (id, payload) => asResend(async () => row(await this.client.broadcasts.update(id, payload))),
|
|
143
|
-
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))),
|
|
144
335
|
remove: async (id) => {
|
|
145
336
|
this.warn('Resend deletes a broadcast; AgentiSend archives it, so what was sent stays auditable (F14). This archived it.');
|
|
146
337
|
return asResend(async () => row(await this.client.broadcasts.archive(id)));
|
|
147
338
|
},
|
|
339
|
+
duplicate: async () => notSupported('broadcasts.duplicate is not available. Read it with broadcasts.get and broadcasts.create a copy.'),
|
|
148
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().');
|
|
149
342
|
this.templates = {
|
|
150
343
|
create: async (payload) => asResend(async () => row(await this.client.templates.create(payload))),
|
|
151
344
|
list: async () => asResend(async () => rowsOf(await this.client.templates.list())),
|
|
@@ -154,69 +347,297 @@ export class Resend {
|
|
|
154
347
|
publish: async (id) => asResend(async () => row(await this.client.templates.publish(id))),
|
|
155
348
|
render: async (id, values = {}) => asResend(async () => await this.client.templates.render(id, values)),
|
|
156
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))),
|
|
157
351
|
};
|
|
158
352
|
this.suppressions = {
|
|
159
|
-
|
|
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
|
+
},
|
|
160
358
|
list: async () => asResend(async () => rowsOf(await this.client.suppressions.list())),
|
|
161
|
-
|
|
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))),
|
|
162
361
|
batch: {
|
|
163
|
-
add: async (
|
|
164
|
-
|
|
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
|
+
},
|
|
165
370
|
},
|
|
166
371
|
};
|
|
167
|
-
|
|
372
|
+
const automations = {
|
|
168
373
|
create: async (payload) => asResend(async () => row(await this.client.automations.create(payload))),
|
|
169
374
|
list: async () => asResend(async () => rowsOf(await this.client.automations.list())),
|
|
170
375
|
get: async (id) => asResend(async () => row(await this.client.automations.get(id))),
|
|
171
376
|
update: async (id, payload) => asResend(async () => row(await this.client.automations.update(id, payload))),
|
|
172
377
|
enable: async (id) => asResend(async () => row(await this.client.automations.enable(id))),
|
|
173
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
|
+
},
|
|
174
383
|
};
|
|
175
|
-
this.
|
|
176
|
-
|
|
177
|
-
|
|
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),
|
|
178
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',
|
|
179
394
|
}))),
|
|
180
395
|
list: async () => asResend(async () => rowsOf(await this.client.webhooks.list())),
|
|
181
|
-
get: async (id) => asResend(async () =>
|
|
182
|
-
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))),
|
|
183
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.'),
|
|
184
406
|
};
|
|
185
|
-
this.
|
|
186
|
-
|
|
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
|
+
}),
|
|
187
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 } : {}) };
|
|
188
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
|
+
*/
|
|
189
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;
|
|
190
469
|
return {
|
|
191
|
-
|
|
192
|
-
to: toArray(
|
|
193
|
-
|
|
194
|
-
...(
|
|
195
|
-
...(
|
|
196
|
-
...(
|
|
197
|
-
|
|
198
|
-
...(payload.reply_to !== undefined ? { reply_to: toArray(payload.reply_to) } : {}),
|
|
199
|
-
...(payload.headers !== undefined ? { headers: payload.headers } : {}),
|
|
200
|
-
...(payload.tags !== undefined
|
|
201
|
-
? { 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 }
|
|
202
477
|
: {}),
|
|
203
|
-
...(
|
|
204
|
-
|
|
478
|
+
...(attachments !== undefined
|
|
479
|
+
? { attachments: attachments.map((a) => ({ ...a, content: attachmentContentToWire(a.content) })) }
|
|
480
|
+
: {}),
|
|
481
|
+
...(topic !== undefined && topic !== null ? { topic_id: topic } : {}),
|
|
205
482
|
};
|
|
206
483
|
}
|
|
207
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
|
+
}
|
|
208
515
|
function toArray(value) {
|
|
209
516
|
return Array.isArray(value) ? value : [value];
|
|
210
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
|
+
}
|
|
211
566
|
function row(value) {
|
|
212
567
|
return value;
|
|
213
568
|
}
|
|
569
|
+
/** `object:'webhook'` beside the existing `secret`/`signing_secret` (RESENDCOMPAT-7). */
|
|
570
|
+
function webhookRow(value) {
|
|
571
|
+
return { object: 'webhook', ...value };
|
|
572
|
+
}
|
|
214
573
|
function rowsOf(result) {
|
|
215
|
-
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);
|
|
216
620
|
}
|
|
217
621
|
function toResendError(caught) {
|
|
218
622
|
if (caught instanceof AgentiSendError) {
|
|
219
|
-
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 };
|
|
220
641
|
}
|
|
221
642
|
if (caught instanceof Error) {
|
|
222
643
|
return { message: caught.message, name: 'application_error', statusCode: 500 };
|