@upyo/plunk 0.5.0-dev.87 → 0.5.1-dev.259
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 +1 -1
- package/dist/index.cjs +126 -44
- package/dist/index.d.cts +15 -12
- package/dist/index.d.ts +15 -12
- package/dist/index.js +104 -44
- package/package.json +3 -8
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ See the [Plunk docs] for more information about configuration options.
|
|
|
72
72
|
### Available Options
|
|
73
73
|
|
|
74
74
|
- `apiKey`: Your Plunk API key
|
|
75
|
-
- `baseUrl`: API base URL (default: `https://api.useplunk.com`)
|
|
75
|
+
- `baseUrl`: API base URL (default: `https://next-api.useplunk.com`)
|
|
76
76
|
- `timeout`: Request timeout in milliseconds (default: `30000`)
|
|
77
77
|
- `retries`: Number of retry attempts (default: `3`)
|
|
78
78
|
- `validateSsl`: Whether to validate SSL certificates (default: `true`)
|
package/dist/index.cjs
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
const __upyo_core = __toESM(require("@upyo/core"));
|
|
1
25
|
|
|
2
26
|
//#region src/config.ts
|
|
3
27
|
/**
|
|
@@ -14,7 +38,7 @@
|
|
|
14
38
|
function createPlunkConfig(config) {
|
|
15
39
|
return {
|
|
16
40
|
apiKey: config.apiKey,
|
|
17
|
-
baseUrl: config.baseUrl ?? "https://api.useplunk.com",
|
|
41
|
+
baseUrl: config.baseUrl ?? "https://next-api.useplunk.com",
|
|
18
42
|
timeout: config.timeout ?? 3e4,
|
|
19
43
|
retries: config.retries ?? 3,
|
|
20
44
|
validateSsl: config.validateSsl ?? true,
|
|
@@ -25,6 +49,40 @@ function createPlunkConfig(config) {
|
|
|
25
49
|
//#endregion
|
|
26
50
|
//#region src/http-client.ts
|
|
27
51
|
/**
|
|
52
|
+
* Error thrown when a Plunk API request fails.
|
|
53
|
+
*
|
|
54
|
+
* @since 0.5.0
|
|
55
|
+
*/
|
|
56
|
+
var PlunkApiError = class extends Error {
|
|
57
|
+
/**
|
|
58
|
+
* HTTP status code returned by Plunk, if the request reached the API.
|
|
59
|
+
*/
|
|
60
|
+
statusCode;
|
|
61
|
+
/**
|
|
62
|
+
* Retry delay from Plunk's `Retry-After` response header.
|
|
63
|
+
*/
|
|
64
|
+
retryAfterMilliseconds;
|
|
65
|
+
/**
|
|
66
|
+
* Number of attempts made before this error was produced.
|
|
67
|
+
*/
|
|
68
|
+
attempts;
|
|
69
|
+
/**
|
|
70
|
+
* Creates a Plunk API error.
|
|
71
|
+
*
|
|
72
|
+
* @param message Error message.
|
|
73
|
+
* @param statusCode HTTP status code returned by Plunk.
|
|
74
|
+
* @param retryAfterMilliseconds Retry delay from the response.
|
|
75
|
+
* @param attempts Number of attempts made before this error.
|
|
76
|
+
*/
|
|
77
|
+
constructor(message, statusCode, retryAfterMilliseconds, attempts) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "PlunkApiError";
|
|
80
|
+
this.statusCode = statusCode;
|
|
81
|
+
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
|
82
|
+
this.attempts = attempts;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
28
86
|
* HTTP client wrapper for Plunk API requests.
|
|
29
87
|
*
|
|
30
88
|
* This class handles authentication, request formatting, error handling,
|
|
@@ -60,18 +118,18 @@ var PlunkHttpClient = class {
|
|
|
60
118
|
const response = await this.makeRequest(url, emailData, signal);
|
|
61
119
|
return await this.parseResponse(response);
|
|
62
120
|
} catch (error) {
|
|
121
|
+
if (isCallerAbort$1(error, signal)) throw error;
|
|
63
122
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
64
|
-
if (error instanceof Error)
|
|
65
|
-
|
|
66
|
-
if (error.message.includes("status: 4")) throw this.createPlunkError(error.message, 400);
|
|
67
|
-
}
|
|
123
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
124
|
+
if (error instanceof PlunkApiError && error.statusCode !== void 0 && error.statusCode >= 400 && error.statusCode < 500) throw new PlunkApiError(error.message, error.statusCode, error.retryAfterMilliseconds, attempt + 1);
|
|
68
125
|
if (attempt === this.config.retries) break;
|
|
69
126
|
const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
|
|
70
127
|
await this.sleep(delay);
|
|
71
128
|
}
|
|
72
129
|
}
|
|
73
130
|
const errorMessage = lastError?.message ?? "Unknown error occurred";
|
|
74
|
-
throw this.
|
|
131
|
+
if (lastError instanceof PlunkApiError) throw new PlunkApiError(lastError.message, lastError.statusCode, lastError.retryAfterMilliseconds, this.config.retries + 1);
|
|
132
|
+
throw new PlunkApiError(errorMessage, void 0, void 0, this.config.retries + 1);
|
|
75
133
|
}
|
|
76
134
|
/**
|
|
77
135
|
* Makes an HTTP request to the Plunk API.
|
|
@@ -87,13 +145,24 @@ var PlunkHttpClient = class {
|
|
|
87
145
|
"Content-Type": "application/json",
|
|
88
146
|
...this.config.headers
|
|
89
147
|
};
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
148
|
+
const timeoutController = new AbortController();
|
|
149
|
+
const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
|
|
150
|
+
const combinedSignal = (0, __upyo_core.combineSignals)(timeoutController.signal, signal);
|
|
151
|
+
let response;
|
|
152
|
+
try {
|
|
153
|
+
response = await fetch(url, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers,
|
|
156
|
+
body: JSON.stringify(emailData),
|
|
157
|
+
signal: combinedSignal.signal
|
|
158
|
+
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (isAbortError$1(error) && timeoutController.signal.aborted && !signal?.aborted) throw new Error(`Plunk API request timed out after ${this.config.timeout} ms.`);
|
|
161
|
+
throw error;
|
|
162
|
+
} finally {
|
|
163
|
+
combinedSignal.cleanup();
|
|
164
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
165
|
+
}
|
|
97
166
|
if (!response.ok) {
|
|
98
167
|
let errorBody;
|
|
99
168
|
try {
|
|
@@ -101,7 +170,7 @@ var PlunkHttpClient = class {
|
|
|
101
170
|
} catch {
|
|
102
171
|
errorBody = "Failed to read error response";
|
|
103
172
|
}
|
|
104
|
-
throw new
|
|
173
|
+
throw new PlunkApiError(`HTTP ${response.status}: ${response.statusText}. ${truncateErrorBody(errorBody)}`, response.status, (0, __upyo_core.parseRetryAfter)(response.headers.get("Retry-After")));
|
|
105
174
|
}
|
|
106
175
|
return response;
|
|
107
176
|
}
|
|
@@ -117,26 +186,19 @@ var PlunkHttpClient = class {
|
|
|
117
186
|
if (typeof data !== "object" || data === null) throw new Error("Invalid response format: expected object");
|
|
118
187
|
if (typeof data.success !== "boolean") throw new Error("Invalid response format: missing success field");
|
|
119
188
|
if (!data.success) throw new Error(data.message ?? "Send operation failed without error details");
|
|
120
|
-
|
|
189
|
+
const responseData = "data" in data ? data.data : data;
|
|
190
|
+
if (typeof responseData !== "object" || responseData === null) throw new Error("Invalid response format: missing data field");
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
emails: responseData.emails,
|
|
194
|
+
timestamp: responseData.timestamp
|
|
195
|
+
};
|
|
121
196
|
} catch (error) {
|
|
122
197
|
if (error instanceof SyntaxError) throw new Error("Invalid JSON response from Plunk API");
|
|
123
198
|
throw error;
|
|
124
199
|
}
|
|
125
200
|
}
|
|
126
201
|
/**
|
|
127
|
-
* Creates a PlunkError from an error message and optional status code.
|
|
128
|
-
*
|
|
129
|
-
* @param message - The error message
|
|
130
|
-
* @param statusCode - Optional HTTP status code
|
|
131
|
-
* @returns PlunkError instance
|
|
132
|
-
*/
|
|
133
|
-
createPlunkError(message, statusCode) {
|
|
134
|
-
return {
|
|
135
|
-
message,
|
|
136
|
-
statusCode
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
202
|
* Sleeps for the specified number of milliseconds.
|
|
141
203
|
*
|
|
142
204
|
* @param ms - Milliseconds to sleep
|
|
@@ -146,6 +208,15 @@ var PlunkHttpClient = class {
|
|
|
146
208
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
147
209
|
}
|
|
148
210
|
};
|
|
211
|
+
function isAbortError$1(error) {
|
|
212
|
+
return error instanceof Error && error.name === "AbortError";
|
|
213
|
+
}
|
|
214
|
+
function isCallerAbort$1(error, signal) {
|
|
215
|
+
return signal?.aborted === true && (isAbortError$1(error) || error === signal.reason);
|
|
216
|
+
}
|
|
217
|
+
function truncateErrorBody(text) {
|
|
218
|
+
return text.length > 500 ? `${text.slice(0, 500)}...` : text;
|
|
219
|
+
}
|
|
149
220
|
|
|
150
221
|
//#endregion
|
|
151
222
|
//#region src/message-converter.ts
|
|
@@ -191,8 +262,7 @@ async function convertMessage(message, _config) {
|
|
|
191
262
|
const plunkEmail = {
|
|
192
263
|
to,
|
|
193
264
|
subject: message.subject,
|
|
194
|
-
body
|
|
195
|
-
subscribed: false
|
|
265
|
+
body
|
|
196
266
|
};
|
|
197
267
|
if (senderName) plunkEmail.name = senderName;
|
|
198
268
|
if (senderEmail) plunkEmail.from = senderEmail;
|
|
@@ -214,7 +284,7 @@ async function convertAttachment(attachment) {
|
|
|
214
284
|
return {
|
|
215
285
|
filename: attachment.filename,
|
|
216
286
|
content: base64Content,
|
|
217
|
-
|
|
287
|
+
contentType: attachment.contentType
|
|
218
288
|
};
|
|
219
289
|
} catch (error) {
|
|
220
290
|
console.warn(`Failed to convert attachment ${attachment.filename}:`, error);
|
|
@@ -263,7 +333,7 @@ function arrayBufferToBase64(buffer) {
|
|
|
263
333
|
*
|
|
264
334
|
* const transport = new PlunkTransport({
|
|
265
335
|
* apiKey: 'your-plunk-api-key',
|
|
266
|
-
* baseUrl: 'https://api.useplunk.com', // or self-hosted URL
|
|
336
|
+
* baseUrl: 'https://next-api.useplunk.com', // or self-hosted URL
|
|
267
337
|
* timeout: 30000,
|
|
268
338
|
* retries: 3
|
|
269
339
|
* });
|
|
@@ -277,6 +347,7 @@ function arrayBufferToBase64(buffer) {
|
|
|
277
347
|
* ```
|
|
278
348
|
*/
|
|
279
349
|
var PlunkTransport = class {
|
|
350
|
+
id = "plunk";
|
|
280
351
|
/**
|
|
281
352
|
* The resolved Plunk configuration used by this transport.
|
|
282
353
|
*/
|
|
@@ -333,14 +404,13 @@ var PlunkTransport = class {
|
|
|
333
404
|
const messageId = this.extractMessageId(response, message);
|
|
334
405
|
return {
|
|
335
406
|
successful: true,
|
|
336
|
-
messageId
|
|
407
|
+
messageId,
|
|
408
|
+
provider: "plunk"
|
|
337
409
|
};
|
|
338
410
|
} catch (error) {
|
|
411
|
+
if (isCallerAbort(error, options?.signal)) throw error;
|
|
339
412
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
340
|
-
return
|
|
341
|
-
successful: false,
|
|
342
|
-
errorMessages: [errorMessage]
|
|
343
|
-
};
|
|
413
|
+
return createPlunkFailure(errorMessage, error);
|
|
344
414
|
}
|
|
345
415
|
}
|
|
346
416
|
/**
|
|
@@ -410,25 +480,37 @@ var PlunkTransport = class {
|
|
|
410
480
|
/**
|
|
411
481
|
* Extracts or generates a message ID from the Plunk response.
|
|
412
482
|
*
|
|
413
|
-
* Plunk returns email
|
|
414
|
-
*
|
|
483
|
+
* Plunk returns its email record ID in the response. This ID can be used to
|
|
484
|
+
* correlate the send operation with webhook events.
|
|
415
485
|
*
|
|
416
486
|
* @param response The Plunk API response.
|
|
417
487
|
* @param message The original message for fallback ID generation.
|
|
418
488
|
* @returns A message ID string.
|
|
419
489
|
*/
|
|
420
490
|
extractMessageId(response, message) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const timestamp$1 = response.timestamp;
|
|
424
|
-
if (contactId && timestamp$1) return `plunk-${contactId}-${new Date(timestamp$1).getTime()}`;
|
|
425
|
-
}
|
|
491
|
+
const emailId = response.emails?.[0]?.email;
|
|
492
|
+
if (emailId) return emailId;
|
|
426
493
|
const timestamp = Date.now();
|
|
427
494
|
const recipientHash = message.recipients[0]?.address.split("@")[0].substring(0, 8) ?? "unknown";
|
|
428
495
|
const random = Math.random().toString(36).substring(2, 8);
|
|
429
496
|
return `plunk-${timestamp}-${recipientHash}-${random}`;
|
|
430
497
|
}
|
|
431
498
|
};
|
|
499
|
+
function createPlunkFailure(message, error) {
|
|
500
|
+
if (error instanceof PlunkApiError) return (0, __upyo_core.createFailedReceipt)(message, {
|
|
501
|
+
provider: "plunk",
|
|
502
|
+
statusCode: error.statusCode,
|
|
503
|
+
retryAfterMilliseconds: error.retryAfterMilliseconds,
|
|
504
|
+
attempts: error.attempts
|
|
505
|
+
});
|
|
506
|
+
return (0, __upyo_core.createFailedReceipt)(message, { provider: "plunk" });
|
|
507
|
+
}
|
|
508
|
+
function isAbortError(error) {
|
|
509
|
+
return error instanceof Error && error.name === "AbortError";
|
|
510
|
+
}
|
|
511
|
+
function isCallerAbort(error, signal) {
|
|
512
|
+
return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
|
|
513
|
+
}
|
|
432
514
|
|
|
433
515
|
//#endregion
|
|
434
516
|
exports.PlunkTransport = PlunkTransport;
|
package/dist/index.d.cts
CHANGED
|
@@ -13,7 +13,7 @@ import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
|
|
|
13
13
|
* ```typescript
|
|
14
14
|
* const config: PlunkConfig = {
|
|
15
15
|
* apiKey: 'your-api-key',
|
|
16
|
-
* baseUrl: 'https://api.useplunk.com', // or self-hosted URL
|
|
16
|
+
* baseUrl: 'https://next-api.useplunk.com', // or self-hosted URL
|
|
17
17
|
* timeout: 30000,
|
|
18
18
|
* retries: 3
|
|
19
19
|
* };
|
|
@@ -30,11 +30,11 @@ interface PlunkConfig {
|
|
|
30
30
|
/**
|
|
31
31
|
* Base URL for the Plunk API.
|
|
32
32
|
*
|
|
33
|
-
* For Plunk's hosted service, use "https://api.useplunk.com" (default).
|
|
33
|
+
* For Plunk's hosted service, use "https://next-api.useplunk.com" (default).
|
|
34
34
|
* For self-hosted instances, use your domain with "/api" path
|
|
35
35
|
* (e.g., "https://plunk.example.com/api").
|
|
36
36
|
*
|
|
37
|
-
* @default "https://api.useplunk.com"
|
|
37
|
+
* @default "https://next-api.useplunk.com"
|
|
38
38
|
*/
|
|
39
39
|
readonly baseUrl?: string;
|
|
40
40
|
/**
|
|
@@ -93,7 +93,7 @@ type ResolvedPlunkConfig = Required<PlunkConfig>;
|
|
|
93
93
|
*
|
|
94
94
|
* const transport = new PlunkTransport({
|
|
95
95
|
* apiKey: 'your-plunk-api-key',
|
|
96
|
-
* baseUrl: 'https://api.useplunk.com', // or self-hosted URL
|
|
96
|
+
* baseUrl: 'https://next-api.useplunk.com', // or self-hosted URL
|
|
97
97
|
* timeout: 30000,
|
|
98
98
|
* retries: 3
|
|
99
99
|
* });
|
|
@@ -106,7 +106,8 @@ type ResolvedPlunkConfig = Required<PlunkConfig>;
|
|
|
106
106
|
* }
|
|
107
107
|
* ```
|
|
108
108
|
*/
|
|
109
|
-
declare class PlunkTransport implements Transport {
|
|
109
|
+
declare class PlunkTransport implements Transport<"plunk"> {
|
|
110
|
+
readonly id = "plunk";
|
|
110
111
|
/**
|
|
111
112
|
* The resolved Plunk configuration used by this transport.
|
|
112
113
|
*/
|
|
@@ -151,7 +152,7 @@ declare class PlunkTransport implements Transport {
|
|
|
151
152
|
* @returns A promise that resolves to a receipt indicating success or
|
|
152
153
|
* failure.
|
|
153
154
|
*/
|
|
154
|
-
send(message: Message, options?: TransportOptions): Promise<Receipt
|
|
155
|
+
send(message: Message, options?: TransportOptions): Promise<Receipt<"plunk">>;
|
|
155
156
|
/**
|
|
156
157
|
* Sends multiple email messages efficiently via Plunk API.
|
|
157
158
|
*
|
|
@@ -204,12 +205,12 @@ declare class PlunkTransport implements Transport {
|
|
|
204
205
|
* cancellation.
|
|
205
206
|
* @returns An async iterable of receipts, one for each message.
|
|
206
207
|
*/
|
|
207
|
-
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt
|
|
208
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"plunk">>;
|
|
208
209
|
/**
|
|
209
210
|
* Extracts or generates a message ID from the Plunk response.
|
|
210
211
|
*
|
|
211
|
-
* Plunk returns email
|
|
212
|
-
*
|
|
212
|
+
* Plunk returns its email record ID in the response. This ID can be used to
|
|
213
|
+
* correlate the send operation with webhook events.
|
|
213
214
|
*
|
|
214
215
|
* @param response The Plunk API response.
|
|
215
216
|
* @param message The original message for fallback ID generation.
|
|
@@ -235,6 +236,9 @@ interface PlunkResponse {
|
|
|
235
236
|
readonly id: string;
|
|
236
237
|
readonly email: string;
|
|
237
238
|
};
|
|
239
|
+
/**
|
|
240
|
+
* Plunk email record ID used to correlate webhook events.
|
|
241
|
+
*/
|
|
238
242
|
readonly email: string;
|
|
239
243
|
}[];
|
|
240
244
|
/**
|
|
@@ -260,10 +264,9 @@ interface PlunkError {
|
|
|
260
264
|
readonly details?: unknown;
|
|
261
265
|
}
|
|
262
266
|
/**
|
|
263
|
-
*
|
|
267
|
+
* Error thrown when a Plunk API request fails.
|
|
264
268
|
*
|
|
265
|
-
*
|
|
266
|
-
* and retry logic for the Plunk HTTP API.
|
|
269
|
+
* @since 0.5.0
|
|
267
270
|
*/
|
|
268
271
|
//#endregion
|
|
269
272
|
export { PlunkConfig, PlunkError, PlunkResponse, PlunkTransport, ResolvedPlunkConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
|
|
|
13
13
|
* ```typescript
|
|
14
14
|
* const config: PlunkConfig = {
|
|
15
15
|
* apiKey: 'your-api-key',
|
|
16
|
-
* baseUrl: 'https://api.useplunk.com', // or self-hosted URL
|
|
16
|
+
* baseUrl: 'https://next-api.useplunk.com', // or self-hosted URL
|
|
17
17
|
* timeout: 30000,
|
|
18
18
|
* retries: 3
|
|
19
19
|
* };
|
|
@@ -30,11 +30,11 @@ interface PlunkConfig {
|
|
|
30
30
|
/**
|
|
31
31
|
* Base URL for the Plunk API.
|
|
32
32
|
*
|
|
33
|
-
* For Plunk's hosted service, use "https://api.useplunk.com" (default).
|
|
33
|
+
* For Plunk's hosted service, use "https://next-api.useplunk.com" (default).
|
|
34
34
|
* For self-hosted instances, use your domain with "/api" path
|
|
35
35
|
* (e.g., "https://plunk.example.com/api").
|
|
36
36
|
*
|
|
37
|
-
* @default "https://api.useplunk.com"
|
|
37
|
+
* @default "https://next-api.useplunk.com"
|
|
38
38
|
*/
|
|
39
39
|
readonly baseUrl?: string;
|
|
40
40
|
/**
|
|
@@ -93,7 +93,7 @@ type ResolvedPlunkConfig = Required<PlunkConfig>;
|
|
|
93
93
|
*
|
|
94
94
|
* const transport = new PlunkTransport({
|
|
95
95
|
* apiKey: 'your-plunk-api-key',
|
|
96
|
-
* baseUrl: 'https://api.useplunk.com', // or self-hosted URL
|
|
96
|
+
* baseUrl: 'https://next-api.useplunk.com', // or self-hosted URL
|
|
97
97
|
* timeout: 30000,
|
|
98
98
|
* retries: 3
|
|
99
99
|
* });
|
|
@@ -106,7 +106,8 @@ type ResolvedPlunkConfig = Required<PlunkConfig>;
|
|
|
106
106
|
* }
|
|
107
107
|
* ```
|
|
108
108
|
*/
|
|
109
|
-
declare class PlunkTransport implements Transport {
|
|
109
|
+
declare class PlunkTransport implements Transport<"plunk"> {
|
|
110
|
+
readonly id = "plunk";
|
|
110
111
|
/**
|
|
111
112
|
* The resolved Plunk configuration used by this transport.
|
|
112
113
|
*/
|
|
@@ -151,7 +152,7 @@ declare class PlunkTransport implements Transport {
|
|
|
151
152
|
* @returns A promise that resolves to a receipt indicating success or
|
|
152
153
|
* failure.
|
|
153
154
|
*/
|
|
154
|
-
send(message: Message, options?: TransportOptions): Promise<Receipt
|
|
155
|
+
send(message: Message, options?: TransportOptions): Promise<Receipt<"plunk">>;
|
|
155
156
|
/**
|
|
156
157
|
* Sends multiple email messages efficiently via Plunk API.
|
|
157
158
|
*
|
|
@@ -204,12 +205,12 @@ declare class PlunkTransport implements Transport {
|
|
|
204
205
|
* cancellation.
|
|
205
206
|
* @returns An async iterable of receipts, one for each message.
|
|
206
207
|
*/
|
|
207
|
-
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt
|
|
208
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"plunk">>;
|
|
208
209
|
/**
|
|
209
210
|
* Extracts or generates a message ID from the Plunk response.
|
|
210
211
|
*
|
|
211
|
-
* Plunk returns email
|
|
212
|
-
*
|
|
212
|
+
* Plunk returns its email record ID in the response. This ID can be used to
|
|
213
|
+
* correlate the send operation with webhook events.
|
|
213
214
|
*
|
|
214
215
|
* @param response The Plunk API response.
|
|
215
216
|
* @param message The original message for fallback ID generation.
|
|
@@ -235,6 +236,9 @@ interface PlunkResponse {
|
|
|
235
236
|
readonly id: string;
|
|
236
237
|
readonly email: string;
|
|
237
238
|
};
|
|
239
|
+
/**
|
|
240
|
+
* Plunk email record ID used to correlate webhook events.
|
|
241
|
+
*/
|
|
238
242
|
readonly email: string;
|
|
239
243
|
}[];
|
|
240
244
|
/**
|
|
@@ -260,10 +264,9 @@ interface PlunkError {
|
|
|
260
264
|
readonly details?: unknown;
|
|
261
265
|
}
|
|
262
266
|
/**
|
|
263
|
-
*
|
|
267
|
+
* Error thrown when a Plunk API request fails.
|
|
264
268
|
*
|
|
265
|
-
*
|
|
266
|
-
* and retry logic for the Plunk HTTP API.
|
|
269
|
+
* @since 0.5.0
|
|
267
270
|
*/
|
|
268
271
|
//#endregion
|
|
269
272
|
export { PlunkConfig, PlunkError, PlunkResponse, PlunkTransport, ResolvedPlunkConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { combineSignals, createFailedReceipt, parseRetryAfter } from "@upyo/core";
|
|
2
|
+
|
|
1
3
|
//#region src/config.ts
|
|
2
4
|
/**
|
|
3
5
|
* Creates a resolved Plunk configuration by applying default values to optional fields.
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
function createPlunkConfig(config) {
|
|
14
16
|
return {
|
|
15
17
|
apiKey: config.apiKey,
|
|
16
|
-
baseUrl: config.baseUrl ?? "https://api.useplunk.com",
|
|
18
|
+
baseUrl: config.baseUrl ?? "https://next-api.useplunk.com",
|
|
17
19
|
timeout: config.timeout ?? 3e4,
|
|
18
20
|
retries: config.retries ?? 3,
|
|
19
21
|
validateSsl: config.validateSsl ?? true,
|
|
@@ -24,6 +26,40 @@ function createPlunkConfig(config) {
|
|
|
24
26
|
//#endregion
|
|
25
27
|
//#region src/http-client.ts
|
|
26
28
|
/**
|
|
29
|
+
* Error thrown when a Plunk API request fails.
|
|
30
|
+
*
|
|
31
|
+
* @since 0.5.0
|
|
32
|
+
*/
|
|
33
|
+
var PlunkApiError = class extends Error {
|
|
34
|
+
/**
|
|
35
|
+
* HTTP status code returned by Plunk, if the request reached the API.
|
|
36
|
+
*/
|
|
37
|
+
statusCode;
|
|
38
|
+
/**
|
|
39
|
+
* Retry delay from Plunk's `Retry-After` response header.
|
|
40
|
+
*/
|
|
41
|
+
retryAfterMilliseconds;
|
|
42
|
+
/**
|
|
43
|
+
* Number of attempts made before this error was produced.
|
|
44
|
+
*/
|
|
45
|
+
attempts;
|
|
46
|
+
/**
|
|
47
|
+
* Creates a Plunk API error.
|
|
48
|
+
*
|
|
49
|
+
* @param message Error message.
|
|
50
|
+
* @param statusCode HTTP status code returned by Plunk.
|
|
51
|
+
* @param retryAfterMilliseconds Retry delay from the response.
|
|
52
|
+
* @param attempts Number of attempts made before this error.
|
|
53
|
+
*/
|
|
54
|
+
constructor(message, statusCode, retryAfterMilliseconds, attempts) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "PlunkApiError";
|
|
57
|
+
this.statusCode = statusCode;
|
|
58
|
+
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
|
59
|
+
this.attempts = attempts;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
27
63
|
* HTTP client wrapper for Plunk API requests.
|
|
28
64
|
*
|
|
29
65
|
* This class handles authentication, request formatting, error handling,
|
|
@@ -59,18 +95,18 @@ var PlunkHttpClient = class {
|
|
|
59
95
|
const response = await this.makeRequest(url, emailData, signal);
|
|
60
96
|
return await this.parseResponse(response);
|
|
61
97
|
} catch (error) {
|
|
98
|
+
if (isCallerAbort$1(error, signal)) throw error;
|
|
62
99
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
63
|
-
if (error instanceof Error)
|
|
64
|
-
|
|
65
|
-
if (error.message.includes("status: 4")) throw this.createPlunkError(error.message, 400);
|
|
66
|
-
}
|
|
100
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
101
|
+
if (error instanceof PlunkApiError && error.statusCode !== void 0 && error.statusCode >= 400 && error.statusCode < 500) throw new PlunkApiError(error.message, error.statusCode, error.retryAfterMilliseconds, attempt + 1);
|
|
67
102
|
if (attempt === this.config.retries) break;
|
|
68
103
|
const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
|
|
69
104
|
await this.sleep(delay);
|
|
70
105
|
}
|
|
71
106
|
}
|
|
72
107
|
const errorMessage = lastError?.message ?? "Unknown error occurred";
|
|
73
|
-
throw this.
|
|
108
|
+
if (lastError instanceof PlunkApiError) throw new PlunkApiError(lastError.message, lastError.statusCode, lastError.retryAfterMilliseconds, this.config.retries + 1);
|
|
109
|
+
throw new PlunkApiError(errorMessage, void 0, void 0, this.config.retries + 1);
|
|
74
110
|
}
|
|
75
111
|
/**
|
|
76
112
|
* Makes an HTTP request to the Plunk API.
|
|
@@ -86,13 +122,24 @@ var PlunkHttpClient = class {
|
|
|
86
122
|
"Content-Type": "application/json",
|
|
87
123
|
...this.config.headers
|
|
88
124
|
};
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
const timeoutController = new AbortController();
|
|
126
|
+
const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
|
|
127
|
+
const combinedSignal = combineSignals(timeoutController.signal, signal);
|
|
128
|
+
let response;
|
|
129
|
+
try {
|
|
130
|
+
response = await fetch(url, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers,
|
|
133
|
+
body: JSON.stringify(emailData),
|
|
134
|
+
signal: combinedSignal.signal
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (isAbortError$1(error) && timeoutController.signal.aborted && !signal?.aborted) throw new Error(`Plunk API request timed out after ${this.config.timeout} ms.`);
|
|
138
|
+
throw error;
|
|
139
|
+
} finally {
|
|
140
|
+
combinedSignal.cleanup();
|
|
141
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
142
|
+
}
|
|
96
143
|
if (!response.ok) {
|
|
97
144
|
let errorBody;
|
|
98
145
|
try {
|
|
@@ -100,7 +147,7 @@ var PlunkHttpClient = class {
|
|
|
100
147
|
} catch {
|
|
101
148
|
errorBody = "Failed to read error response";
|
|
102
149
|
}
|
|
103
|
-
throw new
|
|
150
|
+
throw new PlunkApiError(`HTTP ${response.status}: ${response.statusText}. ${truncateErrorBody(errorBody)}`, response.status, parseRetryAfter(response.headers.get("Retry-After")));
|
|
104
151
|
}
|
|
105
152
|
return response;
|
|
106
153
|
}
|
|
@@ -116,26 +163,19 @@ var PlunkHttpClient = class {
|
|
|
116
163
|
if (typeof data !== "object" || data === null) throw new Error("Invalid response format: expected object");
|
|
117
164
|
if (typeof data.success !== "boolean") throw new Error("Invalid response format: missing success field");
|
|
118
165
|
if (!data.success) throw new Error(data.message ?? "Send operation failed without error details");
|
|
119
|
-
|
|
166
|
+
const responseData = "data" in data ? data.data : data;
|
|
167
|
+
if (typeof responseData !== "object" || responseData === null) throw new Error("Invalid response format: missing data field");
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
emails: responseData.emails,
|
|
171
|
+
timestamp: responseData.timestamp
|
|
172
|
+
};
|
|
120
173
|
} catch (error) {
|
|
121
174
|
if (error instanceof SyntaxError) throw new Error("Invalid JSON response from Plunk API");
|
|
122
175
|
throw error;
|
|
123
176
|
}
|
|
124
177
|
}
|
|
125
178
|
/**
|
|
126
|
-
* Creates a PlunkError from an error message and optional status code.
|
|
127
|
-
*
|
|
128
|
-
* @param message - The error message
|
|
129
|
-
* @param statusCode - Optional HTTP status code
|
|
130
|
-
* @returns PlunkError instance
|
|
131
|
-
*/
|
|
132
|
-
createPlunkError(message, statusCode) {
|
|
133
|
-
return {
|
|
134
|
-
message,
|
|
135
|
-
statusCode
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
179
|
* Sleeps for the specified number of milliseconds.
|
|
140
180
|
*
|
|
141
181
|
* @param ms - Milliseconds to sleep
|
|
@@ -145,6 +185,15 @@ var PlunkHttpClient = class {
|
|
|
145
185
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
146
186
|
}
|
|
147
187
|
};
|
|
188
|
+
function isAbortError$1(error) {
|
|
189
|
+
return error instanceof Error && error.name === "AbortError";
|
|
190
|
+
}
|
|
191
|
+
function isCallerAbort$1(error, signal) {
|
|
192
|
+
return signal?.aborted === true && (isAbortError$1(error) || error === signal.reason);
|
|
193
|
+
}
|
|
194
|
+
function truncateErrorBody(text) {
|
|
195
|
+
return text.length > 500 ? `${text.slice(0, 500)}...` : text;
|
|
196
|
+
}
|
|
148
197
|
|
|
149
198
|
//#endregion
|
|
150
199
|
//#region src/message-converter.ts
|
|
@@ -190,8 +239,7 @@ async function convertMessage(message, _config) {
|
|
|
190
239
|
const plunkEmail = {
|
|
191
240
|
to,
|
|
192
241
|
subject: message.subject,
|
|
193
|
-
body
|
|
194
|
-
subscribed: false
|
|
242
|
+
body
|
|
195
243
|
};
|
|
196
244
|
if (senderName) plunkEmail.name = senderName;
|
|
197
245
|
if (senderEmail) plunkEmail.from = senderEmail;
|
|
@@ -213,7 +261,7 @@ async function convertAttachment(attachment) {
|
|
|
213
261
|
return {
|
|
214
262
|
filename: attachment.filename,
|
|
215
263
|
content: base64Content,
|
|
216
|
-
|
|
264
|
+
contentType: attachment.contentType
|
|
217
265
|
};
|
|
218
266
|
} catch (error) {
|
|
219
267
|
console.warn(`Failed to convert attachment ${attachment.filename}:`, error);
|
|
@@ -262,7 +310,7 @@ function arrayBufferToBase64(buffer) {
|
|
|
262
310
|
*
|
|
263
311
|
* const transport = new PlunkTransport({
|
|
264
312
|
* apiKey: 'your-plunk-api-key',
|
|
265
|
-
* baseUrl: 'https://api.useplunk.com', // or self-hosted URL
|
|
313
|
+
* baseUrl: 'https://next-api.useplunk.com', // or self-hosted URL
|
|
266
314
|
* timeout: 30000,
|
|
267
315
|
* retries: 3
|
|
268
316
|
* });
|
|
@@ -276,6 +324,7 @@ function arrayBufferToBase64(buffer) {
|
|
|
276
324
|
* ```
|
|
277
325
|
*/
|
|
278
326
|
var PlunkTransport = class {
|
|
327
|
+
id = "plunk";
|
|
279
328
|
/**
|
|
280
329
|
* The resolved Plunk configuration used by this transport.
|
|
281
330
|
*/
|
|
@@ -332,14 +381,13 @@ var PlunkTransport = class {
|
|
|
332
381
|
const messageId = this.extractMessageId(response, message);
|
|
333
382
|
return {
|
|
334
383
|
successful: true,
|
|
335
|
-
messageId
|
|
384
|
+
messageId,
|
|
385
|
+
provider: "plunk"
|
|
336
386
|
};
|
|
337
387
|
} catch (error) {
|
|
388
|
+
if (isCallerAbort(error, options?.signal)) throw error;
|
|
338
389
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
339
|
-
return
|
|
340
|
-
successful: false,
|
|
341
|
-
errorMessages: [errorMessage]
|
|
342
|
-
};
|
|
390
|
+
return createPlunkFailure(errorMessage, error);
|
|
343
391
|
}
|
|
344
392
|
}
|
|
345
393
|
/**
|
|
@@ -409,25 +457,37 @@ var PlunkTransport = class {
|
|
|
409
457
|
/**
|
|
410
458
|
* Extracts or generates a message ID from the Plunk response.
|
|
411
459
|
*
|
|
412
|
-
* Plunk returns email
|
|
413
|
-
*
|
|
460
|
+
* Plunk returns its email record ID in the response. This ID can be used to
|
|
461
|
+
* correlate the send operation with webhook events.
|
|
414
462
|
*
|
|
415
463
|
* @param response The Plunk API response.
|
|
416
464
|
* @param message The original message for fallback ID generation.
|
|
417
465
|
* @returns A message ID string.
|
|
418
466
|
*/
|
|
419
467
|
extractMessageId(response, message) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const timestamp$1 = response.timestamp;
|
|
423
|
-
if (contactId && timestamp$1) return `plunk-${contactId}-${new Date(timestamp$1).getTime()}`;
|
|
424
|
-
}
|
|
468
|
+
const emailId = response.emails?.[0]?.email;
|
|
469
|
+
if (emailId) return emailId;
|
|
425
470
|
const timestamp = Date.now();
|
|
426
471
|
const recipientHash = message.recipients[0]?.address.split("@")[0].substring(0, 8) ?? "unknown";
|
|
427
472
|
const random = Math.random().toString(36).substring(2, 8);
|
|
428
473
|
return `plunk-${timestamp}-${recipientHash}-${random}`;
|
|
429
474
|
}
|
|
430
475
|
};
|
|
476
|
+
function createPlunkFailure(message, error) {
|
|
477
|
+
if (error instanceof PlunkApiError) return createFailedReceipt(message, {
|
|
478
|
+
provider: "plunk",
|
|
479
|
+
statusCode: error.statusCode,
|
|
480
|
+
retryAfterMilliseconds: error.retryAfterMilliseconds,
|
|
481
|
+
attempts: error.attempts
|
|
482
|
+
});
|
|
483
|
+
return createFailedReceipt(message, { provider: "plunk" });
|
|
484
|
+
}
|
|
485
|
+
function isAbortError(error) {
|
|
486
|
+
return error instanceof Error && error.name === "AbortError";
|
|
487
|
+
}
|
|
488
|
+
function isCallerAbort(error, signal) {
|
|
489
|
+
return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
|
|
490
|
+
}
|
|
431
491
|
|
|
432
492
|
//#endregion
|
|
433
493
|
export { PlunkTransport };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/plunk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-dev.259",
|
|
4
4
|
"description": "Plunk transport for Upyo email library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -53,18 +53,13 @@
|
|
|
53
53
|
},
|
|
54
54
|
"sideEffects": false,
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@upyo/core": "0.5.
|
|
56
|
+
"@upyo/core": "0.5.1-dev.259+e2930fa8"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@dotenvx/dotenvx": "^1.47.3",
|
|
60
59
|
"tsdown": "^0.12.7",
|
|
61
60
|
"typescript": "5.8.3"
|
|
62
61
|
},
|
|
63
62
|
"scripts": {
|
|
64
|
-
"
|
|
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"
|
|
63
|
+
"prepublish": "mise run --no-deps :build"
|
|
69
64
|
}
|
|
70
65
|
}
|