sently 0.7.2 → 0.9.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/AGENTS.md +34 -0
- package/CHANGELOG.md +49 -1
- package/README.md +89 -776
- package/dist/adapters/bun.js +1 -1
- package/dist/adapters/cf.js +2 -2
- package/dist/adapters/cf.js.map +2 -2
- package/dist/adapters/deno.js +1 -1
- package/dist/adapters/node.js +1 -1
- package/dist/auth/oauth2.js +2 -2
- package/dist/auth/oauth2.js.map +1 -1
- package/dist/chunk-0gqy32pe.js +4 -0
- package/dist/chunk-0gqy32pe.js.map +10 -0
- package/dist/{chunk-kebhy657.js → chunk-0qxws3kj.js} +2 -2
- package/dist/{chunk-kebhy657.js.map → chunk-0qxws3kj.js.map} +1 -1
- package/dist/chunk-29z7fkzy.js +4 -0
- package/dist/{chunk-74rha3ac.js.map → chunk-29z7fkzy.js.map} +2 -2
- package/dist/chunk-5915vbcj.js +4 -0
- package/dist/chunk-5915vbcj.js.map +10 -0
- package/dist/{chunk-keksa6fx.js → chunk-6npp3x3c.js} +2 -2
- package/dist/{chunk-keksa6fx.js.map → chunk-6npp3x3c.js.map} +1 -1
- package/dist/chunk-7gy2q9sh.js +4 -0
- package/dist/chunk-7gy2q9sh.js.map +10 -0
- package/dist/{chunk-gkkv7yqm.js → chunk-8dty8c4p.js} +3 -3
- package/dist/{chunk-gkkv7yqm.js.map → chunk-8dty8c4p.js.map} +2 -2
- package/dist/{chunk-qp4y0kp2.js → chunk-8njyga62.js} +2 -2
- package/dist/{chunk-qp4y0kp2.js.map → chunk-8njyga62.js.map} +1 -1
- package/dist/{chunk-gh5n759p.js → chunk-93vqxxj2.js} +3 -3
- package/dist/{chunk-gh5n759p.js.map → chunk-93vqxxj2.js.map} +1 -1
- package/dist/chunk-b602dhck.js +4 -0
- package/dist/{chunk-rm78s1ek.js.map → chunk-b602dhck.js.map} +2 -2
- package/dist/chunk-gp4fwrs8.js +4 -0
- package/dist/chunk-gp4fwrs8.js.map +10 -0
- package/dist/chunk-n0qeyzqm.js +13 -0
- package/dist/{chunk-cvrbfke9.js.map → chunk-n0qeyzqm.js.map} +2 -2
- package/dist/chunk-qtkd5bak.js +4 -0
- package/dist/chunk-qtkd5bak.js.map +11 -0
- package/dist/chunk-skqhj0rm.js +4 -0
- package/dist/{chunk-r7ee9tk7.js.map → chunk-skqhj0rm.js.map} +2 -2
- package/dist/{chunk-a1c87hn1.js → chunk-tamww15j.js} +2 -2
- package/dist/{chunk-a1c87hn1.js.map → chunk-tamww15j.js.map} +1 -1
- package/dist/chunk-vjpds2pf.js +3 -0
- package/dist/chunk-vjpds2pf.js.map +10 -0
- package/dist/chunk-yp311efr.js +4 -0
- package/dist/chunk-yp311efr.js.map +10 -0
- package/dist/chunk-ywq6s10g.js +5 -0
- package/dist/{chunk-d5rwm3br.js.map → chunk-ywq6s10g.js.map} +3 -3
- package/dist/core/base64.d.ts +10 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/core/hooks.d.ts +14 -0
- package/dist/core/plugin.d.ts +7 -7
- package/dist/core/provider-label.d.ts +6 -0
- package/dist/core/push-endpoint.d.ts +29 -0
- package/dist/core/push-types.d.ts +98 -0
- package/dist/core/sms-types.d.ts +76 -0
- package/dist/core/smtp.js +2 -2
- package/dist/core/smtp.js.map +1 -1
- package/dist/core/types.d.ts +27 -4
- package/dist/core/whatsapp-types.d.ts +116 -0
- package/dist/detect.js +2 -2
- package/dist/detect.js.map +1 -1
- package/dist/dkim.js +2 -2
- package/dist/dkim.js.map +2 -2
- package/dist/idempotency.d.ts +1 -0
- package/dist/idempotency.js +2 -2
- package/dist/idempotency.js.map +3 -3
- package/dist/index.d.ts +24 -2
- package/dist/index.js +23 -0
- package/dist/mailer.js +2 -2
- package/dist/mailer.js.map +1 -1
- package/dist/observability/console.d.ts +12 -0
- package/dist/observability/console.js +3 -0
- package/dist/observability/console.js.map +10 -0
- package/dist/plugins/react.js +2 -2
- package/dist/plugins/react.js.map +2 -2
- package/dist/plugins/template.js +1 -1
- package/dist/pool/pool.js +2 -2
- package/dist/pool/pool.js.map +1 -1
- package/dist/push.d.ts +26 -0
- package/dist/push.js +3 -0
- package/dist/push.js.map +10 -0
- package/dist/sms.d.ts +26 -0
- package/dist/sms.js +3 -0
- package/dist/sms.js.map +10 -0
- package/dist/smtp-mailer.js +2 -2
- package/dist/smtp-mailer.js.map +2 -2
- package/dist/transports/brevo.d.ts +1 -0
- package/dist/transports/brevo.js +2 -2
- package/dist/transports/brevo.js.map +3 -3
- package/dist/transports/cloudflare-email.d.ts +58 -0
- package/dist/transports/cloudflare-email.js +3 -0
- package/dist/transports/cloudflare-email.js.map +10 -0
- package/dist/transports/fallback.d.ts +100 -0
- package/dist/transports/fallback.js +3 -0
- package/dist/transports/fallback.js.map +9 -0
- package/dist/transports/loops.d.ts +32 -0
- package/dist/transports/loops.js +3 -0
- package/dist/transports/loops.js.map +10 -0
- package/dist/transports/mailersend.d.ts +27 -0
- package/dist/transports/mailersend.js +3 -0
- package/dist/transports/mailersend.js.map +10 -0
- package/dist/transports/mailgun.d.ts +1 -0
- package/dist/transports/mailgun.js +2 -2
- package/dist/transports/mailgun.js.map +3 -3
- package/dist/transports/mailtrap.d.ts +36 -0
- package/dist/transports/mailtrap.js +3 -0
- package/dist/transports/mailtrap.js.map +10 -0
- package/dist/transports/msegat.d.ts +132 -0
- package/dist/transports/msegat.js +3 -0
- package/dist/transports/msegat.js.map +10 -0
- package/dist/transports/plunk.d.ts +29 -0
- package/dist/transports/plunk.js +3 -0
- package/dist/transports/plunk.js.map +10 -0
- package/dist/transports/postmark.d.ts +1 -0
- package/dist/transports/postmark.js +2 -2
- package/dist/transports/postmark.js.map +3 -3
- package/dist/transports/preview.d.ts +1 -0
- package/dist/transports/preview.js +2 -2
- package/dist/transports/preview.js.map +3 -3
- package/dist/transports/resend.d.ts +1 -0
- package/dist/transports/resend.js +2 -2
- package/dist/transports/resend.js.map +3 -3
- package/dist/transports/retry.d.ts +1 -0
- package/dist/transports/retry.js +2 -2
- package/dist/transports/retry.js.map +3 -3
- package/dist/transports/sendgrid.d.ts +1 -0
- package/dist/transports/sendgrid.js +2 -2
- package/dist/transports/sendgrid.js.map +3 -3
- package/dist/transports/ses.d.ts +1 -0
- package/dist/transports/ses.js +3 -3
- package/dist/transports/ses.js.map +3 -3
- package/dist/transports/smtp.d.ts +1 -0
- package/dist/transports/smtp.js +2 -2
- package/dist/transports/smtp.js.map +1 -1
- package/dist/transports/sndr.d.ts +41 -0
- package/dist/transports/sndr.js +3 -0
- package/dist/transports/sndr.js.map +10 -0
- package/dist/transports/sparkpost.d.ts +30 -0
- package/dist/transports/sparkpost.js +3 -0
- package/dist/transports/sparkpost.js.map +10 -0
- package/dist/transports/taqnyat-mail.d.ts +33 -0
- package/dist/transports/taqnyat-mail.js +3 -0
- package/dist/transports/taqnyat-mail.js.map +10 -0
- package/dist/transports/taqnyat-phone.d.ts +4 -0
- package/dist/transports/taqnyat-sms.d.ts +131 -0
- package/dist/transports/taqnyat-sms.js +3 -0
- package/dist/transports/taqnyat-sms.js.map +10 -0
- package/dist/transports/taqnyat-whatsapp.d.ts +51 -0
- package/dist/transports/taqnyat-whatsapp.js +3 -0
- package/dist/transports/taqnyat-whatsapp.js.map +10 -0
- package/dist/transports/twilio-sms.d.ts +66 -0
- package/dist/transports/twilio-sms.js +3 -0
- package/dist/transports/twilio-sms.js.map +10 -0
- package/dist/transports/webpush.d.ts +46 -0
- package/dist/transports/webpush.js +3 -0
- package/dist/transports/webpush.js.map +10 -0
- package/dist/transports/weighted-fallback.d.ts +65 -0
- package/dist/transports/weighted-fallback.js +3 -0
- package/dist/transports/weighted-fallback.js.map +10 -0
- package/dist/transports/whatsapp-cloud.d.ts +56 -0
- package/dist/transports/whatsapp-cloud.js +3 -0
- package/dist/transports/whatsapp-cloud.js.map +10 -0
- package/dist/webhooks/brevo.js +1 -1
- package/dist/webhooks/mailgun.js +1 -1
- package/dist/webhooks/postmark.js +1 -1
- package/dist/webhooks/resend.js +1 -1
- package/dist/webhooks/sendgrid.js +1 -1
- package/dist/webhooks/ses.js +1 -1
- package/dist/webhooks/sndr.d.ts +16 -0
- package/dist/webhooks/sndr.js +3 -0
- package/dist/webhooks/sndr.js.map +10 -0
- package/dist/webhooks/timing-safe-equal.js +1 -1
- package/dist/webhooks.d.ts +9 -2
- package/dist/webhooks.js +4 -0
- package/dist/whatsapp.d.ts +26 -0
- package/dist/whatsapp.js +3 -0
- package/dist/whatsapp.js.map +10 -0
- package/package.json +121 -4
- package/site/README.md +26 -0
- package/site/content/docs/ai/index.mdx +13 -0
- package/site/content/docs/ai/llms-txt.mdx +12 -0
- package/site/content/docs/ai/mcp.mdx +16 -0
- package/site/content/docs/ai/meta.json +9 -0
- package/site/content/docs/channels/email.mdx +100 -0
- package/site/content/docs/channels/hooks.mdx +87 -0
- package/site/content/docs/channels/index.mdx +81 -0
- package/site/content/docs/channels/meta.json +12 -0
- package/site/content/docs/channels/push.mdx +92 -0
- package/site/content/docs/channels/sms.mdx +88 -0
- package/site/content/docs/channels/whatsapp.mdx +95 -0
- package/site/content/docs/decorators/fallback.mdx +34 -0
- package/site/content/docs/decorators/idempotency.mdx +34 -0
- package/site/content/docs/decorators/index.mdx +50 -0
- package/site/content/docs/decorators/meta.json +12 -0
- package/site/content/docs/decorators/preview.mdx +34 -0
- package/site/content/docs/decorators/retry.mdx +34 -0
- package/site/content/docs/decorators/weighted-fallback.mdx +34 -0
- package/site/content/docs/get-started/entrypoints.mdx +114 -0
- package/site/content/docs/get-started/index.mdx +14 -0
- package/site/content/docs/get-started/installation.mdx +78 -0
- package/site/content/docs/get-started/introduction.mdx +77 -0
- package/site/content/docs/get-started/meta.json +12 -0
- package/site/content/docs/get-started/migrate-nodemailer.mdx +23 -0
- package/site/content/docs/get-started/runtimes.mdx +17 -0
- package/site/content/docs/guides/adapters.mdx +26 -0
- package/site/content/docs/guides/attachments.mdx +19 -0
- package/site/content/docs/guides/bundle-size.mdx +78 -0
- package/site/content/docs/guides/dkim.mdx +26 -0
- package/site/content/docs/guides/index.mdx +15 -0
- package/site/content/docs/guides/meta.json +21 -0
- package/site/content/docs/guides/oauth2.mdx +23 -0
- package/site/content/docs/guides/observability.mdx +16 -0
- package/site/content/docs/guides/plugins-template.mdx +22 -0
- package/site/content/docs/guides/pool.mdx +24 -0
- package/site/content/docs/guides/react-email.mdx +20 -0
- package/site/content/docs/guides/security.mdx +17 -0
- package/site/content/docs/guides/send-bulk.mdx +18 -0
- package/site/content/docs/guides/vendor-extras-otp.mdx +18 -0
- package/site/content/docs/guides/webhooks.mdx +120 -0
- package/site/content/docs/guides/webpush-interop.mdx +18 -0
- package/site/content/docs/index.mdx +15 -0
- package/site/content/docs/meta.json +15 -0
- package/site/content/docs/quick-start/email.mdx +40 -0
- package/site/content/docs/quick-start/index.mdx +15 -0
- package/site/content/docs/quick-start/meta.json +5 -0
- package/site/content/docs/quick-start/push.mdx +36 -0
- package/site/content/docs/quick-start/sms.mdx +36 -0
- package/site/content/docs/quick-start/whatsapp.mdx +38 -0
- package/site/content/docs/reference/errors.mdx +22 -0
- package/site/content/docs/reference/exports.mdx +83 -0
- package/site/content/docs/reference/index.mdx +15 -0
- package/site/content/docs/reference/mail-options.mdx +22 -0
- package/site/content/docs/reference/meta.json +15 -0
- package/site/content/docs/reference/push-options.mdx +20 -0
- package/site/content/docs/reference/sms-options.mdx +17 -0
- package/site/content/docs/reference/transport-contracts.mdx +17 -0
- package/site/content/docs/reference/webhook-events.mdx +70 -0
- package/site/content/docs/reference/whatsapp-options.mdx +15 -0
- package/site/content/docs/transports/brevo.mdx +40 -0
- package/site/content/docs/transports/cloudflare-email.mdx +40 -0
- package/site/content/docs/transports/index.mdx +105 -0
- package/site/content/docs/transports/loops.mdx +41 -0
- package/site/content/docs/transports/mailersend.mdx +40 -0
- package/site/content/docs/transports/mailgun.mdx +42 -0
- package/site/content/docs/transports/mailtrap.mdx +42 -0
- package/site/content/docs/transports/meta.json +32 -0
- package/site/content/docs/transports/msegat.mdx +42 -0
- package/site/content/docs/transports/plunk.mdx +40 -0
- package/site/content/docs/transports/postmark.mdx +40 -0
- package/site/content/docs/transports/resend.mdx +43 -0
- package/site/content/docs/transports/sendgrid.mdx +40 -0
- package/site/content/docs/transports/ses.mdx +44 -0
- package/site/content/docs/transports/smtp.mdx +47 -0
- package/site/content/docs/transports/sndr.mdx +144 -0
- package/site/content/docs/transports/sparkpost.mdx +41 -0
- package/site/content/docs/transports/taqnyat-mail.mdx +41 -0
- package/site/content/docs/transports/taqnyat-sms.mdx +41 -0
- package/site/content/docs/transports/taqnyat-whatsapp.mdx +40 -0
- package/site/content/docs/transports/twilio-sms.mdx +128 -0
- package/site/content/docs/transports/webpush.mdx +43 -0
- package/site/content/docs/transports/whatsapp-cloud.mdx +42 -0
- package/dist/chunk-4mvp3b85.js +0 -3
- package/dist/chunk-4mvp3b85.js.map +0 -10
- package/dist/chunk-74rha3ac.js +0 -4
- package/dist/chunk-cvrbfke9.js +0 -13
- package/dist/chunk-d5rwm3br.js +0 -5
- package/dist/chunk-ka6t8g77.js +0 -4
- package/dist/chunk-ka6t8g77.js.map +0 -11
- package/dist/chunk-r7ee9tk7.js +0 -4
- package/dist/chunk-rm78s1ek.js +0 -4
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Msegat SMS gateway transport (Saudi Arabia).
|
|
4
|
+
*
|
|
5
|
+
* Shapes follow the official Postman collection
|
|
6
|
+
* ([MSEGAT Communication Platform API](https://documenter.getpostman.com/view/39158411/2sBY4LT3EY)):
|
|
7
|
+
* - `POST /gw/sendsms.php` — JSON send (`code` `"1"` / `"M0000"`)
|
|
8
|
+
* - `GET /gw/Credits.php` — balance / credential check
|
|
9
|
+
* - `POST /gw/sendOTPCode.php` / `verifyOTPCode.php` — vendor OTP helpers
|
|
10
|
+
*
|
|
11
|
+
* Sently-first: wire into {@link createSmsSender}; OTP stays on this class.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { createSmsSender } from "sently/sms";
|
|
16
|
+
* import { MsegatTransport } from "sently/transports/msegat";
|
|
17
|
+
*
|
|
18
|
+
* const msegat = new MsegatTransport({
|
|
19
|
+
* userName: process.env.MSEGAT_USERNAME!,
|
|
20
|
+
* apiKey: process.env.MSEGAT_API_KEY!,
|
|
21
|
+
* sender: "MyBrand",
|
|
22
|
+
* });
|
|
23
|
+
* const sms = createSmsSender({ transport: msegat });
|
|
24
|
+
*
|
|
25
|
+
* await sms.send({ to: "+9665xxxxxxxx", body: "Hello" });
|
|
26
|
+
* const otp = await msegat.sendOtp({ to: "+9665xxxxxxxx", lang: "En" });
|
|
27
|
+
* await msegat.verifyOtp({ id: otp.id, code: "1234", lang: "En" });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import { SentlyError, type SentlyErrorCode } from "../core/errors.js";
|
|
31
|
+
import type { SmsOptions, SmsSendResult, SmsTransport } from "../core/sms-types.js";
|
|
32
|
+
import type { VerifyResult } from "../core/types.js";
|
|
33
|
+
/** Msegat SMS API configuration. */
|
|
34
|
+
export interface MsegatConfig {
|
|
35
|
+
/** Msegat account username. */
|
|
36
|
+
userName: string;
|
|
37
|
+
/** Msegat API key. */
|
|
38
|
+
apiKey: string;
|
|
39
|
+
/** Pre-approved sender name (use `auth-mseg` for free OTP tests). */
|
|
40
|
+
sender: string;
|
|
41
|
+
}
|
|
42
|
+
/** Options for {@link MsegatTransport.sendOtp}. */
|
|
43
|
+
export interface MsegatSendOtpOptions {
|
|
44
|
+
/** Recipient phone number (E.164 or international digits). */
|
|
45
|
+
to: string;
|
|
46
|
+
/** Sender ID override; defaults to transport `sender`. */
|
|
47
|
+
from?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Message language for header + body (`Ar` or `En`).
|
|
50
|
+
* Postman: "lang : Ar or En in header". Defaults to `"En"`.
|
|
51
|
+
*/
|
|
52
|
+
lang?: "Ar" | "En";
|
|
53
|
+
}
|
|
54
|
+
/** Result of a successful {@link MsegatTransport.sendOtp} call. */
|
|
55
|
+
export interface MsegatOtpSendResult {
|
|
56
|
+
/** OTP session id required by {@link MsegatTransport.verifyOtp}. */
|
|
57
|
+
id: string;
|
|
58
|
+
/** Recipient phone number as passed in. */
|
|
59
|
+
to: string;
|
|
60
|
+
/** Delivery status (`"accepted"` on success). */
|
|
61
|
+
status: string;
|
|
62
|
+
/** Raw response body text. */
|
|
63
|
+
response: string;
|
|
64
|
+
/** Provider identifier. */
|
|
65
|
+
provider: "msegat";
|
|
66
|
+
}
|
|
67
|
+
/** Options for {@link MsegatTransport.verifyOtp}. */
|
|
68
|
+
export interface MsegatVerifyOtpOptions {
|
|
69
|
+
/** OTP session id from {@link MsegatOtpSendResult.id}. */
|
|
70
|
+
id: string | number;
|
|
71
|
+
/** Code the user entered. */
|
|
72
|
+
code: string;
|
|
73
|
+
/** Sender ID override; defaults to transport `sender`. */
|
|
74
|
+
from?: string;
|
|
75
|
+
/** Message language (`Ar` or `En`). Defaults to `"En"`. */
|
|
76
|
+
lang?: "Ar" | "En";
|
|
77
|
+
}
|
|
78
|
+
/** Result of a successful {@link MsegatTransport.verifyOtp} call. */
|
|
79
|
+
export interface MsegatOtpVerifyResult {
|
|
80
|
+
/** Always `true` when the call resolves (failures throw). */
|
|
81
|
+
ok: true;
|
|
82
|
+
/** Provider message when present. */
|
|
83
|
+
message: string;
|
|
84
|
+
/** Raw response body text. */
|
|
85
|
+
response: string;
|
|
86
|
+
/** Provider identifier. */
|
|
87
|
+
provider: "msegat";
|
|
88
|
+
}
|
|
89
|
+
/** Error thrown when the Msegat SMS API reports a failure. */
|
|
90
|
+
export declare class MsegatError extends SentlyError {
|
|
91
|
+
readonly statusCode: number;
|
|
92
|
+
readonly apiError: unknown;
|
|
93
|
+
/** Creates a Msegat SMS API error with status code and response payload. */
|
|
94
|
+
constructor(message: string, statusCode: number, apiError: unknown, sentlyCode?: SentlyErrorCode,
|
|
95
|
+
/** Msegat response `code` when present (e.g. `"1020"`, `"M0002"`). */
|
|
96
|
+
providerCode?: string);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Msegat SMS transport via JSON `sendsms.php` + Credits-based `verify()`,
|
|
100
|
+
* with provider-specific OTP helpers.
|
|
101
|
+
*/
|
|
102
|
+
export declare class MsegatTransport implements SmsTransport {
|
|
103
|
+
readonly provider = "msegat";
|
|
104
|
+
private readonly userName;
|
|
105
|
+
private readonly apiKey;
|
|
106
|
+
private readonly sender;
|
|
107
|
+
/** Creates an Msegat SMS transport. */
|
|
108
|
+
constructor(config: MsegatConfig);
|
|
109
|
+
/**
|
|
110
|
+
* Sends an SMS via `POST /gw/sendsms.php`
|
|
111
|
+
* (JSON: `userName`, `apiKey`, `numbers`, `userSender`, `msg`).
|
|
112
|
+
*/
|
|
113
|
+
send(options: SmsOptions): Promise<SmsSendResult>;
|
|
114
|
+
/**
|
|
115
|
+
* Vendor extra: send a provider-generated OTP via `sendOTPCode.php`.
|
|
116
|
+
* Not part of {@link SmsTransport} — call on {@link MsegatTransport} directly.
|
|
117
|
+
* Keep the returned {@link MsegatOtpSendResult.id} for {@link verifyOtp}.
|
|
118
|
+
*/
|
|
119
|
+
sendOtp(options: MsegatSendOtpOptions): Promise<MsegatOtpSendResult>;
|
|
120
|
+
/**
|
|
121
|
+
* Vendor extra: verify a code previously sent with {@link sendOtp} via
|
|
122
|
+
* `verifyOTPCode.php`. Not part of {@link SmsTransport} — call on
|
|
123
|
+
* {@link MsegatTransport} directly. Throws {@link MsegatError} when the code
|
|
124
|
+
* is invalid, expired (`400`), or not found (`404`).
|
|
125
|
+
*/
|
|
126
|
+
verifyOtp(options: MsegatVerifyOtpOptions): Promise<MsegatOtpVerifyResult>;
|
|
127
|
+
/**
|
|
128
|
+
* Credential check via `GET /gw/Credits.php` (no SMS sent).
|
|
129
|
+
* Postman success body is a bare balance number (e.g. `"272.6"`).
|
|
130
|
+
*/
|
|
131
|
+
verify(): Promise<VerifyResult>;
|
|
132
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{ca as Z,da as V}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class L extends Z{statusCode;apiError;constructor(q,A,F,D,B){super(q,D??(A>=400?V(A):"PROVIDER_ERROR"),{statusCode:A,provider:"msegat",cause:F});this.statusCode=A;this.apiError=F;if(this.name="MsegatError",B!==void 0)this.code=B}}function X(q){let A=q.trim();if(A.startsWith("+"))A=A.slice(1);if(A.startsWith("00"))A=A.slice(2);return A}function W(q){let A=q.code??q.Code;if(typeof A==="string"||typeof A==="number")return String(A);return}function Y(q){return q==="1"||q==="M0000"}function _(q){let A=W(q);if(Y(A))return!0;return q.success===!0&&(A===void 0||Y(A))}function K(q){if(typeof q.message==="string")return q.message;if(typeof q.Message==="string")return q.Message;return}function $(q){switch(q){case"1060":return"PROVIDER_ERROR";case"400":case"404":return"BAD_REQUEST";default:return"BAD_REQUEST"}}async function Q(q,A,F){let D=await fetch(q,{method:"POST",headers:{"Content-Type":"application/json",...F},body:JSON.stringify(A)}),B=await D.text(),G;try{G=JSON.parse(B)}catch{throw new L("Msegat returned non-JSON response",D.status,B)}return{response:D,text:B,payload:G}}function U(q,A,F){let D=W(A);if(!q.ok||!_(A)){let B=K(A)??`${F} (code ${D??"unknown"})`,G=q.status>=400?V(q.status):$(D);throw new L(B,q.status,A,G,D)}return D??"1"}class j{provider="msegat";userName;apiKey;sender;constructor(q){this.userName=q.userName,this.apiKey=q.apiKey,this.sender=q.sender}async send(q){let A=X(q.to),{response:F,text:D,payload:B}=await Q("https://www.msegat.com/gw/sendsms.php",{userName:this.userName,apiKey:this.apiKey,numbers:A,userSender:q.from??this.sender,msg:q.body,msgEncoding:"UTF8",reqBulkId:"true"});U(F,B,"Msegat SMS API error");let G=B.bulk_id??B.messageId??B.id??B.Id;return{messageId:typeof G==="string"||typeof G==="number"?String(G):q.messageId??crypto.randomUUID(),to:q.to,status:"accepted",response:D,provider:"msegat"}}async sendOtp(q){let A=q.lang??"En",F=X(q.to),{response:D,text:B,payload:G}=await Q("https://www.msegat.com/gw/sendOTPCode.php",{userName:this.userName,apiKey:this.apiKey,number:F,userSender:q.from??this.sender,lang:A},{lang:A});U(D,G,"Msegat OTP send error");let I=G.id??G.Id;if(typeof I!=="string"&&typeof I!=="number")throw new L("Msegat OTP send succeeded but returned no id",D.status,G);return{id:String(I),to:q.to,status:"accepted",response:B,provider:"msegat"}}async verifyOtp(q){let A=q.lang??"En",{response:F,text:D,payload:B}=await Q("https://www.msegat.com/gw/verifyOTPCode.php",{userName:this.userName,apiKey:this.apiKey,code:q.code,id:q.id,userSender:q.from??this.sender,lang:A},{lang:A});return U(F,B,"Msegat OTP verify error"),{ok:!0,message:K(B)??"Success",response:D,provider:"msegat"}}async verify(){if(!this.userName||!this.apiKey||!this.sender)return{ok:!1,provider:"msegat",message:"Missing credentials"};let q=new URL("https://www.msegat.com/gw/Credits.php");q.searchParams.set("userName",this.userName),q.searchParams.set("apiKey",this.apiKey);try{let A=await fetch(q,{method:"GET",headers:{"Content-Type":"application/json"}}),F=(await A.text()).trim();if(!A.ok)return{ok:!1,provider:"msegat",message:`Credits inquiry failed (HTTP ${A.status}): ${F||"empty body"}`};let D=Number(F);if(Number.isFinite(D))return{ok:!0,provider:"msegat",message:`Balance: ${F}`};try{let B=JSON.parse(F);if(_(B))return{ok:!0,provider:"msegat",message:K(B)??"OK"};let G=W(B);return{ok:!1,provider:"msegat",message:K(B)??`Credits inquiry failed (code ${G??"unknown"})`}}catch{return{ok:!1,provider:"msegat",message:`Credits inquiry returned unexpected body: ${F}`}}}catch(A){return{ok:!1,provider:"msegat",message:A instanceof Error?A.message:"Credits inquiry failed"}}}}export{j as MsegatTransport,L as MsegatError};
|
|
2
|
+
|
|
3
|
+
//# debugId=C8170D426FAB9BF364756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/msegat.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Msegat SMS gateway transport (Saudi Arabia).\n *\n * Shapes follow the official Postman collection\n * ([MSEGAT Communication Platform API](https://documenter.getpostman.com/view/39158411/2sBY4LT3EY)):\n * - `POST /gw/sendsms.php` — JSON send (`code` `\"1\"` / `\"M0000\"`)\n * - `GET /gw/Credits.php` — balance / credential check\n * - `POST /gw/sendOTPCode.php` / `verifyOTPCode.php` — vendor OTP helpers\n *\n * Sently-first: wire into {@link createSmsSender}; OTP stays on this class.\n *\n * @example\n * ```ts\n * import { createSmsSender } from \"sently/sms\";\n * import { MsegatTransport } from \"sently/transports/msegat\";\n *\n * const msegat = new MsegatTransport({\n * userName: process.env.MSEGAT_USERNAME!,\n * apiKey: process.env.MSEGAT_API_KEY!,\n * sender: \"MyBrand\",\n * });\n * const sms = createSmsSender({ transport: msegat });\n *\n * await sms.send({ to: \"+9665xxxxxxxx\", body: \"Hello\" });\n * const otp = await msegat.sendOtp({ to: \"+9665xxxxxxxx\", lang: \"En\" });\n * await msegat.verifyOtp({ id: otp.id, code: \"1234\", lang: \"En\" });\n * ```\n */\nimport { httpStatusToSentlyCode, SentlyError, type SentlyErrorCode } from \"../core/errors.js\";\nimport type { SmsOptions, SmsSendResult, SmsTransport } from \"../core/sms-types.js\";\nimport type { VerifyResult } from \"../core/types.js\";\n\n/** Msegat SMS API configuration. */\nexport interface MsegatConfig {\n /** Msegat account username. */\n userName: string;\n /** Msegat API key. */\n apiKey: string;\n /** Pre-approved sender name (use `auth-mseg` for free OTP tests). */\n sender: string;\n}\n\n/** Options for {@link MsegatTransport.sendOtp}. */\nexport interface MsegatSendOtpOptions {\n /** Recipient phone number (E.164 or international digits). */\n to: string;\n /** Sender ID override; defaults to transport `sender`. */\n from?: string;\n /**\n * Message language for header + body (`Ar` or `En`).\n * Postman: \"lang : Ar or En in header\". Defaults to `\"En\"`.\n */\n lang?: \"Ar\" | \"En\";\n}\n\n/** Result of a successful {@link MsegatTransport.sendOtp} call. */\nexport interface MsegatOtpSendResult {\n /** OTP session id required by {@link MsegatTransport.verifyOtp}. */\n id: string;\n /** Recipient phone number as passed in. */\n to: string;\n /** Delivery status (`\"accepted\"` on success). */\n status: string;\n /** Raw response body text. */\n response: string;\n /** Provider identifier. */\n provider: \"msegat\";\n}\n\n/** Options for {@link MsegatTransport.verifyOtp}. */\nexport interface MsegatVerifyOtpOptions {\n /** OTP session id from {@link MsegatOtpSendResult.id}. */\n id: string | number;\n /** Code the user entered. */\n code: string;\n /** Sender ID override; defaults to transport `sender`. */\n from?: string;\n /** Message language (`Ar` or `En`). Defaults to `\"En\"`. */\n lang?: \"Ar\" | \"En\";\n}\n\n/** Result of a successful {@link MsegatTransport.verifyOtp} call. */\nexport interface MsegatOtpVerifyResult {\n /** Always `true` when the call resolves (failures throw). */\n ok: true;\n /** Provider message when present. */\n message: string;\n /** Raw response body text. */\n response: string;\n /** Provider identifier. */\n provider: \"msegat\";\n}\n\n/** Error thrown when the Msegat SMS API reports a failure. */\nexport class MsegatError extends SentlyError {\n /** Creates a Msegat SMS API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n sentlyCode?: SentlyErrorCode,\n /** Msegat response `code` when present (e.g. `\"1020\"`, `\"M0002\"`). */\n providerCode?: string,\n ) {\n super(\n message,\n sentlyCode ?? (statusCode >= 400 ? httpStatusToSentlyCode(statusCode) : \"PROVIDER_ERROR\"),\n {\n statusCode,\n provider: \"msegat\",\n cause: apiError,\n },\n );\n this.name = \"MsegatError\";\n if (providerCode !== undefined) {\n this.code = providerCode;\n }\n }\n}\n\n/**\n * International format without `+` or leading `00`\n * (Postman: \"international format without zeros\").\n */\nfunction normalizeMsegatPhone(phone: string): string {\n let normalized = phone.trim();\n if (normalized.startsWith(\"+\")) {\n normalized = normalized.slice(1);\n }\n if (normalized.startsWith(\"00\")) {\n normalized = normalized.slice(2);\n }\n return normalized;\n}\n\nfunction responseCode(payload: Record<string, unknown>): string | undefined {\n const raw = payload.code ?? payload.Code;\n if (typeof raw === \"string\" || typeof raw === \"number\") {\n return String(raw);\n }\n return undefined;\n}\n\n/** Documented success codes: `1` / `M0000` (and OTP `success: true`). */\nfunction isSuccessCode(code: string | undefined): boolean {\n return code === \"1\" || code === \"M0000\";\n}\n\nfunction isSuccessPayload(payload: Record<string, unknown>): boolean {\n const code = responseCode(payload);\n if (isSuccessCode(code)) return true;\n // sendOTPCode success example includes `\"success\": true` alongside `code: \"1\"`.\n return payload.success === true && (code === undefined || isSuccessCode(code));\n}\n\nfunction responseMessage(payload: Record<string, unknown>): string | undefined {\n if (typeof payload.message === \"string\") return payload.message;\n if (typeof payload.Message === \"string\") return payload.Message;\n return undefined;\n}\n\n/** Map documented Msegat codes onto stable sently codes. */\nfunction msegatCodeToSently(code: string | undefined): SentlyErrorCode {\n switch (code) {\n case \"1060\":\n // Balance is not enough — provider-side account state.\n return \"PROVIDER_ERROR\";\n case \"400\":\n case \"404\":\n // OTP expired / not found.\n return \"BAD_REQUEST\";\n default:\n return \"BAD_REQUEST\";\n }\n}\n\nasync function postJson(\n url: string,\n body: Record<string, unknown>,\n headers?: Record<string, string>,\n): Promise<{ response: Response; text: string; payload: Record<string, unknown> }> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify(body),\n });\n\n const text = await response.text();\n let payload: Record<string, unknown>;\n try {\n payload = JSON.parse(text) as Record<string, unknown>;\n } catch {\n throw new MsegatError(\"Msegat returned non-JSON response\", response.status, text);\n }\n\n return { response, text, payload };\n}\n\nfunction assertSuccess(\n response: Response,\n payload: Record<string, unknown>,\n fallbackMessage: string,\n): string {\n const code = responseCode(payload);\n if (!response.ok || !isSuccessPayload(payload)) {\n const message = responseMessage(payload) ?? `${fallbackMessage} (code ${code ?? \"unknown\"})`;\n const sentlyCode =\n response.status >= 400 ? httpStatusToSentlyCode(response.status) : msegatCodeToSently(code);\n throw new MsegatError(message, response.status, payload, sentlyCode, code);\n }\n return code ?? \"1\";\n}\n\n/**\n * Msegat SMS transport via JSON `sendsms.php` + Credits-based `verify()`,\n * with provider-specific OTP helpers.\n */\nexport class MsegatTransport implements SmsTransport {\n readonly provider = \"msegat\";\n\n private readonly userName: string;\n private readonly apiKey: string;\n private readonly sender: string;\n\n /** Creates an Msegat SMS transport. */\n constructor(config: MsegatConfig) {\n this.userName = config.userName;\n this.apiKey = config.apiKey;\n this.sender = config.sender;\n }\n\n /**\n * Sends an SMS via `POST /gw/sendsms.php`\n * (JSON: `userName`, `apiKey`, `numbers`, `userSender`, `msg`).\n */\n async send(options: SmsOptions): Promise<SmsSendResult> {\n const numbers = normalizeMsegatPhone(options.to);\n const { response, text, payload } = await postJson(\"https://www.msegat.com/gw/sendsms.php\", {\n userName: this.userName,\n apiKey: this.apiKey,\n numbers,\n userSender: options.from ?? this.sender,\n msg: options.body,\n // Optional per Postman; UTF8 is the documented default.\n msgEncoding: \"UTF8\",\n // Optional: request bulk id when the account returns one.\n reqBulkId: \"true\",\n });\n\n assertSuccess(response, payload, \"Msegat SMS API error\");\n\n const idCandidate = payload.bulk_id ?? payload.messageId ?? payload.id ?? payload.Id;\n const messageId =\n typeof idCandidate === \"string\" || typeof idCandidate === \"number\"\n ? String(idCandidate)\n : (options.messageId ?? crypto.randomUUID());\n\n return {\n messageId,\n to: options.to,\n status: \"accepted\",\n response: text,\n provider: \"msegat\",\n };\n }\n\n /**\n * Vendor extra: send a provider-generated OTP via `sendOTPCode.php`.\n * Not part of {@link SmsTransport} — call on {@link MsegatTransport} directly.\n * Keep the returned {@link MsegatOtpSendResult.id} for {@link verifyOtp}.\n */\n async sendOtp(options: MsegatSendOtpOptions): Promise<MsegatOtpSendResult> {\n const lang = options.lang ?? \"En\";\n const number = normalizeMsegatPhone(options.to);\n const { response, text, payload } = await postJson(\n \"https://www.msegat.com/gw/sendOTPCode.php\",\n {\n userName: this.userName,\n apiKey: this.apiKey,\n number,\n userSender: options.from ?? this.sender,\n lang,\n },\n // Postman: \"lang : Ar or En in header\"\n { lang },\n );\n\n assertSuccess(response, payload, \"Msegat OTP send error\");\n\n const idRaw = payload.id ?? payload.Id;\n if (typeof idRaw !== \"string\" && typeof idRaw !== \"number\") {\n throw new MsegatError(\n \"Msegat OTP send succeeded but returned no id\",\n response.status,\n payload,\n );\n }\n\n return {\n id: String(idRaw),\n to: options.to,\n status: \"accepted\",\n response: text,\n provider: \"msegat\",\n };\n }\n\n /**\n * Vendor extra: verify a code previously sent with {@link sendOtp} via\n * `verifyOTPCode.php`. Not part of {@link SmsTransport} — call on\n * {@link MsegatTransport} directly. Throws {@link MsegatError} when the code\n * is invalid, expired (`400`), or not found (`404`).\n */\n async verifyOtp(options: MsegatVerifyOtpOptions): Promise<MsegatOtpVerifyResult> {\n const lang = options.lang ?? \"En\";\n const { response, text, payload } = await postJson(\n \"https://www.msegat.com/gw/verifyOTPCode.php\",\n {\n userName: this.userName,\n apiKey: this.apiKey,\n code: options.code,\n id: options.id,\n userSender: options.from ?? this.sender,\n lang,\n },\n { lang },\n );\n\n assertSuccess(response, payload, \"Msegat OTP verify error\");\n\n return {\n ok: true,\n message: responseMessage(payload) ?? \"Success\",\n response: text,\n provider: \"msegat\",\n };\n }\n\n /**\n * Credential check via `GET /gw/Credits.php` (no SMS sent).\n * Postman success body is a bare balance number (e.g. `\"272.6\"`).\n */\n async verify(): Promise<VerifyResult> {\n if (!this.userName || !this.apiKey || !this.sender) {\n return { ok: false, provider: \"msegat\", message: \"Missing credentials\" };\n }\n\n const url = new URL(\"https://www.msegat.com/gw/Credits.php\");\n url.searchParams.set(\"userName\", this.userName);\n url.searchParams.set(\"apiKey\", this.apiKey);\n\n try {\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n // Matches Postman Credits request headers.\n \"Content-Type\": \"application/json\",\n },\n });\n const text = (await response.text()).trim();\n\n if (!response.ok) {\n return {\n ok: false,\n provider: \"msegat\",\n message: `Credits inquiry failed (HTTP ${response.status}): ${text || \"empty body\"}`,\n };\n }\n\n const balance = Number(text);\n if (Number.isFinite(balance)) {\n return {\n ok: true,\n provider: \"msegat\",\n message: `Balance: ${text}`,\n };\n }\n\n // Some failures still return HTTP 200 with a code/message JSON body.\n try {\n const payload = JSON.parse(text) as Record<string, unknown>;\n if (isSuccessPayload(payload)) {\n return { ok: true, provider: \"msegat\", message: responseMessage(payload) ?? \"OK\" };\n }\n const code = responseCode(payload);\n return {\n ok: false,\n provider: \"msegat\",\n message: responseMessage(payload) ?? `Credits inquiry failed (code ${code ?? \"unknown\"})`,\n };\n } catch {\n return {\n ok: false,\n provider: \"msegat\",\n message: `Credits inquiry returned unexpected body: ${text}`,\n };\n }\n } catch (error) {\n return {\n ok: false,\n provider: \"msegat\",\n message: error instanceof Error ? error.message : \"Credits inquiry failed\",\n };\n }\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "C8170D426FAB9BF364756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SentlyError } from "../core/errors.js";
|
|
2
|
+
import type { MailOptions, SendResult, Transport, VerifyResult } from "../core/types.js";
|
|
3
|
+
/** Plunk HTTP API configuration. */
|
|
4
|
+
export interface PlunkConfig {
|
|
5
|
+
/** Plunk API key for Bearer authentication. */
|
|
6
|
+
apiKey: string;
|
|
7
|
+
}
|
|
8
|
+
/** Error thrown when the Plunk API returns a non-success response. */
|
|
9
|
+
export declare class PlunkError extends SentlyError {
|
|
10
|
+
readonly statusCode: number;
|
|
11
|
+
readonly apiError: unknown;
|
|
12
|
+
/** Creates a Plunk API error with status code. */
|
|
13
|
+
constructor(message: string, statusCode: number, apiError: unknown);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Plunk HTTP API transport.
|
|
17
|
+
*/
|
|
18
|
+
export declare class PlunkTransport implements Transport {
|
|
19
|
+
readonly provider = "plunk";
|
|
20
|
+
private readonly apiKey;
|
|
21
|
+
/** Creates a Plunk transport with the given API key. */
|
|
22
|
+
constructor(config: PlunkConfig);
|
|
23
|
+
/** Sends an email via the Plunk HTTP API (one request per recipient). */
|
|
24
|
+
send(options: MailOptions): Promise<SendResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Returns success without a network call — Plunk has no documented verify endpoint.
|
|
27
|
+
*/
|
|
28
|
+
verify(): Promise<VerifyResult>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{Q as K,S as L}from"../chunk-93vqxxj2.js";import"../chunk-ywq6s10g.js";import{ca as M,da as N}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class O extends M{statusCode;apiError;constructor(q,B,z){super(q,N(B),{statusCode:B,provider:"plunk",cause:z});this.statusCode=B;this.apiError=z;this.name="PlunkError"}}class V{provider="plunk";apiKey;constructor(q){this.apiKey=q.apiKey}async send(q){let B=K(q.from)[0],z=L(q.to),Q=q.html??q.text??"",H=[],J=[];for(let U of z){let F=await fetch("https://api.useplunk.com/v1/send",{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify({to:U,subject:q.subject,body:Q,subscribed:!1})}),D=await F.json();if(!F.ok||D.success!==!0)throw new O(D.message??"Plunk API error",F.status,D);let G=D.emails?.[0]?.id??"";if(G)H.push(G);J.push(G||"sent")}return{messageId:H.join(","),accepted:z,rejected:[],response:J.join("; "),envelope:{from:B?.address??"",to:z}}}async verify(){return{ok:!0,provider:"plunk",message:"no verify endpoint; key not validated"}}}export{V as PlunkTransport,O as PlunkError};
|
|
2
|
+
|
|
3
|
+
//# debugId=8B468EAA2B0EF13864756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/plunk.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Plunk HTTP transport for sently.\n *\n * Plunk's send API targets a single recipient per request. When `options.to`\n * contains multiple addresses, one HTTP request is sent per recipient and\n * results are aggregated into a single {@link SendResult}.\n *\n * @example\n * ```ts\n * import { PlunkTransport } from \"sently/transports/plunk\";\n * import { createMailer } from \"sently/mailer\";\n *\n * const mailer = await createMailer({\n * transport: new PlunkTransport({ apiKey: process.env.PLUNK_API_KEY! }),\n * });\n * ```\n */\nimport { extractEmails, parseAddresses } from \"../core/address.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\n\n/** Plunk HTTP API configuration. */\nexport interface PlunkConfig {\n /** Plunk API key for Bearer authentication. */\n apiKey: string;\n}\n\n/** Error thrown when the Plunk API returns a non-success response. */\nexport class PlunkError extends SentlyError {\n /** Creates a Plunk API error with status code. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"plunk\",\n cause: apiError,\n });\n this.name = \"PlunkError\";\n }\n}\n\n/**\n * Plunk HTTP API transport.\n */\nexport class PlunkTransport implements Transport {\n readonly provider = \"plunk\";\n\n private readonly apiKey: string;\n\n /** Creates a Plunk transport with the given API key. */\n constructor(config: PlunkConfig) {\n this.apiKey = config.apiKey;\n }\n\n /** Sends an email via the Plunk HTTP API (one request per recipient). */\n async send(options: MailOptions): Promise<SendResult> {\n const from = parseAddresses(options.from)[0];\n const recipients = extractEmails(options.to);\n const bodyContent = options.html ?? options.text ?? \"\";\n const messageIds: string[] = [];\n const responses: string[] = [];\n\n for (const recipient of recipients) {\n const response = await fetch(\"https://api.useplunk.com/v1/send\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n to: recipient,\n subject: options.subject,\n body: bodyContent,\n subscribed: false,\n }),\n });\n\n const payload = (await response.json()) as {\n success?: boolean;\n emails?: Array<{ id?: string; email?: string }>;\n message?: string;\n };\n\n if (!response.ok || payload.success !== true) {\n throw new PlunkError(payload.message ?? \"Plunk API error\", response.status, payload);\n }\n\n const emailId = payload.emails?.[0]?.id ?? \"\";\n if (emailId) {\n messageIds.push(emailId);\n }\n responses.push(emailId || \"sent\");\n }\n\n return {\n messageId: messageIds.join(\",\"),\n accepted: recipients,\n rejected: [],\n response: responses.join(\"; \"),\n envelope: {\n from: from?.address ?? \"\",\n to: recipients,\n },\n };\n }\n\n /**\n * Returns success without a network call — Plunk has no documented verify endpoint.\n */\n async verify(): Promise<VerifyResult> {\n return {\n ok: true,\n provider: \"plunk\",\n message: \"no verify endpoint; key not validated\",\n };\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "2KA6BO,CAAM,KAAmB,JAAY,CAIxB,WACA,SAHlB,WAAW,CACT,EACgB,EACA,EAChB,CACA,MAAM,EAAS,EAAuB,CAAU,EAAG,CACjD,aACA,SAAU,QACV,MAAO,CACT,CAAC,EAPe,kBACA,gBAOhB,KAAK,KAAO,aAEhB,CAKO,MAAM,CAAoC,CACtC,SAAW,QAEH,OAGjB,WAAW,CAAC,EAAqB,CAC/B,KAAK,OAAS,EAAO,YAIjB,KAAI,CAAC,EAA2C,CACpD,IAAM,EAAO,EAAe,EAAQ,IAAI,EAAE,GACpC,EAAa,EAAc,EAAQ,EAAE,EACrC,EAAc,EAAQ,MAAQ,EAAQ,MAAQ,GAC9C,EAAuB,CAAC,EACxB,EAAsB,CAAC,EAE7B,QAAW,KAAa,EAAY,CAClC,IAAM,EAAW,MAAM,MAAM,mCAAoC,CAC/D,OAAQ,OACR,QAAS,CACP,cAAe,UAAU,KAAK,SAC9B,eAAgB,kBAClB,EACA,KAAM,KAAK,UAAU,CACnB,GAAI,EACJ,QAAS,EAAQ,QACjB,KAAM,EACN,WAAY,EACd,CAAC,CACH,CAAC,EAEK,EAAW,MAAM,EAAS,KAAK,EAMrC,GAAI,CAAC,EAAS,IAAM,EAAQ,UAAY,GACtC,MAAM,IAAI,EAAW,EAAQ,SAAW,kBAAmB,EAAS,OAAQ,CAAO,EAGrF,IAAM,EAAU,EAAQ,SAAS,IAAI,IAAM,GAC3C,GAAI,EACF,EAAW,KAAK,CAAO,EAEzB,EAAU,KAAK,GAAW,MAAM,EAGlC,MAAO,CACL,UAAW,EAAW,KAAK,GAAG,EAC9B,SAAU,EACV,SAAU,CAAC,EACX,SAAU,EAAU,KAAK,IAAI,EAC7B,SAAU,CACR,KAAM,GAAM,SAAW,GACvB,GAAI,CACN,CACF,OAMI,OAAM,EAA0B,CACpC,MAAO,CACL,GAAI,GACJ,SAAU,QACV,QAAS,uCACX,EAEJ",
|
|
8
|
+
"debugId": "8B468EAA2B0EF13864756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -16,6 +16,7 @@ export declare class PostmarkError extends SentlyError {
|
|
|
16
16
|
* Postmark HTTP API transport.
|
|
17
17
|
*/
|
|
18
18
|
export declare class PostmarkTransport implements Transport {
|
|
19
|
+
readonly provider = "postmark";
|
|
19
20
|
/** Postmark server API token for the X-Postmark-Server-Token header. */
|
|
20
21
|
private readonly serverToken;
|
|
21
22
|
/** Creates a Postmark transport with the given server token. */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{I as U}from"../chunk-skqhj0rm.js";import{Q as D,R as F,S as J}from"../chunk-93vqxxj2.js";import{Y as O}from"../chunk-ywq6s10g.js";import{ca as Q,da as R}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class W extends Q{statusCode;apiError;constructor(q,w,G){super(q,R(w),{statusCode:w,provider:"postmark",cause:G});this.statusCode=w;this.apiError=G;this.name="PostmarkError"}}class Z{provider="postmark";serverToken;constructor(q){this.serverToken=q.serverToken}async send(q){let w=await U(q.attachments),G=D(q.from)[0],X={From:G?F(G):"",To:D(q.to).map(F).join(", "),Subject:q.subject,...q.cc?{Cc:D(q.cc).map(F).join(", ")}:{},...q.bcc?{Bcc:D(q.bcc).map(F).join(", ")}:{},...q.replyTo?{ReplyTo:D(q.replyTo).map(F).join(", ")}:{},...q.text?{TextBody:q.text}:{},...q.html?{HtmlBody:q.html}:{},...q.headers?{Headers:Object.entries(q.headers).map(([z,Y])=>({Name:z,Value:Y}))}:{},...w.length>0?{Attachments:w.map((z)=>({Name:z.filename,Content:z.content instanceof Uint8Array?O(z.content).replace(/\r\n/g,""):z.content,ContentType:z.contentType??"application/octet-stream",...z.contentId?{ContentID:z.contentId}:{}}))}:{}},L=await fetch("https://api.postmarkapp.com/email",{method:"POST",headers:{"X-Postmark-Server-Token":this.serverToken,"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(X)}),K=await L.json();if(!L.ok)throw new W(K.Message??"Postmark API error",L.status,K);return{messageId:K.MessageID??q.messageId??"",accepted:J(q.to),rejected:[],response:K.Message??"OK",envelope:{from:G?.address??"",to:[...J(q.to),...q.cc?J(q.cc):[],...q.bcc?J(q.bcc):[]]}}}async verify(){try{let q=await fetch("https://api.postmarkapp.com/server",{headers:{"X-Postmark-Server-Token":this.serverToken,Accept:"application/json"}}),w=await q.json();if(!q.ok)return{ok:!1,provider:"postmark",message:w.Message??`HTTP ${q.status}`};return{ok:!0,provider:"postmark",...w.Name?{message:w.Name}:{}}}catch(q){return{ok:!1,provider:"postmark",message:q instanceof Error?q.message:String(q)}}}}export{Z as PostmarkTransport,W as PostmarkError};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=A47A928CB69DD2EA64756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/transports/postmark.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @module\n * Postmark HTTP API transport for sending email via api.postmarkapp.com.\n *\n * @example\n * ```ts\n * import { PostmarkTransport } from \"sently/transports/postmark\";\n * import { createMailer } from \"sently/mailer\";\n *\n * const mailer = await createMailer({\n * transport: new PostmarkTransport({ serverToken: process.env.POSTMARK_TOKEN! }),\n * });\n *\n * await mailer.send({\n * from: \"sender@example.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * text: \"Plain text body\",\n * });\n * ```\n */\nimport { extractEmails, parseAddresses, toMIMEHeader } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Postmark API configuration. */\nexport interface PostmarkConfig {\n /** Postmark server API token. */\n serverToken: string;\n}\n\n/** Error thrown when the Postmark API returns a non-success response. */\nexport class PostmarkError extends SentlyError {\n /** Creates a Postmark API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"postmark\",\n cause: apiError,\n });\n this.name = \"PostmarkError\";\n }\n}\n\n/**\n * Postmark HTTP API transport.\n */\nexport class PostmarkTransport implements Transport {\n /** Postmark server API token for the X-Postmark-Server-Token header. */\n private readonly serverToken: string;\n\n /** Creates a Postmark transport with the given server token. */\n constructor(config: PostmarkConfig) {\n this.serverToken = config.serverToken;\n }\n\n /** Sends an email via the Postmark HTTP API. */\n async send(options: MailOptions): Promise<SendResult> {\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n\n const body = {\n From: from ? toMIMEHeader(from) : \"\",\n To: parseAddresses(options.to).map(toMIMEHeader).join(\", \"),\n Subject: options.subject,\n ...(options.cc ? { Cc: parseAddresses(options.cc).map(toMIMEHeader).join(\", \") } : {}),\n ...(options.bcc ? { Bcc: parseAddresses(options.bcc).map(toMIMEHeader).join(\", \") } : {}),\n ...(options.replyTo\n ? { ReplyTo: parseAddresses(options.replyTo).map(toMIMEHeader).join(\", \") }\n : {}),\n ...(options.text ? { TextBody: options.text } : {}),\n ...(options.html ? { HtmlBody: options.html } : {}),\n ...(options.headers\n ? { Headers: Object.entries(options.headers).map(([Name, Value]) => ({ Name, Value })) }\n : {}),\n ...(attachments.length > 0\n ? {\n Attachments: attachments.map((att) => ({\n Name: att.filename,\n Content:\n att.content instanceof Uint8Array\n ? encodeBase64(att.content).replace(/\\r\\n/g, \"\")\n : att.content,\n ContentType: att.contentType ?? \"application/octet-stream\",\n ...(att.contentId ? { ContentID: att.contentId } : {}),\n })),\n }\n : {}),\n };\n\n const response = await fetch(\"https://api.postmarkapp.com/email\", {\n method: \"POST\",\n headers: {\n \"X-Postmark-Server-Token\": this.serverToken,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const payload = (await response.json()) as {\n MessageID?: string;\n Message?: string;\n ErrorCode?: number;\n };\n\n if (!response.ok) {\n throw new PostmarkError(payload.Message ?? \"Postmark API error\", response.status, payload);\n }\n\n return {\n messageId: payload.MessageID ?? options.messageId ?? \"\",\n accepted: extractEmails(options.to),\n rejected: [],\n response: payload.Message ?? \"OK\",\n envelope: {\n from: from?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n },\n };\n }\n\n /** Verifies the Postmark server token by fetching server info. */\n async verify(): Promise<VerifyResult> {\n try {\n const response = await fetch(\"https://api.postmarkapp.com/server\", {\n headers: {\n \"X-Postmark-Server-Token\": this.serverToken,\n Accept: \"application/json\",\n },\n });\n\n const payload = (await response.json()) as { Name?: string; Message?: string };\n\n if (!response.ok) {\n return {\n ok: false,\n provider: \"postmark\",\n message: payload.Message ?? `HTTP ${response.status}`,\n };\n }\n\n return {\n ok: true,\n provider: \"postmark\",\n ...(payload.Name ? { message: payload.Name } : {}),\n };\n } catch (err) {\n return {\n ok: false,\n provider: \"postmark\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
|
|
5
|
+
"/**\n * @module\n * Postmark HTTP API transport for sending email via api.postmarkapp.com.\n *\n * @example\n * ```ts\n * import { PostmarkTransport } from \"sently/transports/postmark\";\n * import { createMailer } from \"sently/mailer\";\n *\n * const mailer = await createMailer({\n * transport: new PostmarkTransport({ serverToken: process.env.POSTMARK_TOKEN! }),\n * });\n *\n * await mailer.send({\n * from: \"sender@example.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * text: \"Plain text body\",\n * });\n * ```\n */\nimport { extractEmails, parseAddresses, toMIMEHeader } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Postmark API configuration. */\nexport interface PostmarkConfig {\n /** Postmark server API token. */\n serverToken: string;\n}\n\n/** Error thrown when the Postmark API returns a non-success response. */\nexport class PostmarkError extends SentlyError {\n /** Creates a Postmark API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"postmark\",\n cause: apiError,\n });\n this.name = \"PostmarkError\";\n }\n}\n\n/**\n * Postmark HTTP API transport.\n */\nexport class PostmarkTransport implements Transport {\n readonly provider = \"postmark\";\n\n /** Postmark server API token for the X-Postmark-Server-Token header. */\n private readonly serverToken: string;\n\n /** Creates a Postmark transport with the given server token. */\n constructor(config: PostmarkConfig) {\n this.serverToken = config.serverToken;\n }\n\n /** Sends an email via the Postmark HTTP API. */\n async send(options: MailOptions): Promise<SendResult> {\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n\n const body = {\n From: from ? toMIMEHeader(from) : \"\",\n To: parseAddresses(options.to).map(toMIMEHeader).join(\", \"),\n Subject: options.subject,\n ...(options.cc ? { Cc: parseAddresses(options.cc).map(toMIMEHeader).join(\", \") } : {}),\n ...(options.bcc ? { Bcc: parseAddresses(options.bcc).map(toMIMEHeader).join(\", \") } : {}),\n ...(options.replyTo\n ? { ReplyTo: parseAddresses(options.replyTo).map(toMIMEHeader).join(\", \") }\n : {}),\n ...(options.text ? { TextBody: options.text } : {}),\n ...(options.html ? { HtmlBody: options.html } : {}),\n ...(options.headers\n ? { Headers: Object.entries(options.headers).map(([Name, Value]) => ({ Name, Value })) }\n : {}),\n ...(attachments.length > 0\n ? {\n Attachments: attachments.map((att) => ({\n Name: att.filename,\n Content:\n att.content instanceof Uint8Array\n ? encodeBase64(att.content).replace(/\\r\\n/g, \"\")\n : att.content,\n ContentType: att.contentType ?? \"application/octet-stream\",\n ...(att.contentId ? { ContentID: att.contentId } : {}),\n })),\n }\n : {}),\n };\n\n const response = await fetch(\"https://api.postmarkapp.com/email\", {\n method: \"POST\",\n headers: {\n \"X-Postmark-Server-Token\": this.serverToken,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const payload = (await response.json()) as {\n MessageID?: string;\n Message?: string;\n ErrorCode?: number;\n };\n\n if (!response.ok) {\n throw new PostmarkError(payload.Message ?? \"Postmark API error\", response.status, payload);\n }\n\n return {\n messageId: payload.MessageID ?? options.messageId ?? \"\",\n accepted: extractEmails(options.to),\n rejected: [],\n response: payload.Message ?? \"OK\",\n envelope: {\n from: from?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n },\n };\n }\n\n /** Verifies the Postmark server token by fetching server info. */\n async verify(): Promise<VerifyResult> {\n try {\n const response = await fetch(\"https://api.postmarkapp.com/server\", {\n headers: {\n \"X-Postmark-Server-Token\": this.serverToken,\n Accept: \"application/json\",\n },\n });\n\n const payload = (await response.json()) as { Name?: string; Message?: string };\n\n if (!response.ok) {\n return {\n ok: false,\n provider: \"postmark\",\n message: payload.Message ?? `HTTP ${response.status}`,\n };\n }\n\n return {\n ok: true,\n provider: \"postmark\",\n ...(payload.Name ? { message: payload.Name } : {}),\n };\n } catch (err) {\n return {\n ok: false,\n provider: \"postmark\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "A47A928CB69DD2EA64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -3,6 +3,7 @@ import type { MailOptions, PreviewConfig, SendResult, Transport, VerifyResult }
|
|
|
3
3
|
* Development transport that writes emails to disk instead of sending them.
|
|
4
4
|
*/
|
|
5
5
|
export declare class PreviewTransport implements Transport {
|
|
6
|
+
readonly provider = "preview";
|
|
6
7
|
/** Directory where preview `.eml` or `.html` files are written. */
|
|
7
8
|
private readonly outDir;
|
|
8
9
|
/** Whether to open the written file in the default browser. */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{u as L}from"../chunk-8dty8c4p.js";import{I as N}from"../chunk-skqhj0rm.js";import{S as K}from"../chunk-93vqxxj2.js";import"../chunk-ywq6s10g.js";import{fa as J}from"../chunk-6npp3x3c.js";function T(k){return k.replace(/[^a-zA-Z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,40)||"no-subject"}function U(k){if(k==="darwin")return"open";if(k==="win32")return"start";return"xdg-open"}class V{provider="preview";outDir;open;format;constructor(k){this.outDir=k?.outDir??"./.emails",this.open=k?.open??!1,this.format=k?.format??"eml"}async send(k){let H=await N(k.attachments),P={...k,attachments:H},D=await L(P),F=await import("node:fs/promises");await F.mkdir(this.outDir,{recursive:!0});let Q=`${Date.now()}-${T(k.subject)}.${this.format}`,B=`${this.outDir}/${Q}`;if(this.format==="html"){let G=k.html??(k.text?`<pre>${k.text.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}</pre>`:"");await F.writeFile(B,G,"utf8")}else await F.writeFile(B,D.raw);if(console.log(`[sently preview] Written: ${B}`),this.open){let{spawn:G}=await import("node:child_process"),R=U(process.platform);G(R,[B],{detached:!0,stdio:"ignore"}).unref()}return{messageId:D.messageId,accepted:K(k.to),rejected:[],response:`preview: ${B}`,envelope:D.envelope}}async verify(){return{ok:!0,provider:"preview"}}}export{V as PreviewTransport};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=6B7DD22F98FFF22F64756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/transports/preview.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @module\n * PreviewTransport — development transport that writes emails to disk\n * instead of sending them. Optionally opens in the default browser.\n * Use in place of a real transport during local development.\n *\n * @example\n * ```ts\n * import { PreviewTransport } from 'sently/transports/preview'\n * const transport = new PreviewTransport({ outDir: './.emails', open: true })\n * ```\n */\nimport { extractEmails } from \"../core/address.js\";\nimport { buildMIME } from \"../core/mime.js\";\nimport type {\n MailOptions,\n PreviewConfig,\n SendResult,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\nfunction sanitizeSubject(subject: string): string {\n const sanitized = subject.replace(/[^a-zA-Z0-9]+/g, \"-\").replace(/^-|-$/g, \"\");\n return sanitized.slice(0, 40) || \"no-subject\";\n}\n\nfunction getOpenCommand(platform: string): string {\n if (platform === \"darwin\") {\n return \"open\";\n }\n if (platform === \"win32\") {\n return \"start\";\n }\n return \"xdg-open\";\n}\n\n/**\n * Development transport that writes emails to disk instead of sending them.\n */\nexport class PreviewTransport implements Transport {\n /** Directory where preview `.eml` or `.html` files are written. */\n private readonly outDir: string;\n /** Whether to open the written file in the default browser. */\n private readonly open: boolean;\n /** Output format: full MIME (`.eml`) or HTML body only. */\n private readonly format: \"eml\" | \"html\";\n\n /** Creates a preview transport with optional output directory and format. */\n constructor(config?: PreviewConfig) {\n this.outDir = config?.outDir ?? \"./.emails\";\n this.open = config?.open ?? false;\n this.format = config?.format ?? \"eml\";\n }\n\n /** Writes the message to disk and returns a synthetic SendResult. */\n async send(options: MailOptions): Promise<SendResult> {\n const attachments = await resolveAttachments(options.attachments);\n const resolvedOptions = { ...options, attachments };\n const mime = await buildMIME(resolvedOptions);\n\n const fs = await import(\"node:fs/promises\");\n await fs.mkdir(this.outDir, { recursive: true });\n\n const filename = `${Date.now()}-${sanitizeSubject(options.subject)}.${this.format}`;\n const filepath = `${this.outDir}/${filename}`;\n\n if (this.format === \"html\") {\n const html =\n options.html ??\n (options.text\n ? `<pre>${options.text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")}</pre>`\n : \"\");\n await fs.writeFile(filepath, html, \"utf8\");\n } else {\n await fs.writeFile(filepath, mime.raw);\n }\n\n console.log(`[sently preview] Written: ${filepath}`);\n\n if (this.open) {\n const { spawn } = await import(\"node:child_process\");\n const command = getOpenCommand(process.platform);\n spawn(command, [filepath], { detached: true, stdio: \"ignore\" }).unref();\n }\n\n return {\n messageId: mime.messageId,\n accepted: extractEmails(options.to),\n rejected: [],\n response: `preview: ${filepath}`,\n envelope: mime.envelope,\n };\n }\n\n /** Always succeeds — preview transport requires no external connectivity. */\n async verify(): Promise<VerifyResult> {\n return { ok: true, provider: \"preview\" };\n }\n}\n"
|
|
5
|
+
"/**\n * @module\n * PreviewTransport — development transport that writes emails to disk\n * instead of sending them. Optionally opens in the default browser.\n * Use in place of a real transport during local development.\n *\n * @example\n * ```ts\n * import { PreviewTransport } from 'sently/transports/preview'\n * const transport = new PreviewTransport({ outDir: './.emails', open: true })\n * ```\n */\nimport { extractEmails } from \"../core/address.js\";\nimport { buildMIME } from \"../core/mime.js\";\nimport type {\n MailOptions,\n PreviewConfig,\n SendResult,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\nfunction sanitizeSubject(subject: string): string {\n const sanitized = subject.replace(/[^a-zA-Z0-9]+/g, \"-\").replace(/^-|-$/g, \"\");\n return sanitized.slice(0, 40) || \"no-subject\";\n}\n\nfunction getOpenCommand(platform: string): string {\n if (platform === \"darwin\") {\n return \"open\";\n }\n if (platform === \"win32\") {\n return \"start\";\n }\n return \"xdg-open\";\n}\n\n/**\n * Development transport that writes emails to disk instead of sending them.\n */\nexport class PreviewTransport implements Transport {\n readonly provider = \"preview\";\n\n /** Directory where preview `.eml` or `.html` files are written. */\n private readonly outDir: string;\n /** Whether to open the written file in the default browser. */\n private readonly open: boolean;\n /** Output format: full MIME (`.eml`) or HTML body only. */\n private readonly format: \"eml\" | \"html\";\n\n /** Creates a preview transport with optional output directory and format. */\n constructor(config?: PreviewConfig) {\n this.outDir = config?.outDir ?? \"./.emails\";\n this.open = config?.open ?? false;\n this.format = config?.format ?? \"eml\";\n }\n\n /** Writes the message to disk and returns a synthetic SendResult. */\n async send(options: MailOptions): Promise<SendResult> {\n const attachments = await resolveAttachments(options.attachments);\n const resolvedOptions = { ...options, attachments };\n const mime = await buildMIME(resolvedOptions);\n\n const fs = await import(\"node:fs/promises\");\n await fs.mkdir(this.outDir, { recursive: true });\n\n const filename = `${Date.now()}-${sanitizeSubject(options.subject)}.${this.format}`;\n const filepath = `${this.outDir}/${filename}`;\n\n if (this.format === \"html\") {\n const html =\n options.html ??\n (options.text\n ? `<pre>${options.text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")}</pre>`\n : \"\");\n await fs.writeFile(filepath, html, \"utf8\");\n } else {\n await fs.writeFile(filepath, mime.raw);\n }\n\n console.log(`[sently preview] Written: ${filepath}`);\n\n if (this.open) {\n const { spawn } = await import(\"node:child_process\");\n const command = getOpenCommand(process.platform);\n spawn(command, [filepath], { detached: true, stdio: \"ignore\" }).unref();\n }\n\n return {\n messageId: mime.messageId,\n accepted: extractEmails(options.to),\n rejected: [],\n response: `preview: ${filepath}`,\n envelope: mime.envelope,\n };\n }\n\n /** Always succeeds — preview transport requires no external connectivity. */\n async verify(): Promise<VerifyResult> {\n return { ok: true, provider: \"preview\" };\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "sNAuBA,IAAS,JAAe,JAAC,HAAyB,CAEhD,OADkB,EAAQ,QAAQ,iBAAkB,GAAG,EAAE,QAAQ,SAAU,EAAE,EAC5D,MAAM,EAAG,EAAE,GAAK,aAGnC,SAAS,CAAc,CAAC,EAA0B,CAChD,GAAI,IAAa,SACf,MAAO,OAET,GAAI,IAAa,QACf,MAAO,QAET,MAAO,WAMF,MAAM,CAAsC,CACxC,SAAW,UAGH,OAEA,KAEA,OAGjB,WAAW,CAAC,EAAwB,CAClC,KAAK,OAAS,GAAQ,QAAU,YAChC,KAAK,KAAO,GAAQ,MAAQ,GAC5B,KAAK,OAAS,GAAQ,QAAU,WAI5B,KAAI,CAAC,EAA2C,CACpD,IAAM,EAAc,MAAM,EAAmB,EAAQ,WAAW,EAC1D,EAAkB,IAAK,EAAS,aAAY,EAC5C,EAAO,MAAM,EAAU,CAAe,EAEtC,EAAK,KAAa,4BACxB,MAAM,EAAG,MAAM,KAAK,OAAQ,CAAE,UAAW,EAAK,CAAC,EAE/C,IAAM,EAAW,GAAG,KAAK,IAAI,KAAK,EAAgB,EAAQ,OAAO,KAAK,KAAK,SACrE,EAAW,GAAG,KAAK,UAAU,IAEnC,GAAI,KAAK,SAAW,OAAQ,CAC1B,IAAM,EACJ,EAAQ,OACP,EAAQ,KACL,QAAQ,EAAQ,KAAK,QAAQ,KAAM,OAAO,EAAE,QAAQ,KAAM,MAAM,EAAE,QAAQ,KAAM,MAAM,UACtF,IACN,MAAM,EAAG,UAAU,EAAU,EAAM,MAAM,EAEzC,WAAM,EAAG,UAAU,EAAU,EAAK,GAAG,EAKvC,GAFA,QAAQ,IAAI,6BAA6B,GAAU,EAE/C,KAAK,KAAM,CACb,IAAQ,SAAU,KAAa,8BACzB,EAAU,EAAe,QAAQ,QAAQ,EAC/C,EAAM,EAAS,CAAC,CAAQ,EAAG,CAAE,SAAU,GAAM,MAAO,QAAS,CAAC,EAAE,MAAM,EAGxE,MAAO,CACL,UAAW,EAAK,UAChB,SAAU,EAAc,EAAQ,EAAE,EAClC,SAAU,CAAC,EACX,SAAU,YAAY,IACtB,SAAU,EAAK,QACjB,OAII,OAAM,EAA0B,CACpC,MAAO,CAAE,GAAI,GAAM,SAAU,SAAU,EAE3C",
|
|
8
|
+
"debugId": "6B7DD22F98FFF22F64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -24,6 +24,7 @@ export declare class ResendError extends SentlyError {
|
|
|
24
24
|
* Resend HTTP API transport.
|
|
25
25
|
*/
|
|
26
26
|
export declare class ResendTransport implements Transport {
|
|
27
|
+
readonly provider = "resend";
|
|
27
28
|
/** Resend API key for Bearer authentication. */
|
|
28
29
|
private readonly apiKey;
|
|
29
30
|
/** Resend API base URL. */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{H as b}from"../chunk-8njyga62.js";import{I as X}from"../chunk-skqhj0rm.js";import{O as N}from"../chunk-0qxws3kj.js";import{Q as $,R as v,S as G}from"../chunk-93vqxxj2.js";import{Y as V}from"../chunk-ywq6s10g.js";import{ca as I,da as K}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";var Z=100;class Y extends I{statusCode;apiError;constructor(q,w,z){super(q,K(w),{statusCode:w,provider:"resend",cause:z});this.statusCode=w;this.apiError=z;this.name="ResendError"}}class H{provider="resend";apiKey;baseUrl;rateLimiter;batchMax=Z;constructor(q){this.apiKey=q.apiKey,this.baseUrl=q.baseUrl??"https://api.resend.com";let w=q.rateDelta??2;if(w>0)this.rateLimiter=new N(w,q.rateLimit??1000);else this.rateLimiter=null}async buildEmailBody(q){let w=await X(q.attachments),z=$(q.from)[0];return{from:z?v(z):"",to:G(q.to),subject:q.subject,...q.cc?{cc:G(q.cc)}:{},...q.bcc?{bcc:G(q.bcc)}:{},...q.replyTo?{reply_to:G(q.replyTo)}:{},...q.text?{text:q.text}:{},...q.html?{html:q.html}:{},...q.headers?{headers:q.headers}:{},...w.length>0?{attachments:w.map((F)=>({filename:F.filename,content:F.content instanceof Uint8Array?V(F.content).replace(/\r\n/g,""):F.content,...F.contentType?{content_type:F.contentType}:{}}))}:{}}}toSendResult(q,w){let z=$(q.from)[0];return{messageId:w.id??q.messageId??"",accepted:G(q.to),rejected:[],response:w.message??"Email sent",envelope:{from:z?.address??"",to:[...G(q.to),...q.cc?G(q.cc):[],...q.bcc?G(q.bcc):[]]},...w.batchError!==void 0?{batchError:w.batchError}:{}}}async send(q){let w=await this.buildEmailBody(q),z=b(q),F=await fetch(`${this.baseUrl}/emails`,{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json",...z?{"Idempotency-Key":z}:{}},body:JSON.stringify(w)}),O=await F.json();if(!F.ok)throw new Y(O.message??"Resend API error",F.status,O);return this.toSendResult(q,O)}async sendBatch(q){if(q.length===0)return[];let w=[];for(let z=0;z<q.length;z+=Z){if(z>0&&this.rateLimiter)await this.rateLimiter.acquire();let F=q.slice(z,z+Z),O=await Promise.all(F.map((J)=>this.buildEmailBody(J))),U=await fetch(`${this.baseUrl}/emails/batch`,{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(O)}),Q=await U.json();if(!U.ok)throw new Y(Q.message??"Resend batch API error",U.status,Q);let B=Q.data??[];for(let J=0;J<F.length;J++){let L=F[J],W=B[J];if(W?.id!==void 0){w.push(this.toSendResult(L,{id:W.id,...Q.message!==void 0?{message:Q.message}:{}}));continue}let P=W?.error??"Batch item failed";w.push(this.toSendResult(L,{message:P,batchError:new Y(P,U.status,W??{})}))}}return w}async verify(){try{let q=await fetch(`${this.baseUrl}/domains`,{headers:{Authorization:`Bearer ${this.apiKey}`}});if(!q.ok)return{ok:!1,provider:"resend",message:(await q.json().catch(()=>({}))).message??`HTTP ${q.status}`};return{ok:!0,provider:"resend",message:"API key is valid"}}catch(q){return{ok:!1,provider:"resend",message:q instanceof Error?q.message:String(q)}}}}export{H as ResendTransport,Y as ResendError,Z as RESEND_BATCH_MAX};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=3380FA39A339287764756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/transports/resend.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @module\n * Resend HTTP API transport for sending email via api.resend.com.\n *\n * @example\n * ```ts\n * import { ResendTransport } from \"sently/transports/resend\";\n * import { createMailer } from \"sently/mailer\";\n *\n * const mailer = await createMailer({\n * transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),\n * });\n *\n * await mailer.send({\n * from: \"onboarding@yourdomain.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * html: \"<p>Sent via Resend</p>\",\n * });\n * ```\n */\nimport { extractEmails, parseAddresses, toMIMEHeader } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport { resolveIdempotencyKey } from \"../core/idempotency-key.js\";\nimport { RateLimiter } from \"../core/rate-limiter.js\";\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Resend API configuration. */\nexport interface ResendConfig {\n /** Resend API key (starts with `re_`). */\n apiKey: string;\n /** API base URL. Default: `https://api.resend.com`. */\n baseUrl?: string;\n /** Max batch HTTP requests per rate window. Default: `2` (Resend API limit). Set `0` to disable. */\n rateDelta?: number;\n /** Rate limit window in milliseconds. Default: `1000`. */\n rateLimit?: number;\n}\n\n/** Maximum messages per Resend batch request (overridable per account). */\nexport const RESEND_BATCH_MAX = 100;\n\n/** Error thrown when the Resend API returns a non-success response. */\nexport class ResendError extends SentlyError {\n /** Creates a Resend API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"resend\",\n cause: apiError,\n });\n this.name = \"ResendError\";\n }\n}\n\n/**\n * Resend HTTP API transport.\n */\nexport class ResendTransport implements Transport {\n /** Resend API key for Bearer authentication. */\n private readonly apiKey: string;\n /** Resend API base URL. */\n private readonly baseUrl: string;\n /** Optional rate limiter for multi-chunk batch sends. */\n private readonly rateLimiter: RateLimiter | null;\n\n /** Maximum messages per batch HTTP request. */\n readonly batchMax = RESEND_BATCH_MAX;\n\n /** Creates a Resend transport with the given API key. */\n constructor(config: ResendConfig) {\n this.apiKey = config.apiKey;\n this.baseUrl = config.baseUrl ?? \"https://api.resend.com\";\n\n const rateDelta = config.rateDelta ?? 2;\n if (rateDelta > 0) {\n this.rateLimiter = new RateLimiter(rateDelta, config.rateLimit ?? 1000);\n } else {\n this.rateLimiter = null;\n }\n }\n\n /** Build the JSON body for a single Resend email. */\n private async buildEmailBody(options: MailOptions): Promise<Record<string, unknown>> {\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n return {\n from: from ? toMIMEHeader(from) : \"\",\n to: extractEmails(options.to),\n subject: options.subject,\n ...(options.cc ? { cc: extractEmails(options.cc) } : {}),\n ...(options.bcc ? { bcc: extractEmails(options.bcc) } : {}),\n ...(options.replyTo ? { reply_to: extractEmails(options.replyTo) } : {}),\n ...(options.text ? { text: options.text } : {}),\n ...(options.html ? { html: options.html } : {}),\n ...(options.headers ? { headers: options.headers } : {}),\n ...(attachments.length > 0\n ? {\n attachments: attachments.map((att) => ({\n filename: att.filename,\n content:\n att.content instanceof Uint8Array\n ? encodeBase64(att.content).replace(/\\r\\n/g, \"\")\n : att.content,\n ...(att.contentType ? { content_type: att.contentType } : {}),\n })),\n }\n : {}),\n };\n }\n\n /** Map a Resend API response id to a normalized SendResult. */\n private toSendResult(\n options: MailOptions,\n payload: { id?: string; message?: string; batchError?: unknown },\n ): SendResult {\n const from = parseAddresses(options.from)[0];\n return {\n messageId: payload.id ?? options.messageId ?? \"\",\n accepted: extractEmails(options.to),\n rejected: [],\n response: payload.message ?? \"Email sent\",\n envelope: {\n from: from?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n },\n ...(payload.batchError !== undefined ? { batchError: payload.batchError } : {}),\n };\n }\n\n /** Sends an email via the Resend HTTP API. */\n async send(options: MailOptions): Promise<SendResult> {\n const body = await this.buildEmailBody(options);\n const idempotencyKey = resolveIdempotencyKey(options);\n\n const response = await fetch(`${this.baseUrl}/emails`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n ...(idempotencyKey ? { \"Idempotency-Key\": idempotencyKey } : {}),\n },\n body: JSON.stringify(body),\n });\n\n const payload = (await response.json()) as { id?: string; message?: string };\n\n if (!response.ok) {\n throw new ResendError(payload.message ?? \"Resend API error\", response.status, payload);\n }\n\n return this.toSendResult(options, payload);\n }\n\n /** Send up to {@link RESEND_BATCH_MAX} attachment-free messages per batch request. */\n async sendBatch(messages: MailOptions[]): Promise<SendResult[]> {\n if (messages.length === 0) {\n return [];\n }\n\n const results: SendResult[] = [];\n for (let offset = 0; offset < messages.length; offset += RESEND_BATCH_MAX) {\n if (offset > 0 && this.rateLimiter) {\n await this.rateLimiter.acquire();\n }\n\n const chunk = messages.slice(offset, offset + RESEND_BATCH_MAX);\n const bodies = await Promise.all(chunk.map((message) => this.buildEmailBody(message)));\n\n const response = await fetch(`${this.baseUrl}/emails/batch`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(bodies),\n });\n\n const payload = (await response.json()) as {\n data?: Array<{ id?: string; error?: string }>;\n message?: string;\n };\n\n if (!response.ok) {\n throw new ResendError(\n payload.message ?? \"Resend batch API error\",\n response.status,\n payload,\n );\n }\n\n const ids = payload.data ?? [];\n for (let i = 0; i < chunk.length; i++) {\n const message = chunk[i] as MailOptions;\n const item = ids[i];\n if (item?.id !== undefined) {\n results.push(\n this.toSendResult(message, {\n id: item.id,\n ...(payload.message !== undefined ? { message: payload.message } : {}),\n }),\n );\n continue;\n }\n\n const errorMessage = item?.error ?? \"Batch item failed\";\n results.push(\n this.toSendResult(message, {\n message: errorMessage,\n batchError: new ResendError(errorMessage, response.status, item ?? {}),\n }),\n );\n }\n }\n\n return results;\n }\n\n /** Verifies the Resend API key by listing domains. */\n async verify(): Promise<VerifyResult> {\n try {\n const response = await fetch(`${this.baseUrl}/domains`, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n const payload = (await response.json().catch(() => ({}))) as { message?: string };\n return {\n ok: false,\n provider: \"resend\",\n message: payload.message ?? `HTTP ${response.status}`,\n };\n }\n\n return { ok: true, provider: \"resend\", message: \"API key is valid\" };\n } catch (err) {\n return {\n ok: false,\n provider: \"resend\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
|
|
5
|
+
"/**\n * @module\n * Resend HTTP API transport for sending email via api.resend.com.\n *\n * @example\n * ```ts\n * import { ResendTransport } from \"sently/transports/resend\";\n * import { createMailer } from \"sently/mailer\";\n *\n * const mailer = await createMailer({\n * transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),\n * });\n *\n * await mailer.send({\n * from: \"onboarding@yourdomain.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * html: \"<p>Sent via Resend</p>\",\n * });\n * ```\n */\nimport { extractEmails, parseAddresses, toMIMEHeader } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport { resolveIdempotencyKey } from \"../core/idempotency-key.js\";\nimport { RateLimiter } from \"../core/rate-limiter.js\";\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Resend API configuration. */\nexport interface ResendConfig {\n /** Resend API key (starts with `re_`). */\n apiKey: string;\n /** API base URL. Default: `https://api.resend.com`. */\n baseUrl?: string;\n /** Max batch HTTP requests per rate window. Default: `2` (Resend API limit). Set `0` to disable. */\n rateDelta?: number;\n /** Rate limit window in milliseconds. Default: `1000`. */\n rateLimit?: number;\n}\n\n/** Maximum messages per Resend batch request (overridable per account). */\nexport const RESEND_BATCH_MAX = 100;\n\n/** Error thrown when the Resend API returns a non-success response. */\nexport class ResendError extends SentlyError {\n /** Creates a Resend API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"resend\",\n cause: apiError,\n });\n this.name = \"ResendError\";\n }\n}\n\n/**\n * Resend HTTP API transport.\n */\nexport class ResendTransport implements Transport {\n readonly provider = \"resend\";\n\n /** Resend API key for Bearer authentication. */\n private readonly apiKey: string;\n /** Resend API base URL. */\n private readonly baseUrl: string;\n /** Optional rate limiter for multi-chunk batch sends. */\n private readonly rateLimiter: RateLimiter | null;\n\n /** Maximum messages per batch HTTP request. */\n readonly batchMax = RESEND_BATCH_MAX;\n\n /** Creates a Resend transport with the given API key. */\n constructor(config: ResendConfig) {\n this.apiKey = config.apiKey;\n this.baseUrl = config.baseUrl ?? \"https://api.resend.com\";\n\n const rateDelta = config.rateDelta ?? 2;\n if (rateDelta > 0) {\n this.rateLimiter = new RateLimiter(rateDelta, config.rateLimit ?? 1000);\n } else {\n this.rateLimiter = null;\n }\n }\n\n /** Build the JSON body for a single Resend email. */\n private async buildEmailBody(options: MailOptions): Promise<Record<string, unknown>> {\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n return {\n from: from ? toMIMEHeader(from) : \"\",\n to: extractEmails(options.to),\n subject: options.subject,\n ...(options.cc ? { cc: extractEmails(options.cc) } : {}),\n ...(options.bcc ? { bcc: extractEmails(options.bcc) } : {}),\n ...(options.replyTo ? { reply_to: extractEmails(options.replyTo) } : {}),\n ...(options.text ? { text: options.text } : {}),\n ...(options.html ? { html: options.html } : {}),\n ...(options.headers ? { headers: options.headers } : {}),\n ...(attachments.length > 0\n ? {\n attachments: attachments.map((att) => ({\n filename: att.filename,\n content:\n att.content instanceof Uint8Array\n ? encodeBase64(att.content).replace(/\\r\\n/g, \"\")\n : att.content,\n ...(att.contentType ? { content_type: att.contentType } : {}),\n })),\n }\n : {}),\n };\n }\n\n /** Map a Resend API response id to a normalized SendResult. */\n private toSendResult(\n options: MailOptions,\n payload: { id?: string; message?: string; batchError?: unknown },\n ): SendResult {\n const from = parseAddresses(options.from)[0];\n return {\n messageId: payload.id ?? options.messageId ?? \"\",\n accepted: extractEmails(options.to),\n rejected: [],\n response: payload.message ?? \"Email sent\",\n envelope: {\n from: from?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n },\n ...(payload.batchError !== undefined ? { batchError: payload.batchError } : {}),\n };\n }\n\n /** Sends an email via the Resend HTTP API. */\n async send(options: MailOptions): Promise<SendResult> {\n const body = await this.buildEmailBody(options);\n const idempotencyKey = resolveIdempotencyKey(options);\n\n const response = await fetch(`${this.baseUrl}/emails`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n ...(idempotencyKey ? { \"Idempotency-Key\": idempotencyKey } : {}),\n },\n body: JSON.stringify(body),\n });\n\n const payload = (await response.json()) as { id?: string; message?: string };\n\n if (!response.ok) {\n throw new ResendError(payload.message ?? \"Resend API error\", response.status, payload);\n }\n\n return this.toSendResult(options, payload);\n }\n\n /** Send up to {@link RESEND_BATCH_MAX} attachment-free messages per batch request. */\n async sendBatch(messages: MailOptions[]): Promise<SendResult[]> {\n if (messages.length === 0) {\n return [];\n }\n\n const results: SendResult[] = [];\n for (let offset = 0; offset < messages.length; offset += RESEND_BATCH_MAX) {\n if (offset > 0 && this.rateLimiter) {\n await this.rateLimiter.acquire();\n }\n\n const chunk = messages.slice(offset, offset + RESEND_BATCH_MAX);\n const bodies = await Promise.all(chunk.map((message) => this.buildEmailBody(message)));\n\n const response = await fetch(`${this.baseUrl}/emails/batch`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(bodies),\n });\n\n const payload = (await response.json()) as {\n data?: Array<{ id?: string; error?: string }>;\n message?: string;\n };\n\n if (!response.ok) {\n throw new ResendError(\n payload.message ?? \"Resend batch API error\",\n response.status,\n payload,\n );\n }\n\n const ids = payload.data ?? [];\n for (let i = 0; i < chunk.length; i++) {\n const message = chunk[i] as MailOptions;\n const item = ids[i];\n if (item?.id !== undefined) {\n results.push(\n this.toSendResult(message, {\n id: item.id,\n ...(payload.message !== undefined ? { message: payload.message } : {}),\n }),\n );\n continue;\n }\n\n const errorMessage = item?.error ?? \"Batch item failed\";\n results.push(\n this.toSendResult(message, {\n message: errorMessage,\n batchError: new ResendError(errorMessage, response.status, item ?? {}),\n }),\n );\n }\n }\n\n return results;\n }\n\n /** Verifies the Resend API key by listing domains. */\n async verify(): Promise<VerifyResult> {\n try {\n const response = await fetch(`${this.baseUrl}/domains`, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n const payload = (await response.json().catch(() => ({}))) as { message?: string };\n return {\n ok: false,\n provider: \"resend\",\n message: payload.message ?? `HTTP ${response.status}`,\n };\n }\n\n return { ok: true, provider: \"resend\", message: \"API key is valid\" };\n } catch (err) {\n return {\n ok: false,\n provider: \"resend\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "3380FA39A339287764756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -7,6 +7,7 @@ export declare class RetryTransport implements Transport {
|
|
|
7
7
|
private readonly inner;
|
|
8
8
|
/** Injectable sleep function for testing backoff timing. */
|
|
9
9
|
private readonly _sleep;
|
|
10
|
+
readonly provider = "retry";
|
|
10
11
|
/** Maximum send attempts including the initial try. */
|
|
11
12
|
private readonly maxAttempts;
|
|
12
13
|
/** Backoff strategy between retries. */
|
package/dist/transports/retry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{ca as G}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";var H=[429,500,502,503,504];function I(q,x,z){if(x==="exponential")return z*2**(q-1);if(x==="linear")return z*q;return z}function J(q,x){if(q instanceof G&&q.sentlyCode==="SMTP_AUTH_FAILED")return!1;if(typeof q==="object"&&q!==null&&"statusCode"in q&&typeof q.statusCode==="number")return x.includes(q.statusCode);return!0}class K{inner;_sleep;provider="retry";maxAttempts;backoff;baseDelay;retryOn;onRetry;mailerOnRetry;constructor(q,x,z=(B)=>new Promise((C)=>setTimeout(C,B))){this.inner=q;this._sleep=z;this.maxAttempts=x?.maxAttempts??3,this.backoff=x?.backoff??"exponential",this.baseDelay=x?.baseDelay??1000,this.retryOn=x?.retryOn??H,this.onRetry=x?.onRetry,this.mailerOnRetry=void 0}setMailerOnRetry(q){if(q!==void 0&&this.mailerOnRetry!==void 0&&this.mailerOnRetry!==q){if(!(typeof process<"u"&&process.env?.NODE_ENV==="production"))console.warn("[sently] RetryTransport.setMailerOnRetry: overwriting an active mailer onRetry callback. Do not share one RetryTransport instance across multiple mailers.")}this.mailerOnRetry=q}async send(q){let x;for(let z=1;z<=this.maxAttempts;z++)try{return await this.inner.send(q)}catch(B){if(x=B,z===this.maxAttempts)throw B;if(!J(B,this.retryOn))throw B;let C=I(z,this.backoff,this.baseDelay);this.onRetry?.(z,B),this.mailerOnRetry?.(z,B),await this._sleep(C)}throw x}async verify(){if(this.inner.verify)return this.inner.verify();return{ok:!0,provider:"retry"}}async close(){await this.inner.close?.()}async sendBatch(q){if(this.inner.sendBatch)return this.inner.sendBatch(q);return Promise.all(q.map((x)=>this.send(x)))}}export{K as RetryTransport};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=85CFC69CB504195764756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/transports/retry.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @module\n * RetryTransport — decorator that wraps any sently transport\n * and retries failed sends with configurable backoff.\n * Works with SMTP, Resend, SendGrid, SES, and any custom transport.\n *\n * @example\n * ```ts\n * import { RetryTransport } from 'sently/transports/retry'\n * import { ResendTransport } from 'sently/transports/resend'\n *\n * const transport = new RetryTransport(\n * new ResendTransport({ apiKey }),\n * { maxAttempts: 3, backoff: 'exponential', retryOn: [429, 503] }\n * )\n * ```\n */\nimport { SentlyError } from \"../core/errors.js\";\nimport type {\n MailOptions,\n RetryConfig,\n SendResult,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\n\nconst DEFAULT_RETRY_ON = [429, 500, 502, 503, 504];\n\nfunction computeDelay(attempt: number, backoff: string, base: number): number {\n if (backoff === \"exponential\") {\n return base * 2 ** (attempt - 1);\n }\n if (backoff === \"linear\") {\n return base * attempt;\n }\n return base;\n}\n\nfunction shouldRetry(err: unknown, retryOn: number[]): boolean {\n if (err instanceof SentlyError && err.sentlyCode === \"SMTP_AUTH_FAILED\") {\n return false;\n }\n\n if (\n typeof err === \"object\" &&\n err !== null &&\n \"statusCode\" in err &&\n typeof (err as { statusCode: unknown }).statusCode === \"number\"\n ) {\n return retryOn.includes((err as { statusCode: number }).statusCode);\n }\n\n return true;\n}\n\n/**\n * Decorator transport that retries failed sends with configurable backoff.\n */\nexport class RetryTransport implements Transport {\n /** Maximum send attempts including the initial try. */\n private readonly maxAttempts: number;\n /** Backoff strategy between retries. */\n private readonly backoff: \"exponential\" | \"linear\" | \"fixed\";\n /** Base delay in milliseconds before the first retry. */\n private readonly baseDelay: number;\n /** HTTP status codes that trigger a retry. */\n private readonly retryOn: number[];\n /** Optional callback invoked before each retry attempt. */\n private readonly onRetry: RetryConfig[\"onRetry\"];\n /** Additional onRetry callback wired by {@link MailerImpl} hooks per send. */\n private mailerOnRetry: RetryConfig[\"onRetry\"];\n\n /** Wraps an inner transport with retry logic and optional backoff configuration. */\n constructor(\n /** Transport that performs the actual send on each attempt. */\n private readonly inner: Transport,\n config?: RetryConfig,\n /** Injectable sleep function for testing backoff timing. */\n private readonly _sleep: (ms: number) => Promise<void> = (ms) =>\n new Promise((resolve) => setTimeout(resolve, ms)),\n ) {\n this.maxAttempts = config?.maxAttempts ?? 3;\n this.backoff = config?.backoff ?? \"exponential\";\n this.baseDelay = config?.baseDelay ?? 1000;\n this.retryOn = config?.retryOn ?? DEFAULT_RETRY_ON;\n this.onRetry = config?.onRetry;\n this.mailerOnRetry = undefined;\n }\n\n /**\n * Register a per-send onRetry callback from mailer lifecycle hooks.\n * Do not share one {@link RetryTransport} across multiple mailers — the last\n * `setMailerOnRetry` wins; overwriting an active callback logs a dev warning.\n * @internal Used by {@link MailerImpl}; cleared after each send.\n */\n setMailerOnRetry(callback: RetryConfig[\"onRetry\"] | undefined): void {\n if (\n callback !== undefined &&\n this.mailerOnRetry !== undefined &&\n this.mailerOnRetry !== callback\n ) {\n const isProduction = typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\";\n if (!isProduction) {\n console.warn(\n \"[sently] RetryTransport.setMailerOnRetry: overwriting an active mailer onRetry callback. Do not share one RetryTransport instance across multiple mailers.\",\n );\n }\n }\n this.mailerOnRetry = callback;\n }\n\n /** Sends with retries according to configured backoff and retry rules. */\n async send(options: MailOptions): Promise<SendResult> {\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n try {\n return await this.inner.send(options);\n } catch (err) {\n lastError = err;\n if (attempt === this.maxAttempts) {\n throw err;\n }\n if (!shouldRetry(err, this.retryOn)) {\n throw err;\n }\n const delay = computeDelay(attempt, this.backoff, this.baseDelay);\n this.onRetry?.(attempt, err);\n this.mailerOnRetry?.(attempt, err);\n await this._sleep(delay);\n }\n }\n\n throw lastError;\n }\n\n /** Delegates to the inner transport verify or returns a default success result. */\n async verify(): Promise<VerifyResult> {\n if (this.inner.verify) {\n return this.inner.verify();\n }\n return { ok: true, provider: \"retry\" };\n }\n\n /** Delegates close to the inner transport if available. */\n async close(): Promise<void> {\n await this.inner.close?.();\n }\n\n /** Delegates batch sends to the inner transport when available. */\n async sendBatch(messages: MailOptions[]): Promise<SendResult[]> {\n if (this.inner.sendBatch) {\n return this.inner.sendBatch(messages);\n }\n return Promise.all(messages.map((message) => this.send(message)));\n }\n}\n"
|
|
5
|
+
"/**\n * @module\n * RetryTransport — decorator that wraps any sently transport\n * and retries failed sends with configurable backoff.\n * Works with SMTP, Resend, SendGrid, SES, and any custom transport.\n *\n * @example\n * ```ts\n * import { RetryTransport } from 'sently/transports/retry'\n * import { ResendTransport } from 'sently/transports/resend'\n *\n * const transport = new RetryTransport(\n * new ResendTransport({ apiKey }),\n * { maxAttempts: 3, backoff: 'exponential', retryOn: [429, 503] }\n * )\n * ```\n */\nimport { SentlyError } from \"../core/errors.js\";\nimport type {\n MailOptions,\n RetryConfig,\n SendResult,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\n\nconst DEFAULT_RETRY_ON = [429, 500, 502, 503, 504];\n\nfunction computeDelay(attempt: number, backoff: string, base: number): number {\n if (backoff === \"exponential\") {\n return base * 2 ** (attempt - 1);\n }\n if (backoff === \"linear\") {\n return base * attempt;\n }\n return base;\n}\n\nfunction shouldRetry(err: unknown, retryOn: number[]): boolean {\n if (err instanceof SentlyError && err.sentlyCode === \"SMTP_AUTH_FAILED\") {\n return false;\n }\n\n if (\n typeof err === \"object\" &&\n err !== null &&\n \"statusCode\" in err &&\n typeof (err as { statusCode: unknown }).statusCode === \"number\"\n ) {\n return retryOn.includes((err as { statusCode: number }).statusCode);\n }\n\n return true;\n}\n\n/**\n * Decorator transport that retries failed sends with configurable backoff.\n */\nexport class RetryTransport implements Transport {\n readonly provider = \"retry\";\n\n /** Maximum send attempts including the initial try. */\n private readonly maxAttempts: number;\n /** Backoff strategy between retries. */\n private readonly backoff: \"exponential\" | \"linear\" | \"fixed\";\n /** Base delay in milliseconds before the first retry. */\n private readonly baseDelay: number;\n /** HTTP status codes that trigger a retry. */\n private readonly retryOn: number[];\n /** Optional callback invoked before each retry attempt. */\n private readonly onRetry: RetryConfig[\"onRetry\"];\n /** Additional onRetry callback wired by {@link MailerImpl} hooks per send. */\n private mailerOnRetry: RetryConfig[\"onRetry\"];\n\n /** Wraps an inner transport with retry logic and optional backoff configuration. */\n constructor(\n /** Transport that performs the actual send on each attempt. */\n private readonly inner: Transport,\n config?: RetryConfig,\n /** Injectable sleep function for testing backoff timing. */\n private readonly _sleep: (ms: number) => Promise<void> = (ms) =>\n new Promise((resolve) => setTimeout(resolve, ms)),\n ) {\n this.maxAttempts = config?.maxAttempts ?? 3;\n this.backoff = config?.backoff ?? \"exponential\";\n this.baseDelay = config?.baseDelay ?? 1000;\n this.retryOn = config?.retryOn ?? DEFAULT_RETRY_ON;\n this.onRetry = config?.onRetry;\n this.mailerOnRetry = undefined;\n }\n\n /**\n * Register a per-send onRetry callback from mailer lifecycle hooks.\n * Do not share one {@link RetryTransport} across multiple mailers — the last\n * `setMailerOnRetry` wins; overwriting an active callback logs a dev warning.\n * @internal Used by {@link MailerImpl}; cleared after each send.\n */\n setMailerOnRetry(callback: RetryConfig[\"onRetry\"] | undefined): void {\n if (\n callback !== undefined &&\n this.mailerOnRetry !== undefined &&\n this.mailerOnRetry !== callback\n ) {\n const isProduction = typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\";\n if (!isProduction) {\n console.warn(\n \"[sently] RetryTransport.setMailerOnRetry: overwriting an active mailer onRetry callback. Do not share one RetryTransport instance across multiple mailers.\",\n );\n }\n }\n this.mailerOnRetry = callback;\n }\n\n /** Sends with retries according to configured backoff and retry rules. */\n async send(options: MailOptions): Promise<SendResult> {\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n try {\n return await this.inner.send(options);\n } catch (err) {\n lastError = err;\n if (attempt === this.maxAttempts) {\n throw err;\n }\n if (!shouldRetry(err, this.retryOn)) {\n throw err;\n }\n const delay = computeDelay(attempt, this.backoff, this.baseDelay);\n this.onRetry?.(attempt, err);\n this.mailerOnRetry?.(attempt, err);\n await this._sleep(delay);\n }\n }\n\n throw lastError;\n }\n\n /** Delegates to the inner transport verify or returns a default success result. */\n async verify(): Promise<VerifyResult> {\n if (this.inner.verify) {\n return this.inner.verify();\n }\n return { ok: true, provider: \"retry\" };\n }\n\n /** Delegates close to the inner transport if available. */\n async close(): Promise<void> {\n await this.inner.close?.();\n }\n\n /** Delegates batch sends to the inner transport when available. */\n async sendBatch(messages: MailOptions[]): Promise<SendResult[]> {\n if (this.inner.sendBatch) {\n return this.inner.sendBatch(messages);\n }\n return Promise.all(messages.map((message) => this.send(message)));\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "4EA0BA,DAAM,EAAmB,CAAC,IAAK,IAAK,IAAK,IAAK,GAAG,EAEjD,SAAS,CAAY,CAAC,EAAiB,EAAiB,EAAsB,CAC5E,GAAI,IAAY,cACd,OAAO,EAAO,IAAM,EAAU,GAEhC,GAAI,IAAY,SACd,OAAO,EAAO,EAEhB,OAAO,EAGT,SAAS,CAAW,CAAC,EAAc,EAA4B,CAC7D,GAAI,aAAe,GAAe,EAAI,aAAe,mBACnD,MAAO,GAGT,GACE,OAAO,IAAQ,UACf,IAAQ,MACR,eAAgB,GAChB,OAAQ,EAAgC,aAAe,SAEvD,OAAO,EAAQ,SAAU,EAA+B,UAAU,EAGpE,MAAO,GAMF,MAAM,CAAoC,CAmB5B,MAGA,OArBV,SAAW,QAGH,YAEA,QAEA,UAEA,QAEA,QAET,cAGR,WAAW,CAEQ,EACjB,EAEiB,EAAwC,CAAC,IACxD,IAAI,QAAQ,CAAC,IAAY,WAAW,EAAS,CAAE,CAAC,EAClD,CALiB,aAGA,cAGjB,KAAK,YAAc,GAAQ,aAAe,EAC1C,KAAK,QAAU,GAAQ,SAAW,cAClC,KAAK,UAAY,GAAQ,WAAa,KACtC,KAAK,QAAU,GAAQ,SAAW,EAClC,KAAK,QAAU,GAAQ,QACvB,KAAK,cAAgB,OASvB,gBAAgB,CAAC,EAAoD,CACnE,GACE,IAAa,QACb,KAAK,gBAAkB,QACvB,KAAK,gBAAkB,GAGvB,GAAI,EADiB,OAAO,QAAY,KAAe,QAAQ,KAAK,WAAa,cAE/E,QAAQ,KACN,4JACF,EAGJ,KAAK,cAAgB,OAIjB,KAAI,CAAC,EAA2C,CACpD,IAAI,EAEJ,QAAS,EAAU,EAAG,GAAW,KAAK,YAAa,IACjD,GAAI,CACF,OAAO,MAAM,KAAK,MAAM,KAAK,CAAO,EACpC,MAAO,EAAK,CAEZ,GADA,EAAY,EACR,IAAY,KAAK,YACnB,MAAM,EAER,GAAI,CAAC,EAAY,EAAK,KAAK,OAAO,EAChC,MAAM,EAER,IAAM,EAAQ,EAAa,EAAS,KAAK,QAAS,KAAK,SAAS,EAChE,KAAK,UAAU,EAAS,CAAG,EAC3B,KAAK,gBAAgB,EAAS,CAAG,EACjC,MAAM,KAAK,OAAO,CAAK,EAI3B,MAAM,OAIF,OAAM,EAA0B,CACpC,GAAI,KAAK,MAAM,OACb,OAAO,KAAK,MAAM,OAAO,EAE3B,MAAO,CAAE,GAAI,GAAM,SAAU,OAAQ,OAIjC,MAAK,EAAkB,CAC3B,MAAM,KAAK,MAAM,QAAQ,OAIrB,UAAS,CAAC,EAAgD,CAC9D,GAAI,KAAK,MAAM,UACb,OAAO,KAAK,MAAM,UAAU,CAAQ,EAEtC,OAAO,QAAQ,IAAI,EAAS,IAAI,CAAC,IAAY,KAAK,KAAK,CAAO,CAAC,CAAC,EAEpE",
|
|
8
|
+
"debugId": "85CFC69CB504195764756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -16,6 +16,7 @@ export declare class SendGridError extends SentlyError {
|
|
|
16
16
|
* SendGrid v3 HTTP API transport.
|
|
17
17
|
*/
|
|
18
18
|
export declare class SendGridTransport implements Transport {
|
|
19
|
+
readonly provider = "sendgrid";
|
|
19
20
|
/** SendGrid API key for Bearer authentication. */
|
|
20
21
|
private readonly apiKey;
|
|
21
22
|
/** Creates a SendGrid transport with the given API key. */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{I as R}from"../chunk-skqhj0rm.js";import{Q as H,S as K}from"../chunk-93vqxxj2.js";import{Y as N}from"../chunk-ywq6s10g.js";import{ca as O,da as Q}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class M extends O{statusCode;apiError;constructor(q,w,D){super(q,Q(w),{statusCode:w,provider:"sendgrid",cause:D});this.statusCode=w;this.apiError=D;this.name="SendGridError"}}class U{provider="sendgrid";apiKey;constructor(q){this.apiKey=q.apiKey}buildPersonalization(q){return{to:H(q.to).map((w)=>({email:w.address,name:w.name})),...q.cc?{cc:H(q.cc).map((w)=>({email:w.address,name:w.name}))}:{},...q.bcc?{bcc:H(q.bcc).map((w)=>({email:w.address,name:w.name}))}:{},subject:q.subject,...q.headers?{headers:q.headers}:{}}}buildContent(q){return[...q.text?[{type:"text/plain",value:q.text}]:[],...q.html?[{type:"text/html",value:q.html}]:[]]}toSendResult(q,w,D="Accepted"){let J=H(q.from)[0];return{messageId:w,accepted:K(q.to),rejected:[],response:D,envelope:{from:J?.address??"",to:[...K(q.to),...q.cc?K(q.cc):[],...q.bcc?K(q.bcc):[]]}}}async send(q){let w=await R(q.attachments),D=H(q.from)[0],J={personalizations:[this.buildPersonalization(q)],from:D?{email:D.address,...D.name?{name:D.name}:{}}:{email:""},...q.replyTo?{reply_to:H(q.replyTo).map((z)=>({email:z.address,name:z.name}))[0]}:{},content:this.buildContent(q),...w.length>0?{attachments:w.map((z)=>({filename:z.filename,type:z.contentType??"application/octet-stream",content:z.content instanceof Uint8Array?N(z.content).replace(/\r\n/g,""):z.content,...z.contentId?{content_id:z.contentId}:{},disposition:z.inline?"inline":"attachment"}))}:{}},F=await fetch("https://api.sendgrid.com/v3/mail/send",{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!F.ok){let z=await F.text();throw new M("SendGrid API error",F.status,z)}let L=F.headers.get("x-message-id")??q.messageId??"";return this.toSendResult(q,L)}async sendBatch(q){if(q.length===0)return[];let w=q[0],D=H(w.from)[0],J={personalizations:q.map((z)=>this.buildPersonalization(z)),from:D?{email:D.address,...D.name?{name:D.name}:{}}:{email:""},...w.replyTo?{reply_to:H(w.replyTo).map((z)=>({email:z.address,name:z.name}))[0]}:{},content:this.buildContent(w)},F=await fetch("https://api.sendgrid.com/v3/mail/send",{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!F.ok){let z=await F.text();throw new M("SendGrid API error",F.status,z)}let L=F.headers.get("x-message-id")??"";return q.map((z)=>this.toSendResult(z,L||z.messageId||""))}async verify(){try{let q=await fetch("https://api.sendgrid.com/v3/user/profile",{headers:{Authorization:`Bearer ${this.apiKey}`}});if(!q.ok)return{ok:!1,provider:"sendgrid",message:await q.text().catch(()=>"")||`HTTP ${q.status}`};let w=await q.json();return{ok:!0,provider:"sendgrid",...w.username?{message:w.username}:{}}}catch(q){return{ok:!1,provider:"sendgrid",message:q instanceof Error?q.message:String(q)}}}}export{U as SendGridTransport,M as SendGridError};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=D36AE2D50C91F89564756E2164756E21
|