@upyo/mailgun 0.1.0-dev.10
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 +62 -0
- package/dist/index.cjs +403 -0
- package/dist/index.d.cts +300 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +400 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
//#region src/config.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a resolved Mailgun configuration by applying default values to optional fields.
|
|
4
|
+
*
|
|
5
|
+
* This function takes a partial Mailgun configuration and returns a complete
|
|
6
|
+
* configuration with all optional fields filled with sensible defaults.
|
|
7
|
+
*
|
|
8
|
+
* @param config - The Mailgun configuration with optional fields
|
|
9
|
+
* @returns A resolved configuration with all defaults applied
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const resolved = createMailgunConfig({
|
|
14
|
+
* apiKey: 'your-api-key',
|
|
15
|
+
* domain: 'your-domain.com'
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // resolved.region will be 'us' (default)
|
|
19
|
+
* // resolved.timeout will be 30000 (default)
|
|
20
|
+
* // resolved.retries will be 3 (default)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function createMailgunConfig(config) {
|
|
24
|
+
const region = config.region ?? "us";
|
|
25
|
+
const baseUrl = config.baseUrl ?? getDefaultBaseUrl(region);
|
|
26
|
+
return {
|
|
27
|
+
apiKey: config.apiKey,
|
|
28
|
+
domain: config.domain,
|
|
29
|
+
region,
|
|
30
|
+
baseUrl,
|
|
31
|
+
timeout: config.timeout ?? 3e4,
|
|
32
|
+
retries: config.retries ?? 3,
|
|
33
|
+
validateSsl: config.validateSsl ?? true,
|
|
34
|
+
headers: config.headers ?? {},
|
|
35
|
+
tracking: config.tracking ?? true,
|
|
36
|
+
clickTracking: config.clickTracking ?? true,
|
|
37
|
+
openTracking: config.openTracking ?? true
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Gets the default base URL for a given Mailgun region.
|
|
42
|
+
*
|
|
43
|
+
* @param region - The Mailgun region
|
|
44
|
+
* @returns The default base URL for the region
|
|
45
|
+
*/
|
|
46
|
+
function getDefaultBaseUrl(region) {
|
|
47
|
+
switch (region) {
|
|
48
|
+
case "us": return "https://api.mailgun.net/v3";
|
|
49
|
+
case "eu": return "https://api.eu.mailgun.net/v3";
|
|
50
|
+
default: throw new Error(`Unsupported region: ${region}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/http-client.ts
|
|
56
|
+
/**
|
|
57
|
+
* HTTP client wrapper for Mailgun API requests.
|
|
58
|
+
*
|
|
59
|
+
* This class handles authentication, request formatting, error handling,
|
|
60
|
+
* and retry logic for Mailgun API calls.
|
|
61
|
+
*/
|
|
62
|
+
var MailgunHttpClient = class {
|
|
63
|
+
config;
|
|
64
|
+
constructor(config) {
|
|
65
|
+
this.config = config;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Sends a message via Mailgun API.
|
|
69
|
+
*
|
|
70
|
+
* @param formData The form data to send to Mailgun.
|
|
71
|
+
* @param signal Optional AbortSignal for cancellation.
|
|
72
|
+
* @returns Promise that resolves to the Mailgun response.
|
|
73
|
+
*/
|
|
74
|
+
sendMessage(formData, signal) {
|
|
75
|
+
const url = `${this.config.baseUrl}/${this.config.domain}/messages`;
|
|
76
|
+
return this.makeRequest(url, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
body: formData,
|
|
79
|
+
signal
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Makes an HTTP request to Mailgun API with retry logic.
|
|
84
|
+
*
|
|
85
|
+
* @param url The URL to make the request to.
|
|
86
|
+
* @param options Fetch options.
|
|
87
|
+
* @returns Promise that resolves to the parsed response.
|
|
88
|
+
*/
|
|
89
|
+
async makeRequest(url, options) {
|
|
90
|
+
let lastError = null;
|
|
91
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) try {
|
|
92
|
+
const response = await this.fetchWithAuth(url, options);
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
let data;
|
|
95
|
+
try {
|
|
96
|
+
data = JSON.parse(text);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error(text);
|
|
99
|
+
}
|
|
100
|
+
if (!response.ok) throw new MailgunApiError(data.message ?? `HTTP ${response.status}`, response.status);
|
|
101
|
+
return data;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
104
|
+
if (error instanceof MailgunApiError && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) throw error;
|
|
105
|
+
if (attempt === this.config.retries) throw error;
|
|
106
|
+
const delay = Math.pow(2, attempt) * 1e3;
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
108
|
+
}
|
|
109
|
+
throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Makes a fetch request with Mailgun authentication.
|
|
113
|
+
*
|
|
114
|
+
* @param url The URL to make the request to.
|
|
115
|
+
* @param options Fetch options.
|
|
116
|
+
* @returns Promise that resolves to the fetch response.
|
|
117
|
+
*/
|
|
118
|
+
async fetchWithAuth(url, options) {
|
|
119
|
+
const headers = new Headers(options.headers);
|
|
120
|
+
const auth = btoa(`api:${this.config.apiKey}`);
|
|
121
|
+
headers.set("Authorization", `Basic ${auth}`);
|
|
122
|
+
for (const [key, value] of Object.entries(this.config.headers)) headers.set(key, value);
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
125
|
+
let signal = controller.signal;
|
|
126
|
+
if (options.signal) signal = AbortSignal.any([controller.signal, options.signal]);
|
|
127
|
+
try {
|
|
128
|
+
return await globalThis.fetch(url, {
|
|
129
|
+
...options,
|
|
130
|
+
headers,
|
|
131
|
+
signal
|
|
132
|
+
});
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timeoutId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Custom error class for Mailgun API errors.
|
|
140
|
+
*/
|
|
141
|
+
var MailgunApiError = class extends Error {
|
|
142
|
+
statusCode;
|
|
143
|
+
constructor(message, statusCode) {
|
|
144
|
+
super(message);
|
|
145
|
+
this.name = "MailgunApiError";
|
|
146
|
+
this.statusCode = statusCode;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/message-converter.ts
|
|
152
|
+
/**
|
|
153
|
+
* Converts a Upyo Message to Mailgun API FormData format.
|
|
154
|
+
*
|
|
155
|
+
* This function transforms the standardized Upyo message format into
|
|
156
|
+
* the specific format expected by the Mailgun API.
|
|
157
|
+
*
|
|
158
|
+
* @param message - The Upyo message to convert
|
|
159
|
+
* @param config - The resolved Mailgun configuration
|
|
160
|
+
* @returns FormData object ready for Mailgun API submission
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* const formData = convertMessage(message, config);
|
|
165
|
+
* const response = await fetch(url, { method: 'POST', body: formData });
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
function convertMessage(message, config) {
|
|
169
|
+
const formData = new FormData();
|
|
170
|
+
formData.append("from", formatAddress(message.sender));
|
|
171
|
+
for (const recipient of message.recipients) formData.append("to", formatAddress(recipient));
|
|
172
|
+
for (const ccRecipient of message.ccRecipients) formData.append("cc", formatAddress(ccRecipient));
|
|
173
|
+
for (const bccRecipient of message.bccRecipients) formData.append("bcc", formatAddress(bccRecipient));
|
|
174
|
+
if (message.replyRecipients.length > 0) {
|
|
175
|
+
const replyTo = message.replyRecipients.map(formatAddress).join(", ");
|
|
176
|
+
formData.append("h:Reply-To", replyTo);
|
|
177
|
+
}
|
|
178
|
+
formData.append("subject", message.subject);
|
|
179
|
+
if ("html" in message.content) {
|
|
180
|
+
formData.append("html", message.content.html);
|
|
181
|
+
if (message.content.text) formData.append("text", message.content.text);
|
|
182
|
+
} else formData.append("text", message.content.text);
|
|
183
|
+
if (message.priority !== "normal") {
|
|
184
|
+
const priorityMap = {
|
|
185
|
+
"high": "1",
|
|
186
|
+
"normal": "3",
|
|
187
|
+
"low": "5"
|
|
188
|
+
};
|
|
189
|
+
formData.append("h:X-Priority", priorityMap[message.priority]);
|
|
190
|
+
}
|
|
191
|
+
for (const tag of message.tags) formData.append("o:tag", tag);
|
|
192
|
+
for (const [key, value] of message.headers.entries()) if (!isStandardHeader(key)) formData.append(`h:${key}`, value);
|
|
193
|
+
for (const attachment of message.attachments) appendAttachment(formData, attachment);
|
|
194
|
+
if (config.tracking !== void 0) formData.append("o:tracking", config.tracking ? "yes" : "no");
|
|
195
|
+
if (config.clickTracking !== void 0) formData.append("o:tracking-clicks", config.clickTracking ? "yes" : "no");
|
|
196
|
+
if (config.openTracking !== void 0) formData.append("o:tracking-opens", config.openTracking ? "yes" : "no");
|
|
197
|
+
return formData;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Formats an address for Mailgun API.
|
|
201
|
+
*
|
|
202
|
+
* @param address - The address to format
|
|
203
|
+
* @returns Formatted address string
|
|
204
|
+
*/
|
|
205
|
+
function formatAddress(address) {
|
|
206
|
+
if (address.name) {
|
|
207
|
+
const escapedName = address.name.replace(/"/g, "\\\"");
|
|
208
|
+
return `"${escapedName}" <${address.address}>`;
|
|
209
|
+
}
|
|
210
|
+
return address.address;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Appends an attachment to the FormData.
|
|
214
|
+
*
|
|
215
|
+
* @param formData - The FormData to append to
|
|
216
|
+
* @param attachment - The attachment to append
|
|
217
|
+
*/
|
|
218
|
+
function appendAttachment(formData, attachment) {
|
|
219
|
+
const blob = new Blob([attachment.content], { type: attachment.contentType });
|
|
220
|
+
if (attachment.contentId) formData.append("inline", blob, attachment.filename);
|
|
221
|
+
else formData.append("attachment", blob, attachment.filename);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Checks if a header is a standard email header that should not be prefixed with 'h:'.
|
|
225
|
+
*
|
|
226
|
+
* @param headerName - The header name to check
|
|
227
|
+
* @returns True if it's a standard header
|
|
228
|
+
*/
|
|
229
|
+
function isStandardHeader(headerName) {
|
|
230
|
+
const standardHeaders = [
|
|
231
|
+
"from",
|
|
232
|
+
"to",
|
|
233
|
+
"cc",
|
|
234
|
+
"bcc",
|
|
235
|
+
"reply-to",
|
|
236
|
+
"subject",
|
|
237
|
+
"date",
|
|
238
|
+
"message-id",
|
|
239
|
+
"content-type",
|
|
240
|
+
"content-transfer-encoding",
|
|
241
|
+
"mime-version"
|
|
242
|
+
];
|
|
243
|
+
return standardHeaders.includes(headerName.toLowerCase());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/mailgun-transport.ts
|
|
248
|
+
/**
|
|
249
|
+
* Mailgun transport implementation for sending emails via Mailgun API.
|
|
250
|
+
*
|
|
251
|
+
* This transport provides efficient email delivery using Mailgun's HTTP API,
|
|
252
|
+
* with support for authentication, retry logic, and batch sending capabilities.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* import { MailgunTransport } from '@upyo/mailgun';
|
|
257
|
+
*
|
|
258
|
+
* const transport = new MailgunTransport({
|
|
259
|
+
* apiKey: 'your-api-key',
|
|
260
|
+
* domain: 'your-domain.com',
|
|
261
|
+
* region: 'us' // or 'eu'
|
|
262
|
+
* });
|
|
263
|
+
*
|
|
264
|
+
* const receipt = await transport.send(message);
|
|
265
|
+
* console.log('Message sent:', receipt.messageId);
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
var MailgunTransport = class {
|
|
269
|
+
config;
|
|
270
|
+
httpClient;
|
|
271
|
+
/**
|
|
272
|
+
* Creates a new Mailgun transport instance.
|
|
273
|
+
*
|
|
274
|
+
* @param config Mailgun configuration including API key, domain, and options.
|
|
275
|
+
*/
|
|
276
|
+
constructor(config) {
|
|
277
|
+
this.config = createMailgunConfig(config);
|
|
278
|
+
this.httpClient = new MailgunHttpClient(this.config);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Sends a single email message via Mailgun API.
|
|
282
|
+
*
|
|
283
|
+
* This method converts the message to Mailgun format, makes an HTTP request
|
|
284
|
+
* to the Mailgun API, and returns a receipt with the result.
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* const receipt = await transport.send({
|
|
289
|
+
* sender: { address: 'from@example.com' },
|
|
290
|
+
* recipients: [{ address: 'to@example.com' }],
|
|
291
|
+
* ccRecipients: [],
|
|
292
|
+
* bccRecipients: [],
|
|
293
|
+
* replyRecipients: [],
|
|
294
|
+
* subject: 'Hello',
|
|
295
|
+
* content: { text: 'Hello World!' },
|
|
296
|
+
* attachments: [],
|
|
297
|
+
* priority: 'normal',
|
|
298
|
+
* tags: [],
|
|
299
|
+
* headers: new Headers()
|
|
300
|
+
* });
|
|
301
|
+
*
|
|
302
|
+
* if (receipt.successful) {
|
|
303
|
+
* console.log('Message sent with ID:', receipt.messageId);
|
|
304
|
+
* }
|
|
305
|
+
* ```
|
|
306
|
+
*
|
|
307
|
+
* @param message The email message to send.
|
|
308
|
+
* @param options Optional transport options including `AbortSignal` for
|
|
309
|
+
* cancellation.
|
|
310
|
+
* @returns A promise that resolves to a receipt indicating success or
|
|
311
|
+
* failure.
|
|
312
|
+
*/
|
|
313
|
+
async send(message, options) {
|
|
314
|
+
options?.signal?.throwIfAborted();
|
|
315
|
+
try {
|
|
316
|
+
const formData = convertMessage(message, this.config);
|
|
317
|
+
options?.signal?.throwIfAborted();
|
|
318
|
+
const response = await this.httpClient.sendMessage(formData, options?.signal);
|
|
319
|
+
return {
|
|
320
|
+
messageId: response.id,
|
|
321
|
+
errorMessages: [],
|
|
322
|
+
successful: true
|
|
323
|
+
};
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
326
|
+
return {
|
|
327
|
+
messageId: "",
|
|
328
|
+
errorMessages: [errorMessage],
|
|
329
|
+
successful: false
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Sends multiple email messages efficiently via Mailgun API.
|
|
335
|
+
*
|
|
336
|
+
* This method sends each message individually but provides a streamlined
|
|
337
|
+
* interface for processing multiple messages. Each message is sent as a
|
|
338
|
+
* separate API request to Mailgun.
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```typescript
|
|
342
|
+
* const messages = [
|
|
343
|
+
* {
|
|
344
|
+
* sender: { address: 'from@example.com' },
|
|
345
|
+
* recipients: [{ address: 'user1@example.com' }],
|
|
346
|
+
* ccRecipients: [],
|
|
347
|
+
* bccRecipients: [],
|
|
348
|
+
* replyRecipients: [],
|
|
349
|
+
* subject: 'Message 1',
|
|
350
|
+
* content: { text: 'Hello User 1!' },
|
|
351
|
+
* attachments: [],
|
|
352
|
+
* priority: 'normal',
|
|
353
|
+
* tags: [],
|
|
354
|
+
* headers: new Headers()
|
|
355
|
+
* },
|
|
356
|
+
* {
|
|
357
|
+
* sender: { address: 'from@example.com' },
|
|
358
|
+
* recipients: [{ address: 'user2@example.com' }],
|
|
359
|
+
* ccRecipients: [],
|
|
360
|
+
* bccRecipients: [],
|
|
361
|
+
* replyRecipients: [],
|
|
362
|
+
* subject: 'Message 2',
|
|
363
|
+
* content: { text: 'Hello User 2!' },
|
|
364
|
+
* attachments: [],
|
|
365
|
+
* priority: 'normal',
|
|
366
|
+
* tags: [],
|
|
367
|
+
* headers: new Headers()
|
|
368
|
+
* }
|
|
369
|
+
* ];
|
|
370
|
+
*
|
|
371
|
+
* for await (const receipt of transport.sendMany(messages)) {
|
|
372
|
+
* if (receipt.successful) {
|
|
373
|
+
* console.log('Sent:', receipt.messageId);
|
|
374
|
+
* } else {
|
|
375
|
+
* console.error('Failed:', receipt.errorMessages);
|
|
376
|
+
* }
|
|
377
|
+
* }
|
|
378
|
+
* ```
|
|
379
|
+
*
|
|
380
|
+
* @param messages An iterable or async iterable of messages to send.
|
|
381
|
+
* @param options Optional transport options including `AbortSignal` for
|
|
382
|
+
* cancellation.
|
|
383
|
+
* @returns An async iterable of receipts, one for each message.
|
|
384
|
+
*/
|
|
385
|
+
async *sendMany(messages, options) {
|
|
386
|
+
options?.signal?.throwIfAborted();
|
|
387
|
+
const isAsyncIterable = Symbol.asyncIterator in messages;
|
|
388
|
+
if (isAsyncIterable) for await (const message of messages) {
|
|
389
|
+
options?.signal?.throwIfAborted();
|
|
390
|
+
yield await this.send(message, options);
|
|
391
|
+
}
|
|
392
|
+
else for (const message of messages) {
|
|
393
|
+
options?.signal?.throwIfAborted();
|
|
394
|
+
yield await this.send(message, options);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
//#endregion
|
|
400
|
+
export { MailgunApiError, MailgunTransport, createMailgunConfig };
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@upyo/mailgun",
|
|
3
|
+
"version": "0.1.0-dev.10+c222d7c6",
|
|
4
|
+
"description": "Mailgun transport for Upyo email library",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"email",
|
|
7
|
+
"mail",
|
|
8
|
+
"sendmail",
|
|
9
|
+
"mailgun"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Hong Minhee",
|
|
14
|
+
"email": "hong@minhee.org",
|
|
15
|
+
"url": "https://hongminhee.org/"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://upyo.org/",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/dahlia/upyo.git",
|
|
21
|
+
"directory": "packages/mailgun/"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/dahlia/upyo/issues"
|
|
25
|
+
},
|
|
26
|
+
"funding": [
|
|
27
|
+
"https://github.com/sponsors/dahlia"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.0.0",
|
|
31
|
+
"bun": ">=1.2.0",
|
|
32
|
+
"deno": ">=2.3.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist/",
|
|
36
|
+
"package.json",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"type": "module",
|
|
40
|
+
"module": "./dist/index.js",
|
|
41
|
+
"main": "./dist/index.cjs",
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": {
|
|
46
|
+
"import": "./dist/index.d.ts",
|
|
47
|
+
"require": "./dist/index.d.cts"
|
|
48
|
+
},
|
|
49
|
+
"import": "./dist/index.js",
|
|
50
|
+
"require": "./dist/index.cjs"
|
|
51
|
+
},
|
|
52
|
+
"./package.json": "./package.json"
|
|
53
|
+
},
|
|
54
|
+
"sideEffects": false,
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@upyo/core": "0.1.0-dev.10+c222d7c6"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@dotenvx/dotenvx": "^1.47.3",
|
|
60
|
+
"tsdown": "^0.12.7",
|
|
61
|
+
"typescript": "5.8.3"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsdown",
|
|
65
|
+
"prepublish": "tsdown",
|
|
66
|
+
"test": "tsdown && dotenvx run --ignore=MISSING_ENV_FILE -- node --experimental-transform-types --test",
|
|
67
|
+
"test:bun": "tsdown && bun test --timeout=30000 --env-file=.env",
|
|
68
|
+
"test:deno": "deno test --allow-env --allow-net --env-file=.env"
|
|
69
|
+
}
|
|
70
|
+
}
|