sently 0.5.2 → 0.6.1

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +61 -30
  3. package/dist/adapters/bun.js +1 -1
  4. package/dist/adapters/cf.js +2 -2
  5. package/dist/adapters/cf.js.map +1 -1
  6. package/dist/adapters/deno.js +1 -1
  7. package/dist/adapters/node.js +1 -1
  8. package/dist/auth/oauth2.js +1 -1
  9. package/dist/{chunk-zkczgj2c.js → chunk-348wndn3.js} +2 -2
  10. package/dist/{chunk-zkczgj2c.js.map → chunk-348wndn3.js.map} +1 -1
  11. package/dist/chunk-3y1tr1vt.js +4 -0
  12. package/dist/chunk-3y1tr1vt.js.map +11 -0
  13. package/dist/chunk-n601jhpd.js +4 -0
  14. package/dist/chunk-n601jhpd.js.map +10 -0
  15. package/dist/{chunk-c8ya9em0.js → chunk-qnz9z0d4.js} +2 -2
  16. package/dist/{chunk-c8ya9em0.js.map → chunk-qnz9z0d4.js.map} +1 -1
  17. package/dist/{chunk-mg8m26nr.js → chunk-rszb1n1d.js} +2 -2
  18. package/dist/{chunk-mg8m26nr.js.map → chunk-rszb1n1d.js.map} +1 -1
  19. package/dist/chunk-t0en64h9.js +4 -0
  20. package/dist/chunk-t0en64h9.js.map +10 -0
  21. package/dist/{chunk-vws105eb.js → chunk-tfxje74k.js} +2 -2
  22. package/dist/{chunk-vws105eb.js.map → chunk-tfxje74k.js.map} +1 -1
  23. package/dist/chunk-w3fnjh04.js +13 -0
  24. package/dist/chunk-w3fnjh04.js.map +11 -0
  25. package/dist/{chunk-gxq2aamv.js → chunk-wzdcz88x.js} +2 -2
  26. package/dist/{chunk-gxq2aamv.js.map → chunk-wzdcz88x.js.map} +1 -1
  27. package/dist/{chunk-b0qkndv9.js → chunk-y4qxdyqn.js} +3 -3
  28. package/dist/{chunk-b0qkndv9.js.map → chunk-y4qxdyqn.js.map} +1 -1
  29. package/dist/{chunk-aeqxkeh3.js → chunk-y72qggqf.js} +2 -2
  30. package/dist/{chunk-aeqxkeh3.js.map → chunk-y72qggqf.js.map} +1 -1
  31. package/dist/{chunk-k0gw3qmb.js → chunk-z7j2n9k8.js} +2 -2
  32. package/dist/{chunk-k0gw3qmb.js.map → chunk-z7j2n9k8.js.map} +1 -1
  33. package/dist/core/errors.d.ts +55 -0
  34. package/dist/core/errors.js +3 -0
  35. package/dist/core/errors.js.map +9 -0
  36. package/dist/core/smtp.d.ts +3 -3
  37. package/dist/core/smtp.js +2 -2
  38. package/dist/core/smtp.js.map +1 -1
  39. package/dist/core/types.d.ts +28 -1
  40. package/dist/detect.js +2 -2
  41. package/dist/detect.js.map +3 -3
  42. package/dist/dkim.js +2 -2
  43. package/dist/dkim.js.map +1 -1
  44. package/dist/errors.d.ts +29 -0
  45. package/dist/errors.js +8 -0
  46. package/dist/errors.js.map +9 -0
  47. package/dist/idempotency.js +2 -2
  48. package/dist/idempotency.js.map +1 -1
  49. package/dist/index.d.ts +9 -1
  50. package/dist/index.js +9 -0
  51. package/dist/mailer.d.ts +4 -3
  52. package/dist/mailer.js +2 -2
  53. package/dist/mailer.js.map +1 -1
  54. package/dist/plugins/react.js +2 -2
  55. package/dist/plugins/react.js.map +1 -1
  56. package/dist/plugins/template.js +1 -1
  57. package/dist/pool/pool.js +2 -2
  58. package/dist/pool/pool.js.map +2 -2
  59. package/dist/transports/brevo.d.ts +3 -3
  60. package/dist/transports/brevo.js +2 -2
  61. package/dist/transports/brevo.js.map +3 -3
  62. package/dist/transports/mailgun.d.ts +2 -1
  63. package/dist/transports/mailgun.js +2 -2
  64. package/dist/transports/mailgun.js.map +3 -3
  65. package/dist/transports/postmark.d.ts +2 -1
  66. package/dist/transports/postmark.js +2 -2
  67. package/dist/transports/postmark.js.map +3 -3
  68. package/dist/transports/preview.js +2 -2
  69. package/dist/transports/preview.js.map +1 -1
  70. package/dist/transports/resend.d.ts +2 -1
  71. package/dist/transports/resend.js +2 -2
  72. package/dist/transports/resend.js.map +3 -3
  73. package/dist/transports/retry.d.ts +9 -0
  74. package/dist/transports/retry.js +2 -2
  75. package/dist/transports/retry.js.map +3 -4
  76. package/dist/transports/sendgrid.d.ts +2 -1
  77. package/dist/transports/sendgrid.js +2 -2
  78. package/dist/transports/sendgrid.js.map +3 -3
  79. package/dist/transports/ses.d.ts +3 -3
  80. package/dist/transports/ses.js +5 -5
  81. package/dist/transports/ses.js.map +3 -3
  82. package/dist/transports/smtp.js +2 -2
  83. package/dist/transports/smtp.js.map +1 -1
  84. package/dist/webhooks/brevo.js +1 -1
  85. package/dist/webhooks/mailgun.js +1 -1
  86. package/dist/webhooks/postmark.js +1 -1
  87. package/dist/webhooks/resend.js +1 -1
  88. package/dist/webhooks/sendgrid.js +1 -1
  89. package/dist/webhooks/ses.js +1 -1
  90. package/dist/webhooks/timing-safe-equal.js +1 -1
  91. package/package.json +5 -1
  92. package/dist/chunk-9ea4hx7m.js +0 -4
  93. package/dist/chunk-9ea4hx7m.js.map +0 -11
  94. package/dist/chunk-9p4vev6j.js +0 -13
  95. package/dist/chunk-9p4vev6j.js.map +0 -11
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.1] — 2026-05-31
6
+
7
+ ### Fixed
8
+
9
+ - **Build** — emit `dist/core/errors.js` so the main barrel and Node/Deno smoke
10
+ tests resolve `SentlyError` imports after `bun run build`
11
+
12
+ ### Changed
13
+
14
+ - Bundle size budgets adjusted for minor measurement drift on CI runners
15
+
16
+ ## [0.6.0] — 2026-05-31
17
+
18
+ ### Added
19
+
20
+ - **`sently/errors`** — unified `SentlyError` base class with stable machine-readable
21
+ codes (`RATE_LIMITED`, `BAD_REQUEST`, `SMTP_AUTH_FAILED`, etc.); all existing
22
+ transport error classes now extend `SentlyError` while preserving their names,
23
+ constructor signatures, and public properties (**additive, backward-compatible**)
24
+ - **Mailer lifecycle hooks** — optional `hooks` on `createMailer` (`onSend`,
25
+ `onSuccess`, `onError`, `onRetry`) for metrics and tracing on every send;
26
+ hook context includes `to`, `subject`, `provider`, and `messageId` only (no
27
+ body/PII); `RetryTransport` wires `onRetry` per attempt (**additive**)
28
+ - **`AUDIT-v0.6.md`** — README capability audit with file/line proof (all claims
29
+ verified implemented, including CRAM-MD5)
30
+
31
+ ### Changed
32
+
33
+ - Bundle size budgets updated for expanded mailer (hooks) and error hierarchy
34
+
5
35
  ## [0.5.2] — 2026-05-31
6
36
 
7
37
  ### Fixed
package/README.md CHANGED
@@ -20,7 +20,7 @@ bun add sently
20
20
 
21
21
  | Feature | Nodemailer | sently |
22
22
  |---------|-----------|--------|
23
- | Bundle size | ~220 KB always | ~5.4 KB HTTP · ~14 KB SMTP |
23
+ | Bundle size | ~220 KB always | ~6.6 KB HTTP · ~15 KB SMTP |
24
24
  | Runtimes | Node.js only | Node, Bun, Deno, CF Workers |
25
25
  | Module format | CommonJS | ESM only |
26
26
  | Dependencies | 3 | 0 |
@@ -261,6 +261,8 @@ const mailer = await createMailer({
261
261
  });
262
262
  ```
263
263
 
264
+ Microsoft 365 uses the same `OAuth2Client` — pass `tokenUrl: MICROSOFT_TOKEN_URL` (from `sently/auth/oauth2`) on the `oauth2` config.
265
+
264
266
  #### Connection pooling
265
267
 
266
268
  ```typescript
@@ -367,6 +369,26 @@ console.log(result.sent, result.failed);
367
369
 
368
370
  Resend batches up to `RESEND_BATCH_MAX` (100) messages per request — export from `sently/transports/resend`.
369
371
 
372
+ ### Mailer lifecycle hooks
373
+
374
+ Optional observability hooks on `createMailer` fire for every `send()` and `sendBulk()` message (batch paths invoke hooks once per message, without double-firing). Hook context carries `{ messageId?, to, subject, provider }` — no body fields, to avoid leaking PII into logs.
375
+
376
+ ```typescript
377
+ const mailer = await createMailer({
378
+ transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
379
+ hooks: {
380
+ onSend: (ctx) => metrics.increment("email.send", { provider: ctx.provider }),
381
+ onSuccess: (ctx, result) => metrics.increment("email.success"),
382
+ onError: (ctx, err) => metrics.increment("email.error"),
383
+ onRetry: (ctx, attempt, err) => metrics.increment("email.retry", { attempt }),
384
+ },
385
+ });
386
+ ```
387
+
388
+ Hooks are fully optional and zero-cost when unset. A throwing hook does **not** break the send — in non-production environments the error is logged with `console.warn` and the send continues. Pair `onRetry` with `RetryTransport` for per-attempt retry metrics.
389
+
390
+ Works with both `sently/mailer` (`{ transport, hooks }`) and SMTP config (`{ host, auth, hooks }`).
391
+
370
392
  ### IdempotencyTransport
371
393
 
372
394
  Prevent duplicate sends on retry or replay. Wrap **outside** `RetryTransport` so all retry attempts share one key:
@@ -569,8 +591,10 @@ On Cloudflare Workers and browsers, use `content: Uint8Array` — `attachment.pa
569
591
 
570
592
  ## Error Handling
571
593
 
594
+ All transport errors extend `SentlyError` for unified handling while preserving existing class names and properties:
595
+
572
596
  ```typescript
573
- import { SMTPError } from "sently";
597
+ import { SentlyError, SMTPError } from "sently";
574
598
  import { ResendError } from "sently/transports/resend";
575
599
  // Each HTTP transport exports its own error class:
576
600
  // SendGridError → sently/transports/sendgrid
@@ -582,17 +606,22 @@ import { ResendError } from "sently/transports/resend";
582
606
  try {
583
607
  await mailer.send({ ... });
584
608
  } catch (err) {
609
+ if (err instanceof SentlyError) {
610
+ console.error(err.sentlyCode); // e.g. "BAD_REQUEST", "RATE_LIMITED"
611
+ console.error(err.statusCode); // HTTP status when applicable
612
+ }
585
613
  if (err instanceof SMTPError) {
586
- console.error(err.code); // SMTP response code, e.g. 550
587
- console.error(err.command); // failed command, e.g. "RCPT TO"
614
+ console.error(err.code); // SMTP response code, e.g. 550 (numeric)
615
+ console.error(err.command); // failed command, e.g. "RCPT TO"
588
616
  }
589
617
  if (err instanceof ResendError) {
590
618
  console.error(err.statusCode); // HTTP status code
619
+ console.error(err.code); // machine-readable, e.g. "BAD_REQUEST"
591
620
  }
592
621
  }
593
622
  ```
594
623
 
595
- Import error classes from their transport subpath not from `sently` core. Each exports a `statusCode` property on HTTP failures.
624
+ Use `sentlyCode` for unified machine-readable codes when a subclass shadows `code` (e.g. SMTP numeric codes, Brevo/SES provider API codes). Import error classes from their transport subpath — HTTP failures also expose `statusCode`.
596
625
 
597
626
  ---
598
627
 
@@ -648,20 +677,21 @@ All sizes are **minified + gzip**, measured by bundling each import path in isol
648
677
 
649
678
  | You send via… | Import | Why |
650
679
  |---------------|--------|-----|
651
- | Resend, SendGrid, Postmark, etc. | `sently/mailer` + `sently/transports/<provider>` | **~5.4 KB** — no SMTP code in the bundle |
652
- | SMTP relay (`host` / `port`) | `sently` | **~14 KB** — includes MIME + SMTP stack |
653
- | Raw transport, no plugins | `sently/transports/<provider>` only | **~4 KB** — skip `createMailer` wrapper |
654
- | DKIM signing | `sently/dkim` or `dkim` option on send | **~2 KB** add-on, lazy-loaded by MIME |
680
+ | Resend, SendGrid, Postmark, etc. | `sently/mailer` + `sently/transports/<provider>` | **~6.6 KB** — no SMTP code in the bundle |
681
+ | SMTP relay (`host` / `port`) | `sently` | **~15 KB** — includes MIME + SMTP stack |
682
+ | Raw transport, no plugins | `sently/transports/<provider>` only | **~4.7 KB** — skip `createMailer` wrapper |
683
+ | Unified errors | `sently/errors` or main barrel | **~0.3 KB** add-on |
684
+ | DKIM signing | `sently/dkim` or `dkim` option on send | **~1.7 KB** add-on, lazy-loaded by MIME |
655
685
  | React Email | `sently/react` + peers | **~0.3 KB** plugin shell (+ `@react-email/render` peer) |
656
686
  | Webhook parsing | `sently/webhooks` | **~0.5 KB** |
657
687
  | Idempotency | `sently/idempotency` | **~1.2 KB** |
658
688
 
659
689
  ```ts
660
- // Recommended — HTTP API (~5.4 KB bundled)
690
+ // Recommended — HTTP API (~6.6 KB bundled)
661
691
  import { createMailer } from "sently/mailer";
662
692
  import { ResendTransport } from "sently/transports/resend";
663
693
 
664
- // Avoid for HTTP-only apps — pulls SMTP into flat bundles (~15 KB)
694
+ // Avoid for HTTP-only apps — pulls SMTP into flat bundles (~16 KB)
665
695
  import { createMailer } from "sently";
666
696
  ```
667
697
 
@@ -669,12 +699,12 @@ import { createMailer } from "sently";
669
699
 
670
700
  | Use case | What you import | ~gzip |
671
701
  |----------|-----------------|-------|
672
- | HTTP — Resend | `sently/mailer` + `transports/resend` | **5.4 KB** |
673
- | HTTP — SendGrid | `sently/mailer` + `transports/sendgrid` | **5.2 KB** |
674
- | HTTP — transport only | `transports/resend` (call `.send()` directly) | **4.5 KB** |
675
- | SMTP relay | `sently` + `{ host, port, auth }` | **14.2 KB** |
676
- | SMTP + explicit adapter | `sently` + `adapters/node` | **14.2 KB** |
677
- | Main entry + HTTP ⚠️ | `sently` + `transports/resend` | **15.0 KB** |
702
+ | HTTP — Resend | `sently/mailer` + `transports/resend` | **6.6 KB** |
703
+ | HTTP — SendGrid | `sently/mailer` + `transports/sendgrid` | **6.4 KB** |
704
+ | HTTP — transport only | `transports/resend` (call `.send()` directly) | **4.7 KB** |
705
+ | SMTP relay | `sently` + `{ host, port, auth }` | **15.3 KB** |
706
+ | SMTP + explicit adapter | `sently` + `adapters/node` | **15.3 KB** |
707
+ | Main entry + HTTP ⚠️ | `sently` + `transports/resend` | **16.2 KB** |
678
708
 
679
709
  Adapters are **auto-selected at runtime** for SMTP unless you pass `adapter` explicitly. Only the adapter for your runtime is fetched (dynamic import).
680
710
 
@@ -682,8 +712,9 @@ Adapters are **auto-selected at runtime** for SMTP unless you pass `adapter` exp
682
712
 
683
713
  | Export | ~gzip | Notes |
684
714
  |--------|-------|-------|
685
- | `sently/mailer` | 1.4 KB | `createMailer({ transport })` — plugins, `sendBulk`, rate-limited batch |
686
- | `sently` | 14.1 KB | Full `createMailer` — SMTP config + lazy SMTP chunks |
715
+ | `sently/mailer` | 3.1 KB | `createMailer({ transport })` — plugins, hooks, `sendBulk`, rate-limited batch |
716
+ | `sently` | 15.3 KB | Full `createMailer` — SMTP config + lazy SMTP chunks |
717
+ | `sently/errors` | 0.3 KB | `SentlyError`, `httpStatusToSentlyCode`, `smtpCodeToSentlyCode` |
687
718
  | `sently/dkim` | 1.7 KB | `signDKIM`, `importPrivateKey` — loaded when `dkim` option is set |
688
719
  | `sently/react` | 0.3 KB | `reactPlugin()` — excludes `@react-email/render` peer |
689
720
  | `sently/idempotency` | 1.2 KB | `IdempotencyTransport`, `MemoryIdempotencyStore` |
@@ -693,13 +724,13 @@ Adapters are **auto-selected at runtime** for SMTP unless you pass `adapter` exp
693
724
 
694
725
  | Export | ~gzip | Protocol |
695
726
  |--------|-------|----------|
696
- | `sently/transports/resend` | 4.5 KB | HTTP (+ batch endpoint, rate limit) |
697
- | `sently/transports/sendgrid` | 4.0 KB | HTTP (+ batch personalizations) |
698
- | `sently/transports/postmark` | 3.9 KB | HTTP |
699
- | `sently/transports/mailgun` | 4.0 KB | HTTP |
700
- | `sently/transports/brevo` | 3.8 KB | HTTP |
701
- | `sently/transports/ses` | 7.3 KB | HTTP (SigV4) |
702
- | `sently/transports/smtp` | 10.0 KB | SMTP + MIME |
727
+ | `sently/transports/resend` | 4.7 KB | HTTP (+ batch endpoint, rate limit) |
728
+ | `sently/transports/sendgrid` | 4.2 KB | HTTP (+ batch personalizations) |
729
+ | `sently/transports/postmark` | 4.1 KB | HTTP |
730
+ | `sently/transports/mailgun` | 4.1 KB | HTTP |
731
+ | `sently/transports/brevo` | 3.9 KB | HTTP |
732
+ | `sently/transports/ses` | 7.5 KB | HTTP (SigV4) |
733
+ | `sently/transports/smtp` | 10.3 KB | SMTP + MIME |
703
734
  | `sently/transports/preview` | 6.4 KB | Dev disk preview |
704
735
 
705
736
  HTTP transports share MIME/address parsing (~3.8 KB). SES is larger due to SigV4 signing.
@@ -713,13 +744,13 @@ HTTP transports share MIME/address parsing (~3.8 KB). SES is larger due to SigV4
713
744
  | `sently/adapters/deno` | 0.5 KB | Deno |
714
745
  | `sently/adapters/cf` | 0.6 KB | Cloudflare Workers |
715
746
 
716
- ### What's inside an HTTP stack (~5.4 KB)
747
+ ### What's inside an HTTP stack (~6.6 KB)
717
748
 
718
749
  ```
719
- sently/mailer 1.4 KB createMailer wrapper, plugins, sendBulk + rate limit
720
- transports/resend 4.5 KB fetch client + MIME/address parsing + batch
750
+ sently/mailer 3.1 KB createMailer wrapper, plugins, hooks, sendBulk + rate limit
751
+ transports/resend 4.7 KB fetch client + MIME/address parsing + batch
721
752
  ─────
722
- total 5.4 KB vs Nodemailer ~220 KB
753
+ total 6.6 KB vs Nodemailer ~220 KB
723
754
  ```
724
755
 
725
756
  Regenerate tables after changes: `bun run measure:size` (full report) or `bun tools/measure-bundle-size.ts --markdown`.
@@ -1,3 +1,3 @@
1
- import"../chunk-mg8m26nr.js";import W from"node:net";import Q from"node:tls";function V(f){if(f.rejectUnauthorized===!1)console.warn("[sently] TLS certificate verification is disabled. Never use rejectUnauthorized: false in production.")}class X{socket=null;_secure;_connected=!1;connectionTimeout;tlsOptions;constructor(f={}){if(typeof Bun>"u")throw Error("BunAdapter requires the Bun runtime");this._secure=f.secure??!1,this.connectionTimeout=f.connectionTimeout??30000,this.tlsOptions=f.tls??{}}get secure(){return this._secure}get connected(){return this._connected}async connect(f,B){if(this._secure)await this.connectTls(f,B);else await this.connectPlain(f,B);this._connected=!0}async startTLS(f){if(!this.socket||this._secure)throw Error("Cannot STARTTLS: no plain socket available");let B=this.socket,y={...this.tlsOptions,...f};V(y),await new Promise((F,A)=>{let G=Q.connect({socket:B,servername:y.servername,rejectUnauthorized:y.rejectUnauthorized??!0,minVersion:y.minVersion});G.once("secureConnect",()=>{this.socket=G,this._secure=!0,F()}),G.once("error",A)})}async write(f){if(!this.socket)throw Error("Socket not connected");await new Promise((B,y)=>{this.socket?.write(Buffer.from(f),(F)=>{if(F)y(F);else B()})})}async*read(){if(!this.socket)throw Error("Socket not connected");let f=this.socket,B=[],y=null,F=!1,A=null,G=(H)=>{let I=new Uint8Array(H);if(y)y({value:I,done:!1}),y=null;else B.push(I)},J=(H)=>{if(A=H,F=!0,y)y({value:void 0,done:!0}),y=null},K=()=>{if(F=!0,y)y({value:void 0,done:!0}),y=null};f.on("data",G),f.on("error",J),f.on("close",K);try{while(!F||B.length>0){if(A)throw A;if(B.length>0){yield B.shift();continue}if(F)break;let H=await new Promise((I)=>{y=I});if(H.done)break;yield H.value}}finally{f.off("data",G),f.off("error",J),f.off("close",K)}}async close(){if(!this.socket)return;await new Promise((f)=>{this.socket?.end(()=>f())}),this.socket=null,this._connected=!1}connectPlain(f,B){return new Promise((y,F)=>{let A=W.connect({host:f,port:B},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}connectTls(f,B){return V(this.tlsOptions),new Promise((y,F)=>{let A=Q.connect({host:f,port:B,servername:this.tlsOptions.servername??f,rejectUnauthorized:this.tlsOptions.rejectUnauthorized??!0,minVersion:this.tlsOptions.minVersion},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}}export{X as BunAdapter};
1
+ import"../chunk-rszb1n1d.js";import W from"node:net";import Q from"node:tls";function V(f){if(f.rejectUnauthorized===!1)console.warn("[sently] TLS certificate verification is disabled. Never use rejectUnauthorized: false in production.")}class X{socket=null;_secure;_connected=!1;connectionTimeout;tlsOptions;constructor(f={}){if(typeof Bun>"u")throw Error("BunAdapter requires the Bun runtime");this._secure=f.secure??!1,this.connectionTimeout=f.connectionTimeout??30000,this.tlsOptions=f.tls??{}}get secure(){return this._secure}get connected(){return this._connected}async connect(f,B){if(this._secure)await this.connectTls(f,B);else await this.connectPlain(f,B);this._connected=!0}async startTLS(f){if(!this.socket||this._secure)throw Error("Cannot STARTTLS: no plain socket available");let B=this.socket,y={...this.tlsOptions,...f};V(y),await new Promise((F,A)=>{let G=Q.connect({socket:B,servername:y.servername,rejectUnauthorized:y.rejectUnauthorized??!0,minVersion:y.minVersion});G.once("secureConnect",()=>{this.socket=G,this._secure=!0,F()}),G.once("error",A)})}async write(f){if(!this.socket)throw Error("Socket not connected");await new Promise((B,y)=>{this.socket?.write(Buffer.from(f),(F)=>{if(F)y(F);else B()})})}async*read(){if(!this.socket)throw Error("Socket not connected");let f=this.socket,B=[],y=null,F=!1,A=null,G=(H)=>{let I=new Uint8Array(H);if(y)y({value:I,done:!1}),y=null;else B.push(I)},J=(H)=>{if(A=H,F=!0,y)y({value:void 0,done:!0}),y=null},K=()=>{if(F=!0,y)y({value:void 0,done:!0}),y=null};f.on("data",G),f.on("error",J),f.on("close",K);try{while(!F||B.length>0){if(A)throw A;if(B.length>0){yield B.shift();continue}if(F)break;let H=await new Promise((I)=>{y=I});if(H.done)break;yield H.value}}finally{f.off("data",G),f.off("error",J),f.off("close",K)}}async close(){if(!this.socket)return;await new Promise((f)=>{this.socket?.end(()=>f())}),this.socket=null,this._connected=!1}connectPlain(f,B){return new Promise((y,F)=>{let A=W.connect({host:f,port:B},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}connectTls(f,B){return V(this.tlsOptions),new Promise((y,F)=>{let A=Q.connect({host:f,port:B,servername:this.tlsOptions.servername??f,rejectUnauthorized:this.tlsOptions.rejectUnauthorized??!0,minVersion:this.tlsOptions.minVersion},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}}export{X as BunAdapter};
2
2
 
3
3
  //# debugId=94AAFD676203266764756E2164756E21
@@ -1,3 +1,3 @@
1
- import{R as q}from"../chunk-mg8m26nr.js";class w{socket=null;writer=null;_secure;_connected=!1;directTls;starttls;constructor(b={}){this._secure=b.secure??!1,this.directTls=b.secure??!1,this.starttls=b.starttls??!this.directTls}get secure(){return this._secure}get connected(){return this._connected}async connect(b,g){let{connect:j}=await import("cloudflare:sockets"),m=this.directTls?"on":this.starttls?"starttls":"off";this.socket=j({hostname:b,port:g},{secureTransport:m}),this.writer=this.socket.writable.getWriter(),this._connected=!0,this._secure=m==="on"}async startTLS(b){if(!this.socket||this._secure)throw Error("Cannot STARTTLS: no plain socket available");await this.writer?.close(),this.socket=this.socket.startTls(),this.writer=this.socket.writable.getWriter(),this._secure=!0}async write(b){if(!this.writer)throw Error("Socket not connected");await this.writer.write(b)}async*read(){if(!this.socket)throw Error("Socket not connected");let b=this.socket.readable.getReader();try{while(!0){let{value:g,done:j}=await b.read();if(j)break;if(g)yield g}}finally{b.releaseLock()}}async close(){await this.writer?.close(),await this.socket?.close(),this.writer=null,this.socket=null,this._connected=!1}}export{w as CloudflareAdapter};
1
+ import{V as q}from"../chunk-rszb1n1d.js";class w{socket=null;writer=null;_secure;_connected=!1;directTls;starttls;constructor(b={}){this._secure=b.secure??!1,this.directTls=b.secure??!1,this.starttls=b.starttls??!this.directTls}get secure(){return this._secure}get connected(){return this._connected}async connect(b,g){let{connect:j}=await import("cloudflare:sockets"),m=this.directTls?"on":this.starttls?"starttls":"off";this.socket=j({hostname:b,port:g},{secureTransport:m}),this.writer=this.socket.writable.getWriter(),this._connected=!0,this._secure=m==="on"}async startTLS(b){if(!this.socket||this._secure)throw Error("Cannot STARTTLS: no plain socket available");await this.writer?.close(),this.socket=this.socket.startTls(),this.writer=this.socket.writable.getWriter(),this._secure=!0}async write(b){if(!this.writer)throw Error("Socket not connected");await this.writer.write(b)}async*read(){if(!this.socket)throw Error("Socket not connected");let b=this.socket.readable.getReader();try{while(!0){let{value:g,done:j}=await b.read();if(j)break;if(g)yield g}}finally{b.releaseLock()}}async close(){await this.writer?.close(),await this.socket?.close(),this.writer=null,this.socket=null,this._connected=!1}}export{w as CloudflareAdapter};
2
2
 
3
- //# debugId=A2DFD63796CA67DB64756E2164756E21
3
+ //# debugId=0AEF1D7948DF0E9B64756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "/**\n * @module\n * Cloudflare Workers socket adapter for SMTP via cloudflare:sockets.\n *\n * @example\n * ```ts\n * import { CloudflareAdapter } from \"sently/adapters/cf\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new CloudflareAdapter(),\n * auth: { user: \"relay@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\ninterface CFSocket {\n readonly readable: ReadableStream<Uint8Array>;\n readonly writable: WritableStream<Uint8Array>;\n readonly closed: Promise<void>;\n close(): Promise<void>;\n startTls(): CFSocket;\n}\n\ntype CFConnect = (\n address: { hostname: string; port: number },\n options?: { secureTransport?: \"off\" | \"on\" | \"starttls\"; allowHalfOpen?: boolean },\n) => CFSocket;\n\n/** Configuration options for {@link CloudflareAdapter}. */\nexport interface CloudflareAdapterOptions {\n /** Use implicit TLS on connect (secureTransport: \"on\"). Default: false. */\n secure?: boolean;\n /** Enable STARTTLS upgrade after plain connect. Default: true when not secure. */\n starttls?: boolean;\n /** Reserved for future TLS tuning (Workers sockets API). */\n tls?: TLSOptions;\n}\n\n/**\n * Cloudflare Workers socket adapter via cloudflare:sockets.\n *\n * Limitations:\n * - No connection pooling (isolate lifecycle)\n * - No file system access for attachment.path\n * - No DNS MX lookup — explicit SMTP relay host required\n */\nexport class CloudflareAdapter implements SocketAdapter {\n /** Underlying Cloudflare Workers socket. */\n private socket: CFSocket | null = null;\n /** Writable stream writer for outbound bytes. */\n private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;\n /** Whether the connection is currently encrypted. */\n private _secure: boolean;\n /** Whether the socket is connected. */\n private _connected = false;\n /** Whether implicit TLS is used on connect (port 465 style). */\n private readonly directTls: boolean;\n /** Whether STARTTLS upgrade is enabled after plain connect. */\n private readonly starttls: boolean;\n\n /** Creates a Cloudflare Workers socket adapter. */\n constructor(options: CloudflareAdapterOptions = {}) {\n this._secure = options.secure ?? false;\n this.directTls = options.secure ?? false;\n this.starttls = options.starttls ?? !this.directTls;\n }\n\n /** Whether the connection uses TLS. */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the socket is currently connected. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** Opens a TCP or TLS connection to the given host and port. */\n async connect(host: string, port: number): Promise<void> {\n const { connect } = (await import(\"cloudflare:sockets\")) as { connect: CFConnect };\n\n const secureTransport = this.directTls ? \"on\" : this.starttls ? \"starttls\" : \"off\";\n this.socket = connect({ hostname: host, port }, { secureTransport });\n this.writer = this.socket.writable.getWriter();\n this._connected = true;\n this._secure = secureTransport === \"on\";\n }\n\n /** Upgrades a plain connection to TLS via STARTTLS. */\n async startTLS(_options?: TLSOptions): Promise<void> {\n if (!this.socket || this._secure) {\n throw new Error(\"Cannot STARTTLS: no plain socket available\");\n }\n\n await this.writer?.close();\n this.socket = this.socket.startTls();\n this.writer = this.socket.writable.getWriter();\n this._secure = true;\n }\n\n /** Writes raw bytes to the socket. */\n async write(data: Uint8Array): Promise<void> {\n if (!this.writer) {\n throw new Error(\"Socket not connected\");\n }\n await this.writer.write(data);\n }\n\n /** Reads incoming socket data as an async iterable of byte chunks. */\n async *read(): AsyncGenerator<Uint8Array, void, unknown> {\n if (!this.socket) {\n throw new Error(\"Socket not connected\");\n }\n\n const reader = this.socket.readable.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n break;\n }\n if (value) {\n yield value;\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n /** Closes the socket connection. */\n async close(): Promise<void> {\n await this.writer?.close();\n await this.socket?.close();\n this.writer = null;\n this.socket = null;\n this._connected = false;\n }\n}\n"
6
6
  ],
7
7
  "mappings": "yCAiDO,MAAM,CAA2C,CAE9C,OAA0B,KAE1B,OAAyD,KAEzD,QAEA,WAAa,GAEJ,UAEA,SAGjB,WAAW,CAAC,EAAoC,CAAC,EAAG,CAClD,KAAK,QAAU,EAAQ,QAAU,GACjC,KAAK,UAAY,EAAQ,QAAU,GACnC,KAAK,SAAW,EAAQ,UAAY,CAAC,KAAK,aAIxC,OAAM,EAAY,CACpB,OAAO,KAAK,WAIV,UAAS,EAAY,CACvB,OAAO,KAAK,gBAIR,QAAO,CAAC,EAAc,EAA6B,CACvD,IAAQ,WAAa,KAAa,8BAE5B,EAAkB,KAAK,UAAY,KAAO,KAAK,SAAW,WAAa,MAC7E,KAAK,OAAS,EAAQ,CAAE,SAAU,EAAM,MAAK,EAAG,CAAE,iBAAgB,CAAC,EACnE,KAAK,OAAS,KAAK,OAAO,SAAS,UAAU,EAC7C,KAAK,WAAa,GAClB,KAAK,QAAU,IAAoB,UAI/B,SAAQ,CAAC,EAAsC,CACnD,GAAI,CAAC,KAAK,QAAU,KAAK,QACvB,MAAU,MAAM,4CAA4C,EAG9D,MAAM,KAAK,QAAQ,MAAM,EACzB,KAAK,OAAS,KAAK,OAAO,SAAS,EACnC,KAAK,OAAS,KAAK,OAAO,SAAS,UAAU,EAC7C,KAAK,QAAU,QAIX,MAAK,CAAC,EAAiC,CAC3C,GAAI,CAAC,KAAK,OACR,MAAU,MAAM,sBAAsB,EAExC,MAAM,KAAK,OAAO,MAAM,CAAI,QAIvB,IAAI,EAA8C,CACvD,GAAI,CAAC,KAAK,OACR,MAAU,MAAM,sBAAsB,EAGxC,IAAM,EAAS,KAAK,OAAO,SAAS,UAAU,EAC9C,GAAI,CACF,MAAO,GAAM,CACX,IAAQ,QAAO,QAAS,MAAM,EAAO,KAAK,EAC1C,GAAI,EACF,MAEF,GAAI,EACF,MAAM,UAGV,CACA,EAAO,YAAY,QAKjB,MAAK,EAAkB,CAC3B,MAAM,KAAK,QAAQ,MAAM,EACzB,MAAM,KAAK,QAAQ,MAAM,EACzB,KAAK,OAAS,KACd,KAAK,OAAS,KACd,KAAK,WAAa,GAEtB",
8
- "debugId": "A2DFD63796CA67DB64756E2164756E21",
8
+ "debugId": "0AEF1D7948DF0E9B64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,3 +1,3 @@
1
- import"../chunk-mg8m26nr.js";class v{conn=null;_secure;_connected=!1;tlsOptions;constructor(j={}){if(typeof Deno>"u")throw Error("DenoAdapter requires the Deno runtime");this._secure=j.secure??!1,this.tlsOptions=j.tls??{}}get secure(){return this._secure}get connected(){return this._connected}async connect(j,q){if(this._secure)this.conn=await Deno.connectTls({hostname:j,port:q,...this.tlsOptions.servername?{servername:this.tlsOptions.servername}:{}});else this.conn=await Deno.connect({hostname:j,port:q});this._connected=!0}async startTLS(j){if(!this.conn||this._secure)throw Error("Cannot STARTTLS: no plain connection available");let q={...this.tlsOptions,...j};this.conn=await Deno.startTls(this.conn,{...q.servername?{hostname:q.servername}:{}}),this._secure=!0}async write(j){if(!this.conn)throw Error("Socket not connected");await this.conn.write(j)}async*read(){if(!this.conn)throw Error("Socket not connected");let j=new Uint8Array(8192);while(!0){let q=await this.conn.read(j);if(q===null)break;yield j.slice(0,q)}}async close(){this.conn?.close(),this.conn=null,this._connected=!1}}export{v as DenoAdapter};
1
+ import"../chunk-rszb1n1d.js";class v{conn=null;_secure;_connected=!1;tlsOptions;constructor(j={}){if(typeof Deno>"u")throw Error("DenoAdapter requires the Deno runtime");this._secure=j.secure??!1,this.tlsOptions=j.tls??{}}get secure(){return this._secure}get connected(){return this._connected}async connect(j,q){if(this._secure)this.conn=await Deno.connectTls({hostname:j,port:q,...this.tlsOptions.servername?{servername:this.tlsOptions.servername}:{}});else this.conn=await Deno.connect({hostname:j,port:q});this._connected=!0}async startTLS(j){if(!this.conn||this._secure)throw Error("Cannot STARTTLS: no plain connection available");let q={...this.tlsOptions,...j};this.conn=await Deno.startTls(this.conn,{...q.servername?{hostname:q.servername}:{}}),this._secure=!0}async write(j){if(!this.conn)throw Error("Socket not connected");await this.conn.write(j)}async*read(){if(!this.conn)throw Error("Socket not connected");let j=new Uint8Array(8192);while(!0){let q=await this.conn.read(j);if(q===null)break;yield j.slice(0,q)}}async close(){this.conn?.close(),this.conn=null,this._connected=!1}}export{v as DenoAdapter};
2
2
 
3
3
  //# debugId=433F0358DDA6910C64756E2164756E21
@@ -1,3 +1,3 @@
1
- import"../chunk-mg8m26nr.js";import W from"node:net";import Q from"node:tls";function V(f){if(f.rejectUnauthorized===!1)console.warn("[sently] TLS certificate verification is disabled. Never use rejectUnauthorized: false in production.")}class X{socket=null;_secure;_connected=!1;connectionTimeout;tlsOptions;constructor(f={}){this._secure=f.secure??!1,this.connectionTimeout=f.connectionTimeout??30000,this.tlsOptions=f.tls??{}}get secure(){return this._secure}get connected(){return this._connected}async connect(f,B){if(this._secure)await this.connectTls(f,B);else await this.connectPlain(f,B);this._connected=!0}async startTLS(f){if(!this.socket||this._secure)throw Error("Cannot STARTTLS: no plain socket available");let B=this.socket,y={...this.tlsOptions,...f};V(y),await new Promise((F,A)=>{let G=Q.connect({socket:B,servername:y.servername,rejectUnauthorized:y.rejectUnauthorized??!0,minVersion:y.minVersion});G.once("secureConnect",()=>{this.socket=G,this._secure=!0,F()}),G.once("error",A)})}async write(f){if(!this.socket)throw Error("Socket not connected");await new Promise((B,y)=>{this.socket?.write(Buffer.from(f),(F)=>{if(F)y(F);else B()})})}async*read(){if(!this.socket)throw Error("Socket not connected");let f=this.socket,B=[],y=null,F=!1,A=null,G=(H)=>{let I=new Uint8Array(H);if(y)y({value:I,done:!1}),y=null;else B.push(I)},J=(H)=>{if(A=H,y)y({value:void 0,done:!0}),y=null;F=!0},K=()=>{if(F=!0,y)y({value:void 0,done:!0}),y=null};f.on("data",G),f.on("error",J),f.on("close",K);try{while(!F||B.length>0){if(A)throw A;if(B.length>0){yield B.shift();continue}if(F)break;let H=await new Promise((I)=>{y=I});if(H.done)break;yield H.value}}finally{f.off("data",G),f.off("error",J),f.off("close",K)}}async close(){if(!this.socket)return;await new Promise((f)=>{this.socket?.end(()=>f())}),this.socket=null,this._connected=!1}connectPlain(f,B){return new Promise((y,F)=>{let A=W.connect({host:f,port:B},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}connectTls(f,B){return V(this.tlsOptions),new Promise((y,F)=>{let A=Q.connect({host:f,port:B,servername:this.tlsOptions.servername??f,rejectUnauthorized:this.tlsOptions.rejectUnauthorized??!0,minVersion:this.tlsOptions.minVersion},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}}export{X as NodeAdapter};
1
+ import"../chunk-rszb1n1d.js";import W from"node:net";import Q from"node:tls";function V(f){if(f.rejectUnauthorized===!1)console.warn("[sently] TLS certificate verification is disabled. Never use rejectUnauthorized: false in production.")}class X{socket=null;_secure;_connected=!1;connectionTimeout;tlsOptions;constructor(f={}){this._secure=f.secure??!1,this.connectionTimeout=f.connectionTimeout??30000,this.tlsOptions=f.tls??{}}get secure(){return this._secure}get connected(){return this._connected}async connect(f,B){if(this._secure)await this.connectTls(f,B);else await this.connectPlain(f,B);this._connected=!0}async startTLS(f){if(!this.socket||this._secure)throw Error("Cannot STARTTLS: no plain socket available");let B=this.socket,y={...this.tlsOptions,...f};V(y),await new Promise((F,A)=>{let G=Q.connect({socket:B,servername:y.servername,rejectUnauthorized:y.rejectUnauthorized??!0,minVersion:y.minVersion});G.once("secureConnect",()=>{this.socket=G,this._secure=!0,F()}),G.once("error",A)})}async write(f){if(!this.socket)throw Error("Socket not connected");await new Promise((B,y)=>{this.socket?.write(Buffer.from(f),(F)=>{if(F)y(F);else B()})})}async*read(){if(!this.socket)throw Error("Socket not connected");let f=this.socket,B=[],y=null,F=!1,A=null,G=(H)=>{let I=new Uint8Array(H);if(y)y({value:I,done:!1}),y=null;else B.push(I)},J=(H)=>{if(A=H,y)y({value:void 0,done:!0}),y=null;F=!0},K=()=>{if(F=!0,y)y({value:void 0,done:!0}),y=null};f.on("data",G),f.on("error",J),f.on("close",K);try{while(!F||B.length>0){if(A)throw A;if(B.length>0){yield B.shift();continue}if(F)break;let H=await new Promise((I)=>{y=I});if(H.done)break;yield H.value}}finally{f.off("data",G),f.off("error",J),f.off("close",K)}}async close(){if(!this.socket)return;await new Promise((f)=>{this.socket?.end(()=>f())}),this.socket=null,this._connected=!1}connectPlain(f,B){return new Promise((y,F)=>{let A=W.connect({host:f,port:B},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}connectTls(f,B){return V(this.tlsOptions),new Promise((y,F)=>{let A=Q.connect({host:f,port:B,servername:this.tlsOptions.servername??f,rejectUnauthorized:this.tlsOptions.rejectUnauthorized??!0,minVersion:this.tlsOptions.minVersion},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}}export{X as NodeAdapter};
2
2
 
3
3
  //# debugId=81CB75805F76C9AC64756E2164756E21
@@ -1,3 +1,3 @@
1
- import{n as a,o as b,p as c}from"../chunk-zkczgj2c.js";import"../chunk-gxq2aamv.js";import"../chunk-mg8m26nr.js";export{c as OAuth2Client,b as MICROSOFT_TOKEN_URL,a as GOOGLE_TOKEN_URL};
1
+ import{n as a,o as b,p as c}from"../chunk-348wndn3.js";import"../chunk-wzdcz88x.js";import"../chunk-rszb1n1d.js";export{c as OAuth2Client,b as MICROSOFT_TOKEN_URL,a as GOOGLE_TOKEN_URL};
2
2
 
3
3
  //# debugId=E1346533621D4FAE64756E2164756E21
@@ -1,4 +1,4 @@
1
- import{N as D,Q as J}from"./chunk-gxq2aamv.js";var Q="https://oauth2.googleapis.com/token",$="https://login.microsoftonline.com/common/oauth2/v2.0/token",V=30000;class W{config;cachedToken=null;expiresAt=0;refreshPromise=null;constructor(m){if(this.config=m,m.accessToken)this.cachedToken=m.accessToken,this.expiresAt=Date.now()+3600000}async getAccessToken(){if(this.config.getToken)return this.config.getToken();if(this.cachedToken&&Date.now()<this.expiresAt-V)return this.cachedToken;if(!this.refreshPromise)this.refreshPromise=this.refreshAccessToken().finally(()=>{this.refreshPromise=null});return this.refreshPromise}async refreshAccessToken(){if(this.config.getToken){let v=await this.config.getToken();return this.cachedToken=v,this.expiresAt=Date.now()+3600000,v}let m=this.config.tokenUrl??Q,z=new URLSearchParams({client_id:this.config.clientId,client_secret:this.config.clientSecret,refresh_token:this.config.refreshToken,grant_type:"refresh_token"}),q=await fetch(m,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:z.toString()});if(!q.ok){let v=await q.text();throw Error(`OAuth2 token refresh failed (${q.status}): ${v}`)}let C=await q.json();return this.cachedToken=C.access_token,this.expiresAt=Date.now()+C.expires_in*1000,C.access_token}async buildXOAUTH2(){let m=await this.getAccessToken(),z=`user=${this.config.user}\x01auth=Bearer ${m}\x01\x01`;return D(J(z)).replace(/\r\n/g,"")}}
1
+ import{O as D,R as J}from"./chunk-wzdcz88x.js";var Q="https://oauth2.googleapis.com/token",$="https://login.microsoftonline.com/common/oauth2/v2.0/token",V=30000;class W{config;cachedToken=null;expiresAt=0;refreshPromise=null;constructor(m){if(this.config=m,m.accessToken)this.cachedToken=m.accessToken,this.expiresAt=Date.now()+3600000}async getAccessToken(){if(this.config.getToken)return this.config.getToken();if(this.cachedToken&&Date.now()<this.expiresAt-V)return this.cachedToken;if(!this.refreshPromise)this.refreshPromise=this.refreshAccessToken().finally(()=>{this.refreshPromise=null});return this.refreshPromise}async refreshAccessToken(){if(this.config.getToken){let v=await this.config.getToken();return this.cachedToken=v,this.expiresAt=Date.now()+3600000,v}let m=this.config.tokenUrl??Q,z=new URLSearchParams({client_id:this.config.clientId,client_secret:this.config.clientSecret,refresh_token:this.config.refreshToken,grant_type:"refresh_token"}),q=await fetch(m,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:z.toString()});if(!q.ok){let v=await q.text();throw Error(`OAuth2 token refresh failed (${q.status}): ${v}`)}let C=await q.json();return this.cachedToken=C.access_token,this.expiresAt=Date.now()+C.expires_in*1000,C.access_token}async buildXOAUTH2(){let m=await this.getAccessToken(),z=`user=${this.config.user}\x01auth=Bearer ${m}\x01\x01`;return D(J(z)).replace(/\r\n/g,"")}}
2
2
  export{Q as n,$ as o,W as p};
3
3
 
4
- //# debugId=FAE5F0B424F38FDA64756E2164756E21
4
+ //# debugId=46CE46483A53D9B364756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "/**\n * @module\n * OAuth2 / XOAUTH2 authentication for SMTP.\n * Supports Gmail, Microsoft 365, and custom OAuth2 providers.\n *\n * @example\n * ```ts\n * import { OAuth2Client } from \"sently/auth/oauth2\";\n * const client = new OAuth2Client({\n * user: \"me@gmail.com\",\n * clientId: \"...\",\n * clientSecret: \"...\",\n * refreshToken: \"...\",\n * });\n * const token = await client.getAccessToken();\n * ```\n */\nimport { encodeBase64, encodeUtf8 } from \"../core/base64.js\";\nimport type { OAuth2Config } from \"../core/types.js\";\n\n/** Default Google OAuth2 token endpoint. */\nexport const GOOGLE_TOKEN_URL = \"https://oauth2.googleapis.com/token\";\n\n/** Microsoft OAuth2 token endpoint (common tenant). */\nexport const MICROSOFT_TOKEN_URL = \"https://login.microsoftonline.com/common/oauth2/v2.0/token\";\n\n/** OAuth2 token endpoint response shape. */\nexport interface TokenResponse {\n /** Bearer access token for API or SMTP XOAUTH2. */\n access_token: string;\n /** Token lifetime in seconds. */\n expires_in: number;\n /** Token type (typically `\"Bearer\"`). */\n token_type: string;\n}\n\nconst EXPIRY_BUFFER_MS = 30_000;\n\n/**\n * OAuth2 client with in-memory token cache and automatic refresh.\n */\nexport class OAuth2Client {\n /** OAuth2 configuration supplied at construction. */\n private readonly config: OAuth2Config;\n /** Cached access token, or null before the first fetch. */\n private cachedToken: string | null = null;\n /** Expiry timestamp (ms) for the cached access token. */\n private expiresAt = 0;\n /** In-flight refresh promise used to deduplicate concurrent refreshes. */\n private refreshPromise: Promise<string> | null = null;\n\n /** Creates an OAuth2 client from configuration. */\n constructor(config: OAuth2Config) {\n this.config = config;\n if (config.accessToken) {\n this.cachedToken = config.accessToken;\n this.expiresAt = Date.now() + 3_600_000;\n }\n }\n\n /**\n * Get a valid access token (cached when still valid, refreshed otherwise).\n */\n async getAccessToken(): Promise<string> {\n if (this.config.getToken) {\n return this.config.getToken();\n }\n\n if (this.cachedToken && Date.now() < this.expiresAt - EXPIRY_BUFFER_MS) {\n return this.cachedToken;\n }\n\n if (!this.refreshPromise) {\n this.refreshPromise = this.refreshAccessToken().finally(() => {\n this.refreshPromise = null;\n });\n }\n return this.refreshPromise;\n }\n\n /**\n * Force-refresh the access token regardless of expiry.\n */\n async refreshAccessToken(): Promise<string> {\n if (this.config.getToken) {\n const token = await this.config.getToken();\n this.cachedToken = token;\n this.expiresAt = Date.now() + 3_600_000;\n return token;\n }\n\n const tokenUrl = this.config.tokenUrl ?? GOOGLE_TOKEN_URL;\n const body = new URLSearchParams({\n client_id: this.config.clientId,\n client_secret: this.config.clientSecret,\n refresh_token: this.config.refreshToken,\n grant_type: \"refresh_token\",\n });\n\n const response = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`OAuth2 token refresh failed (${response.status}): ${text}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n this.cachedToken = data.access_token;\n this.expiresAt = Date.now() + data.expires_in * 1000;\n return data.access_token;\n }\n\n /**\n * Build the XOAUTH2 SASL string for SMTP AUTH (base64-encoded).\n */\n async buildXOAUTH2(): Promise<string> {\n const token = await this.getAccessToken();\n const raw = `user=${this.config.user}\\x01auth=Bearer ${token}\\x01\\x01`;\n return encodeBase64(encodeUtf8(raw)).replace(/\\r\\n/g, \"\");\n }\n}\n"
6
6
  ],
7
7
  "mappings": "+CAqBO,IAAM,EAAmB,sCAGnB,EAAsB,6DAY7B,EAAmB,MAKlB,MAAM,CAAa,CAEP,OAET,YAA6B,KAE7B,UAAY,EAEZ,eAAyC,KAGjD,WAAW,CAAC,EAAsB,CAEhC,GADA,KAAK,OAAS,EACV,EAAO,YACT,KAAK,YAAc,EAAO,YAC1B,KAAK,UAAY,KAAK,IAAI,EAAI,aAO5B,eAAc,EAAoB,CACtC,GAAI,KAAK,OAAO,SACd,OAAO,KAAK,OAAO,SAAS,EAG9B,GAAI,KAAK,aAAe,KAAK,IAAI,EAAI,KAAK,UAAY,EACpD,OAAO,KAAK,YAGd,GAAI,CAAC,KAAK,eACR,KAAK,eAAiB,KAAK,mBAAmB,EAAE,QAAQ,IAAM,CAC5D,KAAK,eAAiB,KACvB,EAEH,OAAO,KAAK,oBAMR,mBAAkB,EAAoB,CAC1C,GAAI,KAAK,OAAO,SAAU,CACxB,IAAM,EAAQ,MAAM,KAAK,OAAO,SAAS,EAGzC,OAFA,KAAK,YAAc,EACnB,KAAK,UAAY,KAAK,IAAI,EAAI,QACvB,EAGT,IAAM,EAAW,KAAK,OAAO,UAAY,EACnC,EAAO,IAAI,gBAAgB,CAC/B,UAAW,KAAK,OAAO,SACvB,cAAe,KAAK,OAAO,aAC3B,cAAe,KAAK,OAAO,aAC3B,WAAY,eACd,CAAC,EAEK,EAAW,MAAM,MAAM,EAAU,CACrC,OAAQ,OACR,QAAS,CAAE,eAAgB,mCAAoC,EAC/D,KAAM,EAAK,SAAS,CACtB,CAAC,EAED,GAAI,CAAC,EAAS,GAAI,CAChB,IAAM,EAAO,MAAM,EAAS,KAAK,EACjC,MAAU,MAAM,gCAAgC,EAAS,YAAY,GAAM,EAG7E,IAAM,EAAQ,MAAM,EAAS,KAAK,EAGlC,OAFA,KAAK,YAAc,EAAK,aACxB,KAAK,UAAY,KAAK,IAAI,EAAI,EAAK,WAAa,KACzC,EAAK,kBAMR,aAAY,EAAoB,CACpC,IAAM,EAAQ,MAAM,KAAK,eAAe,EAClC,EAAM,QAAQ,KAAK,OAAO,uBAAuB,YACvD,OAAO,EAAa,EAAW,CAAG,CAAC,EAAE,QAAQ,QAAS,EAAE,EAE5D",
8
- "debugId": "FAE5F0B424F38FDA64756E2164756E21",
8
+ "debugId": "46CE46483A53D9B364756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -0,0 +1,4 @@
1
+ import{v as I}from"./chunk-t0en64h9.js";import{w as M}from"./chunk-tfxje74k.js";import{A as R}from"./chunk-y4qxdyqn.js";async function j(B,G){if(!G||G.length===0)return B;let K=B;for(let Y of G)K=await Y(K);return K}async function k(B){return new q(B.transport,B.plugins??[],B.hooks)}function D(B){return B.attachments!==void 0&&B.attachments.length>0}function f(B){let G=B.constructor.name;if(G==="Object")return"custom";if(G.endsWith("Transport"))return G.slice(0,-9).toLowerCase();return G.toLowerCase()||"unknown"}function C(B,G){return{...B.messageId!==void 0?{messageId:B.messageId}:{},to:R(B.to),subject:B.subject,provider:f(G)}}async function T(B,...G){if(B===void 0)return;try{await B(...G)}catch(K){if(!(typeof process<"u"&&process.env?.NODE_ENV==="production"))console.warn("[sently] Mailer hook threw; send continues:",K)}}class q{transport;plugins;hooks;constructor(B,G=[],K){this.transport=B;this.plugins=G;this.hooks=K}async send(B){let G=await j(B,this.plugins),K=C(G,this.transport);if(await T(this.hooks?.onSend,K),this.hooks?.onRetry!==void 0&&this.transport instanceof I)this.transport.setMailerOnRetry((Y,Z)=>{T(this.hooks?.onRetry,K,Y,Z)});try{let Y=await this.transport.send(G),Z={...K,messageId:Y.messageId};return await T(this.hooks?.onSuccess,Z,Y),Y}catch(Y){throw await T(this.hooks?.onError,K,Y),Y}finally{if(this.transport instanceof I)this.transport.setMailerOnRetry(void 0)}}async processMessage(B){return j(B,this.plugins)}async sendBulk(B,G){if(B.length===0)return{total:0,sent:0,failed:0,results:[]};let K=Array(B.length),Y=G?.stopOnError??!1,Z=!1,S=async(_,$,Q,V)=>{if(K[$]={status:"sent",result:Q},V!==void 0&&this.hooks!==void 0){let W=C(V,this.transport);await T(this.hooks.onSend,W),await T(this.hooks.onSuccess,{...W,messageId:Q.messageId},Q)}G?.onSuccess?.(_,$,Q)},L=async(_,$,Q,V)=>{if(K[$]={status:"failed",error:Q},V!==void 0&&this.hooks!==void 0){let W=C(V,this.transport);await T(this.hooks.onSend,W),await T(this.hooks.onError,W,Q)}if(G?.onError?.(_,$,Q),Y)Z=!0};if(this.transport.sendBatch){let _=[],$=[];for(let[Q,V]of B.entries())if(D(V))$.push({index:Q,message:V});else _.push({index:Q,message:V});if(_.length>0&&!Z){let Q=this.transport.batchMax??_.length,V=G?.rateDelta??2,W=V>0?new M(V,G?.rateLimit??1000,G?.now):null;for(let A=0;A<_.length&&!Z;A+=Q){let X=_.slice(A,A+Q);if(W)await W.acquire();try{let w=await Promise.all(X.map(({message:J})=>this.processMessage(J))),U=await this.transport.sendBatch(w);for(let J=0;J<X.length;J++){let z=X[J],P=w[J],F=U[J];if(F===void 0)await L(z.message,z.index,Error("Batch response missing result for message"),P);else if(F.batchError!==void 0)await L(z.message,z.index,F.batchError,P);else await S(z.message,z.index,F,P)}}catch(w){let U=await Promise.all(X.map(({message:J})=>this.processMessage(J)));for(let J=0;J<X.length;J++){if(Z)break;let z=X[J];await L(z.message,z.index,w,U[J])}}}}if($.length>0&&!Z){let Q=G?.concurrency??1,V=[...$],W=0;await new Promise((A)=>{let X=()=>{if(V.length===0&&W===0||Z)A()},w=()=>{if(V.length===0||Z){X();return}let U=V.shift();if(U===void 0){X();return}W++,this.send(U.message).then((J)=>{K[U.index]={status:"sent",result:J},G?.onSuccess?.(U.message,U.index,J)}).catch((J)=>{if(K[U.index]={status:"failed",error:J},G?.onError?.(U.message,U.index,J),Y)Z=!0}).finally(()=>{W--,w(),X()})};for(let U=0;U<Q;U++)w()})}}else{let _=G?.concurrency??1,$=[...B.entries()],Q=0;await new Promise((V)=>{let W=()=>{if($.length===0&&Q===0||Z)V()},A=()=>{if($.length===0||Z){W();return}let X=$.shift();if(X===void 0){W();return}let[w,U]=X;Q++,this.send(U).then((J)=>{K[w]={status:"sent",result:J},G?.onSuccess?.(U,w,J)}).catch((J)=>{if(K[w]={status:"failed",error:J},G?.onError?.(U,w,J),Y)Z=!0}).finally(()=>{Q--,A(),W()})};for(let X=0;X<_;X++)A()})}let N=0,O=0;for(let _ of K){if(_===void 0)continue;if(_.status==="sent")N++;else O++}return{total:B.length,sent:N,failed:O,results:K}}verify(){if(this.transport.verify)return this.transport.verify();return Promise.resolve({ok:!0,provider:"mailer"})}close(){if(this.transport.close)return this.transport.close();return Promise.resolve()}}
2
+ export{k as t,q as u};
3
+
4
+ //# debugId=3F1ECBDC030264A764756E2164756E21
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core/plugin.ts", "../src/mailer.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @module\n * Plugin pipeline for sently.\n * Plugins transform MailOptions before message construction.\n * They run sequentially — each receives the previous plugin's output.\n *\n * @example\n * ```ts\n * import { runPlugins } from \"sently/core/plugin\";\n * const result = await runPlugins(options, [pluginA, pluginB]);\n * ```\n */\nimport type { MailOptions, MailPlugin } from \"./types.js\";\n\n/**\n * Run a list of plugins sequentially over MailOptions.\n * If plugins is empty or undefined, returns options unchanged.\n * Each plugin may be sync or async.\n *\n * @param options - the original mail options\n * @param plugins - ordered list of plugins to apply\n * @returns transformed mail options after all plugins have run\n */\nexport async function runPlugins(\n options: MailOptions,\n plugins: MailPlugin[] | undefined,\n): Promise<MailOptions> {\n if (!plugins || plugins.length === 0) {\n return options;\n }\n\n let current = options;\n for (const plugin of plugins) {\n current = await plugin(current);\n }\n return current;\n}\n",
6
+ "/**\n * @module\n * Lightweight mailer factory for custom transports — no SMTP code in the bundle.\n *\n * Use this entry instead of `sently` when you pass a transport explicitly\n * (Resend, SendGrid, etc.) and want the smallest bundle size.\n *\n * @example\n * ```ts\n * import { createMailer } from \"sently/mailer\";\n * import { ResendTransport } from \"sently/transports/resend\";\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 } from \"./core/address.js\";\nimport { runPlugins } from \"./core/plugin.js\";\nimport { RateLimiter } from \"./core/rate-limiter.js\";\nimport type {\n BulkSendOptions,\n BulkSendResult,\n Mailer,\n MailerHookContext,\n MailerHooks,\n MailOptions,\n MailPlugin,\n SendResult,\n Transport,\n TransportMailerOptions,\n VerifyResult,\n} from \"./core/types.js\";\nimport { RetryTransport } from \"./transports/retry.js\";\n\nexport type { MailerHookContext, MailerHooks, TransportMailerOptions };\n\n/**\n * Create a mailer that wraps a custom {@link Transport} (HTTP API, preview, retry, etc.).\n */\nexport async function createMailer(options: TransportMailerOptions): Promise<Mailer> {\n return new MailerImpl(options.transport, options.plugins ?? [], options.hooks);\n}\n\nfunction hasAttachments(message: MailOptions): boolean {\n return message.attachments !== undefined && message.attachments.length > 0;\n}\n\n/** Infer a provider label from a transport instance for hook context. */\nfunction inferProvider(transport: Transport): string {\n const name = transport.constructor.name;\n if (name === \"Object\") {\n return \"custom\";\n }\n if (name.endsWith(\"Transport\")) {\n return name.slice(0, -\"Transport\".length).toLowerCase();\n }\n return name.toLowerCase() || \"unknown\";\n}\n\n/** Build hook context from mail options (no body fields). */\nfunction buildHookContext(options: MailOptions, transport: Transport): MailerHookContext {\n return {\n ...(options.messageId !== undefined ? { messageId: options.messageId } : {}),\n to: extractEmails(options.to),\n subject: options.subject,\n provider: inferProvider(transport),\n };\n}\n\n/**\n * Invoke a mailer hook without letting hook failures break the send.\n * In non-production environments, hook errors are logged with `console.warn`.\n */\nasync function invokeHook<T extends unknown[]>(\n hook: ((...args: T) => void | Promise<void>) | undefined,\n ...args: T\n): Promise<void> {\n if (hook === undefined) {\n return;\n }\n try {\n await hook(...args);\n } catch (hookError) {\n const isProduction = typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\";\n if (!isProduction) {\n console.warn(\"[sently] Mailer hook threw; send continues:\", hookError);\n }\n }\n}\n\n/** Internal mailer implementation shared with the full `sently` entry. */\nexport class MailerImpl implements Mailer {\n constructor(\n private readonly transport: Transport,\n private readonly plugins: MailPlugin[] = [],\n private readonly hooks?: MailerHooks,\n ) {}\n\n async send(options: MailOptions): Promise<SendResult> {\n const processed = await runPlugins(options, this.plugins);\n const ctx = buildHookContext(processed, this.transport);\n\n await invokeHook(this.hooks?.onSend, ctx);\n\n if (this.hooks?.onRetry !== undefined && this.transport instanceof RetryTransport) {\n this.transport.setMailerOnRetry((attempt, error) => {\n void invokeHook(this.hooks?.onRetry, ctx, attempt, error);\n });\n }\n\n try {\n const result = await this.transport.send(processed);\n const successCtx: MailerHookContext = {\n ...ctx,\n messageId: result.messageId,\n };\n await invokeHook(this.hooks?.onSuccess, successCtx, result);\n return result;\n } catch (error) {\n await invokeHook(this.hooks?.onError, ctx, error);\n throw error;\n } finally {\n if (this.transport instanceof RetryTransport) {\n this.transport.setMailerOnRetry(undefined);\n }\n }\n }\n\n private async processMessage(message: MailOptions): Promise<MailOptions> {\n return runPlugins(message, this.plugins);\n }\n\n async sendBulk(messages: MailOptions[], options?: BulkSendOptions): Promise<BulkSendResult> {\n if (messages.length === 0) {\n return { total: 0, sent: 0, failed: 0, results: [] };\n }\n\n const results: BulkSendResult[\"results\"] = new Array(messages.length);\n const stopOnError = options?.stopOnError ?? false;\n let halted = false;\n\n const recordSuccess = async (\n message: MailOptions,\n index: number,\n result: SendResult,\n processed?: MailOptions,\n ): Promise<void> => {\n results[index] = { status: \"sent\", result };\n if (processed !== undefined && this.hooks !== undefined) {\n const ctx = buildHookContext(processed, this.transport);\n await invokeHook(this.hooks.onSend, ctx);\n await invokeHook(this.hooks.onSuccess, { ...ctx, messageId: result.messageId }, result);\n }\n options?.onSuccess?.(message, index, result);\n };\n\n const recordFailure = async (\n message: MailOptions,\n index: number,\n error: unknown,\n processed?: MailOptions,\n ): Promise<void> => {\n results[index] = { status: \"failed\", error };\n if (processed !== undefined && this.hooks !== undefined) {\n const ctx = buildHookContext(processed, this.transport);\n await invokeHook(this.hooks.onSend, ctx);\n await invokeHook(this.hooks.onError, ctx, error);\n }\n options?.onError?.(message, index, error);\n if (stopOnError) {\n halted = true;\n }\n };\n\n if (this.transport.sendBatch) {\n const batchEntries: Array<{ index: number; message: MailOptions }> = [];\n const singleEntries: Array<{ index: number; message: MailOptions }> = [];\n\n for (const [index, message] of messages.entries()) {\n if (hasAttachments(message)) {\n singleEntries.push({ index, message });\n } else {\n batchEntries.push({ index, message });\n }\n }\n\n if (batchEntries.length > 0 && !halted) {\n const chunkSize = this.transport.batchMax ?? batchEntries.length;\n const rateDelta = options?.rateDelta ?? 2;\n const rateLimiter =\n rateDelta > 0\n ? new RateLimiter(rateDelta, options?.rateLimit ?? 1000, options?.now)\n : null;\n\n for (\n let chunkStart = 0;\n chunkStart < batchEntries.length && !halted;\n chunkStart += chunkSize\n ) {\n const chunk = batchEntries.slice(chunkStart, chunkStart + chunkSize);\n\n if (rateLimiter) {\n await rateLimiter.acquire();\n }\n\n try {\n const processed = await Promise.all(\n chunk.map(({ message }) => this.processMessage(message)),\n );\n const batchResults = await this.transport.sendBatch(processed);\n\n for (let i = 0; i < chunk.length; i++) {\n const entry = chunk[i] as { index: number; message: MailOptions };\n const processedMessage = processed[i] as MailOptions;\n const result = batchResults[i];\n if (result === undefined) {\n await recordFailure(\n entry.message,\n entry.index,\n new Error(\"Batch response missing result for message\"),\n processedMessage,\n );\n } else if (result.batchError !== undefined) {\n await recordFailure(\n entry.message,\n entry.index,\n result.batchError,\n processedMessage,\n );\n } else {\n await recordSuccess(entry.message, entry.index, result, processedMessage);\n }\n }\n } catch (error) {\n const processed = await Promise.all(\n chunk.map(({ message }) => this.processMessage(message)),\n );\n for (let i = 0; i < chunk.length; i++) {\n if (halted) {\n break;\n }\n const entry = chunk[i] as { index: number; message: MailOptions };\n await recordFailure(entry.message, entry.index, error, processed[i]);\n }\n }\n }\n }\n\n if (singleEntries.length > 0 && !halted) {\n const concurrency = options?.concurrency ?? 1;\n const queue = [...singleEntries];\n let active = 0;\n\n await new Promise<void>((resolve) => {\n const maybeDone = (): void => {\n if ((queue.length === 0 && active === 0) || halted) {\n resolve();\n }\n };\n\n const processNext = (): void => {\n if (queue.length === 0 || halted) {\n maybeDone();\n return;\n }\n\n const entry = queue.shift();\n if (entry === undefined) {\n maybeDone();\n return;\n }\n\n active++;\n void this.send(entry.message)\n .then((result) => {\n results[entry.index] = { status: \"sent\", result };\n options?.onSuccess?.(entry.message, entry.index, result);\n })\n .catch((error: unknown) => {\n results[entry.index] = { status: \"failed\", error };\n options?.onError?.(entry.message, entry.index, error);\n if (stopOnError) {\n halted = true;\n }\n })\n .finally(() => {\n active--;\n processNext();\n maybeDone();\n });\n };\n\n for (let i = 0; i < concurrency; i++) {\n processNext();\n }\n });\n }\n } else {\n const concurrency = options?.concurrency ?? 1;\n const queue = [...messages.entries()];\n let active = 0;\n\n await new Promise<void>((resolve) => {\n const maybeDone = (): void => {\n if ((queue.length === 0 && active === 0) || halted) {\n resolve();\n }\n };\n\n const processNext = (): void => {\n if (queue.length === 0 || halted) {\n maybeDone();\n return;\n }\n\n const entry = queue.shift();\n if (entry === undefined) {\n maybeDone();\n return;\n }\n\n const [index, message] = entry;\n active++;\n\n void this.send(message)\n .then((result) => {\n results[index] = { status: \"sent\", result };\n options?.onSuccess?.(message, index, result);\n })\n .catch((error: unknown) => {\n results[index] = { status: \"failed\", error };\n options?.onError?.(message, index, error);\n if (stopOnError) {\n halted = true;\n }\n })\n .finally(() => {\n active--;\n processNext();\n maybeDone();\n });\n };\n\n for (let i = 0; i < concurrency; i++) {\n processNext();\n }\n });\n }\n\n let sent = 0;\n let failed = 0;\n for (const result of results) {\n if (result === undefined) {\n continue;\n }\n if (result.status === \"sent\") {\n sent++;\n } else {\n failed++;\n }\n }\n\n return {\n total: messages.length,\n sent,\n failed,\n results,\n };\n }\n\n verify(): Promise<VerifyResult> {\n if (this.transport.verify) {\n return this.transport.verify();\n }\n return Promise.resolve({ ok: true, provider: \"mailer\" });\n }\n\n close(): Promise<void> {\n if (this.transport.close) {\n return this.transport.close();\n }\n return Promise.resolve();\n }\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "3F1ECBDC030264A764756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,4 @@
1
+ class v extends Error{sentlyCode;statusCode;provider;code;constructor(j,k,q){super(j,q?.cause!==void 0?{cause:q.cause}:void 0);if(this.name="SentlyError",this.sentlyCode=k,this.code=k,q?.statusCode!==void 0)this.statusCode=q.statusCode;if(q?.provider!==void 0)this.provider=q.provider}}function w(j){if(j===429)return"RATE_LIMITED";if(j>=500)return"PROVIDER_ERROR";if(j>=400)return"BAD_REQUEST";return"PROVIDER_ERROR"}function x(j,k){if(j===535||j===534)return"SMTP_AUTH_FAILED";if(j===421||j===450||j===451)return"TIMEOUT";if(j===0){if(k==="CONNECT"||k==="READ"||k==="MX")return"CONNECTION_FAILED";return"PROVIDER_ERROR"}if(j>=500)return"PROVIDER_ERROR";if(j>=400)return"BAD_REQUEST";return"PROVIDER_ERROR"}
2
+ export{v as S,w as T,x as U};
3
+
4
+ //# debugId=2A4786BCA338BD1D64756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core/errors.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @module\n * Unified error hierarchy for sently transports and SMTP.\n */\n\n/** Stable machine-readable error codes shared across providers. */\nexport type SentlyErrorCode =\n | \"SMTP_AUTH_FAILED\"\n | \"RATE_LIMITED\"\n | \"TIMEOUT\"\n | \"CONNECTION_FAILED\"\n | \"PROVIDER_ERROR\"\n | \"BAD_REQUEST\";\n\n/** Options for constructing a {@link SentlyError}. */\nexport interface SentlyErrorOptions {\n /** HTTP status code when the failure originated from an HTTP transport. */\n statusCode?: number;\n /** Original error or response payload. */\n cause?: unknown;\n /** Provider identifier (e.g. `\"smtp\"`, `\"resend\"`). */\n provider?: string;\n}\n\n/**\n * Base error class for all sently transport and protocol failures.\n *\n * Subclasses may shadow {@link code} with provider-specific or numeric values\n * (e.g. SMTP response codes). Use {@link sentlyCode} for the stable machine-readable\n * code in those cases.\n */\nexport class SentlyError extends Error {\n /**\n * Stable machine-readable error code.\n * Preserved even when a subclass shadows {@link code}.\n */\n readonly sentlyCode: SentlyErrorCode;\n\n /** HTTP status code when applicable. */\n readonly statusCode?: number;\n\n /** Provider identifier when applicable. */\n readonly provider?: string;\n\n /**\n * Error code — machine-readable string on {@link SentlyError}, or a legacy\n * provider/SMTP-specific value on subclasses (numeric SMTP codes, Brevo/SES API codes).\n */\n code: string | number;\n\n /** Creates a sently error with a machine-readable code and optional metadata. */\n constructor(message: string, sentlyCode: SentlyErrorCode, options?: SentlyErrorOptions) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = \"SentlyError\";\n this.sentlyCode = sentlyCode;\n this.code = sentlyCode;\n if (options?.statusCode !== undefined) {\n this.statusCode = options.statusCode;\n }\n if (options?.provider !== undefined) {\n this.provider = options.provider;\n }\n }\n}\n\n/**\n * Map an HTTP status code to a stable sently error code.\n *\n * @param status - HTTP response status code\n * @returns Machine-readable error code\n */\nexport function httpStatusToSentlyCode(status: number): SentlyErrorCode {\n if (status === 429) {\n return \"RATE_LIMITED\";\n }\n if (status >= 500) {\n return \"PROVIDER_ERROR\";\n }\n if (status >= 400) {\n return \"BAD_REQUEST\";\n }\n return \"PROVIDER_ERROR\";\n}\n\n/**\n * Map an SMTP response code and command to a stable sently error code.\n *\n * @param smtpCode - Three-digit SMTP response code (0 for local/parse errors)\n * @param command - SMTP command that failed\n * @returns Machine-readable error code\n */\nexport function smtpCodeToSentlyCode(smtpCode: number, command: string): SentlyErrorCode {\n if (smtpCode === 535 || smtpCode === 534) {\n return \"SMTP_AUTH_FAILED\";\n }\n if (smtpCode === 421 || smtpCode === 450 || smtpCode === 451) {\n return \"TIMEOUT\";\n }\n if (smtpCode === 0) {\n if (command === \"CONNECT\" || command === \"READ\" || command === \"MX\") {\n return \"CONNECTION_FAILED\";\n }\n return \"PROVIDER_ERROR\";\n }\n if (smtpCode >= 500) {\n return \"PROVIDER_ERROR\";\n }\n if (smtpCode >= 400) {\n return \"BAD_REQUEST\";\n }\n return \"PROVIDER_ERROR\";\n}\n"
6
+ ],
7
+ "mappings": "AA+BO,MAAM,UAAoB,KAAM,CAK5B,WAGA,WAGA,SAMT,KAGA,WAAW,CAAC,EAAiB,EAA6B,EAA8B,CACtF,MAAM,EAAS,GAAS,QAAU,OAAY,CAAE,MAAO,EAAQ,KAAM,EAAI,MAAS,EAIlF,GAHA,KAAK,KAAO,cACZ,KAAK,WAAa,EAClB,KAAK,KAAO,EACR,GAAS,aAAe,OAC1B,KAAK,WAAa,EAAQ,WAE5B,GAAI,GAAS,WAAa,OACxB,KAAK,SAAW,EAAQ,SAG9B,CAQO,SAAS,CAAsB,CAAC,EAAiC,CACtE,GAAI,IAAW,IACb,MAAO,eAET,GAAI,GAAU,IACZ,MAAO,iBAET,GAAI,GAAU,IACZ,MAAO,cAET,MAAO,iBAUF,SAAS,CAAoB,CAAC,EAAkB,EAAkC,CACvF,GAAI,IAAa,KAAO,IAAa,IACnC,MAAO,mBAET,GAAI,IAAa,KAAO,IAAa,KAAO,IAAa,IACvD,MAAO,UAET,GAAI,IAAa,EAAG,CAClB,GAAI,IAAY,WAAa,IAAY,QAAU,IAAY,KAC7D,MAAO,oBAET,MAAO,iBAET,GAAI,GAAY,IACd,MAAO,iBAET,GAAI,GAAY,IACd,MAAO,cAET,MAAO",
8
+ "debugId": "2A4786BCA338BD1D64756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,3 +1,3 @@
1
- import{p as U}from"./chunk-zkczgj2c.js";import{r as X}from"./chunk-aeqxkeh3.js";import{s as b}from"./chunk-k0gw3qmb.js";import{B as k,C as x,D as Z,E as O,F as q,G as L,H as j,I as N,J as v,K as A,M as E}from"./chunk-9p4vev6j.js";import{R as C}from"./chunk-mg8m26nr.js";class T{config;adapter=null;constructor(z){this.config=P(z)}async send(z){let B={...z,attachments:await b(z.attachments)},G=await X(B,this.config.dkim),F=await this.getAdapter(),W=this.config.direct?await M(G.envelope.from.split("@")[1]??this.config.host):this.config.host;await F.connect(W,this.config.port),this.adapter=F;try{return await D(F,this.config),await S(F,G)}finally{await H(F),this.adapter=null}}async verify(){try{let z=await this.getAdapter();await z.connect(this.config.host,this.config.port);try{return await D(z,this.config),{ok:!0,provider:"smtp"}}finally{await H(z)}}catch(z){return{ok:!1,provider:"smtp",message:z instanceof Error?z.message:String(z)}}}async close(){if(this.adapter)await this.adapter.close(),this.adapter=null}async getAdapter(){if(!this.config.adapter)throw new x("No socket adapter configured",0,"CONNECT","");return this.config.adapter}}function P(z){let B=z.secure??!1;return{host:z.host,port:z.port??(B?465:587),secure:B,...z.auth!==void 0?{auth:z.auth}:{},...z.requireTLS!==void 0?{requireTLS:z.requireTLS}:{},...z.dkim!==void 0?{dkim:z.dkim}:{},...z.tls!==void 0?{tls:z.tls}:{},...z.connectionTimeout!==void 0?{connectionTimeout:z.connectionTimeout}:{},...z.greetingTimeout!==void 0?{greetingTimeout:z.greetingTimeout}:{},...z.socketTimeout!==void 0?{socketTimeout:z.socketTimeout}:{},...z.direct!==void 0?{direct:z.direct}:{},...z.adapter!==void 0?{adapter:z.adapter}:{}}}async function D(z,B){let G=await J(z);N(G,[220],"greeting");let F=await I(z,B.host),W=F.some((Q)=>Q.toUpperCase()==="STARTTLS");if(!B.secure&&!z.secure&&W){await Y(z,Z({type:"STARTTLS"}));let Q=await J(z);N(Q,[220],"STARTTLS"),await z.startTLS(B.tls),F=await I(z,B.host)}if(B.auth){let Q=B.requireTLS??!0,V=z.secure||B.secure;if(Q&&!V)throw new x("Refusing to authenticate over unencrypted connection. Set requireTLS: false to disable this check (not recommended).",0,"AUTH","");await l(z,B.auth,F)}}async function S(z,B){await _(z,{type:"MAIL_FROM",address:B.envelope.from});let G=await J(z);N(G,[250],"MAIL FROM");let F=[],W=[];for(let K of B.envelope.to)if(await Y(z,Z({type:"RCPT_TO",address:K})),(await J(z)).isSuccess)F.push(K);else W.push(K);await _(z,{type:"DATA"});let Q=await J(z);N(Q,[354],"DATA");let V;try{await Y(z,Z({type:"DATA_BODY",content:B.raw})),V=await J(z)}catch(K){if(await Y(z,Z({type:"DATA_BODY",content:B.raw})),V=await J(z),V.isError)throw K}return N(V,[250],"DATA end"),{messageId:B.messageId,accepted:F,rejected:W,response:V.message,envelope:B.envelope}}async function H(z){try{await _(z,{type:"QUIT"}),await J(z)}catch{}finally{await z.close()}}async function I(z,B){await _(z,{type:"EHLO",domain:B});let G=await J(z);return N(G,[250],"EHLO"),j(G)}async function l(z,B,G){if(B.type==="OAUTH2"&&B.oauth2){let K=await new U(B.oauth2).buildXOAUTH2();await _(z,{type:"AUTH_XOAUTH2",xoauth2String:K});let $=await J(z);if($.code===334)await Y(z,E("")),$=await J(z);N($,[235],"AUTH XOAUTH2");return}let F=B.type??L(G);if(F==="CRAM-MD5"){let V=y(B,"CRAM-MD5");await _(z,{type:"AUTH_CRAM_MD5_INIT"});let K=await J(z);N(K,[334],"AUTH CRAM-MD5");let $=K.message.trim(),w=await k($,B.user,V);await _(z,{type:"AUTH_CRAM_MD5_RESPONSE",response:w}),K=await J(z),N(K,[235],"AUTH CRAM-MD5 response");return}if(F==="PLAIN"){let V=y(B,"PLAIN");await Y(z,Z({type:"AUTH_PLAIN",user:B.user,pass:V}));let K=await J(z);N(K,[235],"AUTH PLAIN");return}let W=y(B,"LOGIN");await Y(z,Z({type:"AUTH_LOGIN",user:B.user,pass:W}));let Q=await J(z);N(Q,[334],"AUTH LOGIN"),await Y(z,A(B.user)),Q=await J(z),N(Q,[334],"AUTH LOGIN user"),await Y(z,v(W)),Q=await J(z),N(Q,[235],"AUTH LOGIN pass")}function y(z,B){if(!z.pass)throw new x(`Password required for ${B} authentication`,0,`AUTH ${B}`,"");return z.pass}async function _(z,B){await Y(z,Z(B))}async function Y(z,B){await z.write(B)}async function J(z){let B=[];for await(let G of z.read()){B.push(G);let F=q(B);if(F)return O(F)}throw new x("Connection closed while reading SMTP response",0,"READ","")}async function M(z){let G=await(await import("node:dns/promises")).resolveMx(z);if(G.length===0)throw new x(`No MX records for ${z}`,0,"MX","");return G.sort((F,W)=>F.priority-W.priority),G[0]?.exchange??z}export{T as h,P as i,D as j,S as k,H as l,J as m};
1
+ import{p as A}from"./chunk-348wndn3.js";import{r as E}from"./chunk-y72qggqf.js";import{s as b}from"./chunk-z7j2n9k8.js";import{C as U,D as x,E as Z,F as X,G as k,H as O,I as q,J as N,K as L,L as j,N as v}from"./chunk-w3fnjh04.js";import{V as C}from"./chunk-rszb1n1d.js";class T{config;adapter=null;constructor(z){this.config=P(z)}async send(z){let B={...z,attachments:await b(z.attachments)},G=await E(B,this.config.dkim),F=await this.getAdapter(),W=this.config.direct?await M(G.envelope.from.split("@")[1]??this.config.host):this.config.host;await F.connect(W,this.config.port),this.adapter=F;try{return await D(F,this.config),await S(F,G)}finally{await H(F),this.adapter=null}}async verify(){try{let z=await this.getAdapter();await z.connect(this.config.host,this.config.port);try{return await D(z,this.config),{ok:!0,provider:"smtp"}}finally{await H(z)}}catch(z){return{ok:!1,provider:"smtp",message:z instanceof Error?z.message:String(z)}}}async close(){if(this.adapter)await this.adapter.close(),this.adapter=null}async getAdapter(){if(!this.config.adapter)throw new x("No socket adapter configured",0,"CONNECT","");return this.config.adapter}}function P(z){let B=z.secure??!1;return{host:z.host,port:z.port??(B?465:587),secure:B,...z.auth!==void 0?{auth:z.auth}:{},...z.requireTLS!==void 0?{requireTLS:z.requireTLS}:{},...z.dkim!==void 0?{dkim:z.dkim}:{},...z.tls!==void 0?{tls:z.tls}:{},...z.connectionTimeout!==void 0?{connectionTimeout:z.connectionTimeout}:{},...z.greetingTimeout!==void 0?{greetingTimeout:z.greetingTimeout}:{},...z.socketTimeout!==void 0?{socketTimeout:z.socketTimeout}:{},...z.direct!==void 0?{direct:z.direct}:{},...z.adapter!==void 0?{adapter:z.adapter}:{}}}async function D(z,B){let G=await J(z);N(G,[220],"greeting");let F=await I(z,B.host),W=F.some((Q)=>Q.toUpperCase()==="STARTTLS");if(!B.secure&&!z.secure&&W){await Y(z,Z({type:"STARTTLS"}));let Q=await J(z);N(Q,[220],"STARTTLS"),await z.startTLS(B.tls),F=await I(z,B.host)}if(B.auth){let Q=B.requireTLS??!0,V=z.secure||B.secure;if(Q&&!V)throw new x("Refusing to authenticate over unencrypted connection. Set requireTLS: false to disable this check (not recommended).",0,"AUTH","");await l(z,B.auth,F)}}async function S(z,B){await _(z,{type:"MAIL_FROM",address:B.envelope.from});let G=await J(z);N(G,[250],"MAIL FROM");let F=[],W=[];for(let K of B.envelope.to)if(await Y(z,Z({type:"RCPT_TO",address:K})),(await J(z)).isSuccess)F.push(K);else W.push(K);await _(z,{type:"DATA"});let Q=await J(z);N(Q,[354],"DATA");let V;try{await Y(z,Z({type:"DATA_BODY",content:B.raw})),V=await J(z)}catch(K){if(await Y(z,Z({type:"DATA_BODY",content:B.raw})),V=await J(z),V.isError)throw K}return N(V,[250],"DATA end"),{messageId:B.messageId,accepted:F,rejected:W,response:V.message,envelope:B.envelope}}async function H(z){try{await _(z,{type:"QUIT"}),await J(z)}catch{}finally{await z.close()}}async function I(z,B){await _(z,{type:"EHLO",domain:B});let G=await J(z);return N(G,[250],"EHLO"),q(G)}async function l(z,B,G){if(B.type==="OAUTH2"&&B.oauth2){let K=await new A(B.oauth2).buildXOAUTH2();await _(z,{type:"AUTH_XOAUTH2",xoauth2String:K});let $=await J(z);if($.code===334)await Y(z,v("")),$=await J(z);N($,[235],"AUTH XOAUTH2");return}let F=B.type??O(G);if(F==="CRAM-MD5"){let V=y(B,"CRAM-MD5");await _(z,{type:"AUTH_CRAM_MD5_INIT"});let K=await J(z);N(K,[334],"AUTH CRAM-MD5");let $=K.message.trim(),w=await U($,B.user,V);await _(z,{type:"AUTH_CRAM_MD5_RESPONSE",response:w}),K=await J(z),N(K,[235],"AUTH CRAM-MD5 response");return}if(F==="PLAIN"){let V=y(B,"PLAIN");await Y(z,Z({type:"AUTH_PLAIN",user:B.user,pass:V}));let K=await J(z);N(K,[235],"AUTH PLAIN");return}let W=y(B,"LOGIN");await Y(z,Z({type:"AUTH_LOGIN",user:B.user,pass:W}));let Q=await J(z);N(Q,[334],"AUTH LOGIN"),await Y(z,j(B.user)),Q=await J(z),N(Q,[334],"AUTH LOGIN user"),await Y(z,L(W)),Q=await J(z),N(Q,[235],"AUTH LOGIN pass")}function y(z,B){if(!z.pass)throw new x(`Password required for ${B} authentication`,0,`AUTH ${B}`,"");return z.pass}async function _(z,B){await Y(z,Z(B))}async function Y(z,B){await z.write(B)}async function J(z){let B=[];for await(let G of z.read()){B.push(G);let F=k(B);if(F)return X(F)}throw new x("Connection closed while reading SMTP response",0,"READ","")}async function M(z){let G=await(await import("node:dns/promises")).resolveMx(z);if(G.length===0)throw new x(`No MX records for ${z}`,0,"MX","");return G.sort((F,W)=>F.priority-W.priority),G[0]?.exchange??z}export{T as h,P as i,D as j,S as k,H as l,J as m};
2
2
 
3
- //# debugId=7F6A4A30744EE38A64756E2164756E21
3
+ //# debugId=DFA0A23A4210988264756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "/**\n * @module\n * SMTP transport — orchestrates socket adapter, MIME builder, and protocol logic.\n *\n * @example\n * ```ts\n * import { SMTPTransport } from \"sently/transports/smtp\";\n * import { NodeAdapter } from \"sently/adapters/node\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * transport: new SMTPTransport({\n * host: \"smtp.example.com\",\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * adapter: new NodeAdapter(),\n * }),\n * });\n * ```\n */\nimport { OAuth2Client } from \"../auth/oauth2.js\";\nimport { buildMIME, type MIMEBuildResult } from \"../core/mime.js\";\nimport type { SMTPResponse } from \"../core/smtp.js\";\nimport {\n accumulateResponse,\n assertResponse,\n computeCRAMMD5,\n encodeAuthLoginPass,\n encodeAuthLoginUser,\n encodeCommand,\n encodeLine,\n parseEHLO,\n parseResponse,\n SMTPError,\n selectAuthMethod,\n} from \"../core/smtp.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPConfig,\n SocketAdapter,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/**\n * SMTP transport orchestrating adapter, MIME builder, and protocol logic.\n */\nexport class SMTPTransport implements Transport {\n /** Resolved SMTP configuration with defaults applied. */\n private readonly config: ResolvedSMTPConfig;\n /** Active socket adapter for the current session, if any. */\n private adapter: SocketAdapter | null = null;\n\n /** Creates an SMTP transport with the given configuration. */\n constructor(config: SMTPConfig) {\n this.config = resolveSMTPConfig(config);\n }\n\n /** Sends an email via SMTP using the configured adapter. */\n async send(options: MailOptions): Promise<SendResult> {\n const resolvedOptions = {\n ...options,\n attachments: await resolveAttachments(options.attachments),\n };\n const mime = await buildMIME(resolvedOptions, this.config.dkim);\n const adapter = await this.getAdapter();\n\n const host = this.config.direct\n ? await resolveMX(mime.envelope.from.split(\"@\")[1] ?? this.config.host)\n : this.config.host;\n\n await adapter.connect(host, this.config.port);\n this.adapter = adapter;\n\n try {\n await openSMTPSession(adapter, this.config);\n return await deliverSMTPMessage(adapter, mime);\n } finally {\n await closeSMTPSession(adapter);\n this.adapter = null;\n }\n }\n\n /** Verifies SMTP connectivity and authentication without sending mail. */\n async verify(): Promise<VerifyResult> {\n try {\n const adapter = await this.getAdapter();\n await adapter.connect(this.config.host, this.config.port);\n\n try {\n await openSMTPSession(adapter, this.config);\n return { ok: true, provider: \"smtp\" };\n } finally {\n await closeSMTPSession(adapter);\n }\n } catch (err) {\n return {\n ok: false,\n provider: \"smtp\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n /** Closes the underlying socket adapter if connected. */\n async close(): Promise<void> {\n if (this.adapter) {\n await this.adapter.close();\n this.adapter = null;\n }\n }\n\n /** Returns the configured socket adapter or throws if none is set. */\n private async getAdapter(): Promise<SocketAdapter> {\n if (!this.config.adapter) {\n throw new SMTPError(\"No socket adapter configured\", 0, \"CONNECT\", \"\");\n }\n return this.config.adapter;\n }\n}\n\n/** Resolved SMTP transport configuration with defaults applied. */\nexport interface ResolvedSMTPConfig {\n /** SMTP server hostname. */\n host: string;\n /** SMTP port with secure/default applied. */\n port: number;\n /** Whether implicit TLS is used. */\n secure: boolean;\n /** Resolved authentication credentials, if any. */\n auth?: SMTPConfig[\"auth\"];\n /** Whether AUTH requires an encrypted connection. */\n requireTLS?: boolean;\n /** TLS options for STARTTLS and direct TLS. */\n tls?: SMTPConfig[\"tls\"];\n /** DKIM signing configuration. */\n dkim?: SMTPConfig[\"dkim\"];\n /** Socket connect timeout in milliseconds. */\n connectionTimeout?: number;\n /** Timeout waiting for the SMTP greeting in milliseconds. */\n greetingTimeout?: number;\n /** Idle socket timeout in milliseconds. */\n socketTimeout?: number;\n /** Direct-to-MX delivery mode. */\n direct?: boolean;\n /** Runtime socket adapter. */\n adapter?: SocketAdapter;\n}\n\n/** Apply defaults to SMTP configuration. */\nexport function resolveSMTPConfig(config: SMTPConfig): ResolvedSMTPConfig {\n const secure = config.secure ?? false;\n return {\n host: config.host,\n port: config.port ?? (secure ? 465 : 587),\n secure,\n ...(config.auth !== undefined ? { auth: config.auth } : {}),\n ...(config.requireTLS !== undefined ? { requireTLS: config.requireTLS } : {}),\n ...(config.dkim !== undefined ? { dkim: config.dkim } : {}),\n ...(config.tls !== undefined ? { tls: config.tls } : {}),\n ...(config.connectionTimeout !== undefined\n ? { connectionTimeout: config.connectionTimeout }\n : {}),\n ...(config.greetingTimeout !== undefined ? { greetingTimeout: config.greetingTimeout } : {}),\n ...(config.socketTimeout !== undefined ? { socketTimeout: config.socketTimeout } : {}),\n ...(config.direct !== undefined ? { direct: config.direct } : {}),\n ...(config.adapter !== undefined ? { adapter: config.adapter } : {}),\n };\n}\n\n/**\n * Connect greeting, EHLO, optional STARTTLS, and AUTH on an open adapter.\n */\nexport async function openSMTPSession(\n adapter: SocketAdapter,\n config: ResolvedSMTPConfig,\n): Promise<void> {\n const greeting = await readSMTPResponse(adapter);\n assertResponse(greeting, [220], \"greeting\");\n\n let capabilities = await ehlo(adapter, config.host);\n const supportsStartTls = capabilities.some((cap) => cap.toUpperCase() === \"STARTTLS\");\n if (!config.secure && !adapter.secure && supportsStartTls) {\n await sendRaw(adapter, encodeCommand({ type: \"STARTTLS\" }));\n const starttlsResp = await readSMTPResponse(adapter);\n assertResponse(starttlsResp, [220], \"STARTTLS\");\n await adapter.startTLS(config.tls);\n capabilities = await ehlo(adapter, config.host);\n }\n\n if (config.auth) {\n const tlsRequired = config.requireTLS ?? true;\n const encrypted = adapter.secure || config.secure;\n if (tlsRequired && !encrypted) {\n throw new SMTPError(\n \"Refusing to authenticate over unencrypted connection. \" +\n \"Set requireTLS: false to disable this check (not recommended).\",\n 0,\n \"AUTH\",\n \"\",\n );\n }\n await authenticate(adapter, config.auth, capabilities);\n }\n}\n\n/**\n * MAIL FROM, RCPT TO, and DATA for a built MIME message on an authenticated session.\n */\nexport async function deliverSMTPMessage(\n adapter: SocketAdapter,\n mime: MIMEBuildResult,\n): Promise<SendResult> {\n await sendCommand(adapter, { type: \"MAIL_FROM\", address: mime.envelope.from });\n const mailResp = await readSMTPResponse(adapter);\n assertResponse(mailResp, [250], \"MAIL FROM\");\n\n const accepted: string[] = [];\n const rejected: string[] = [];\n\n for (const recipient of mime.envelope.to) {\n await sendRaw(adapter, encodeCommand({ type: \"RCPT_TO\", address: recipient }));\n const rcptResp = await readSMTPResponse(adapter);\n if (rcptResp.isSuccess) {\n accepted.push(recipient);\n } else {\n rejected.push(recipient);\n }\n }\n\n await sendCommand(adapter, { type: \"DATA\" });\n const dataResp = await readSMTPResponse(adapter);\n assertResponse(dataResp, [354], \"DATA\");\n\n let finalResp: SMTPResponse;\n try {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n } catch (err) {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n if (finalResp.isError) {\n throw err;\n }\n }\n assertResponse(finalResp, [250], \"DATA end\");\n\n return {\n messageId: mime.messageId,\n accepted,\n rejected,\n response: finalResp.message,\n envelope: mime.envelope,\n };\n}\n\n/**\n * QUIT and close an SMTP session adapter.\n */\nexport async function closeSMTPSession(adapter: SocketAdapter): Promise<void> {\n try {\n await sendCommand(adapter, { type: \"QUIT\" });\n await readSMTPResponse(adapter);\n } catch {\n // ignore errors during shutdown\n } finally {\n await adapter.close();\n }\n}\n\nasync function ehlo(adapter: SocketAdapter, host: string): Promise<string[]> {\n await sendCommand(adapter, { type: \"EHLO\", domain: host });\n const response = await readSMTPResponse(adapter);\n assertResponse(response, [250], \"EHLO\");\n return parseEHLO(response);\n}\n\nasync function authenticate(\n adapter: SocketAdapter,\n auth: NonNullable<SMTPConfig[\"auth\"]>,\n capabilities: string[],\n): Promise<void> {\n if (auth.type === \"OAUTH2\" && auth.oauth2) {\n const client = new OAuth2Client(auth.oauth2);\n const xoauth2 = await client.buildXOAUTH2();\n await sendCommand(adapter, { type: \"AUTH_XOAUTH2\", xoauth2String: xoauth2 });\n let resp = await readSMTPResponse(adapter);\n if (resp.code === 334) {\n await sendRaw(adapter, encodeLine(\"\"));\n resp = await readSMTPResponse(adapter);\n }\n assertResponse(resp, [235], \"AUTH XOAUTH2\");\n return;\n }\n\n const method = auth.type ?? selectAuthMethod(capabilities);\n\n if (method === \"CRAM-MD5\") {\n const pass = requirePassword(auth, \"CRAM-MD5\");\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_INIT\" });\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH CRAM-MD5\");\n const challenge = resp.message.trim();\n const response = await computeCRAMMD5(challenge, auth.user, pass);\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_RESPONSE\", response });\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH CRAM-MD5 response\");\n return;\n }\n\n if (method === \"PLAIN\") {\n const pass = requirePassword(auth, \"PLAIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_PLAIN\", user: auth.user, pass }));\n const resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH PLAIN\");\n return;\n }\n\n const pass = requirePassword(auth, \"LOGIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_LOGIN\", user: auth.user, pass }));\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN\");\n\n await sendRaw(adapter, encodeAuthLoginUser(auth.user));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN user\");\n\n await sendRaw(adapter, encodeAuthLoginPass(pass));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH LOGIN pass\");\n}\n\nfunction requirePassword(auth: NonNullable<SMTPConfig[\"auth\"]>, method: string): string {\n if (!auth.pass) {\n throw new SMTPError(`Password required for ${method} authentication`, 0, `AUTH ${method}`, \"\");\n }\n return auth.pass;\n}\n\nasync function sendCommand(\n adapter: SocketAdapter,\n command: Parameters<typeof encodeCommand>[0],\n): Promise<void> {\n await sendRaw(adapter, encodeCommand(command));\n}\n\nasync function sendRaw(adapter: SocketAdapter, data: Uint8Array): Promise<void> {\n await adapter.write(data);\n}\n\n/** Reads and parses a complete SMTP response from the adapter. */\nasync function readSMTPResponse(adapter: SocketAdapter): Promise<SMTPResponse> {\n const chunks: Uint8Array[] = [];\n for await (const chunk of adapter.read()) {\n chunks.push(chunk);\n const complete = accumulateResponse(chunks);\n if (complete) {\n return parseResponse(complete);\n }\n }\n throw new SMTPError(\"Connection closed while reading SMTP response\", 0, \"READ\", \"\");\n}\n\nasync function resolveMX(domain: string): Promise<string> {\n const dns = await import(\"node:dns/promises\");\n const records = await dns.resolveMx(domain);\n if (records.length === 0) {\n throw new SMTPError(`No MX records for ${domain}`, 0, \"MX\", \"\");\n }\n records.sort((a: { priority: number }, b: { priority: number }) => a.priority - b.priority);\n return records[0]?.exchange ?? domain;\n}\n\n/** @internal Test helper for raw line writes. */\nexport { encodeLine, readSMTPResponse };\n"
6
6
  ],
7
7
  "mappings": "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",
8
- "debugId": "7F6A4A30744EE38A64756E2164756E21",
8
+ "debugId": "DFA0A23A4210988264756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
1
  import{createRequire as a}from"node:module";var c=a(import.meta.url);
2
- export{c as R};
2
+ export{c as V};
3
3
 
4
- //# debugId=78316B99719BC7AC64756E2164756E21
4
+ //# debugId=C9CAEF47EAC82F7B64756E2164756E21
@@ -4,6 +4,6 @@
4
4
  "sourcesContent": [
5
5
  ],
6
6
  "mappings": "",
7
- "debugId": "78316B99719BC7AC64756E2164756E21",
7
+ "debugId": "C9CAEF47EAC82F7B64756E2164756E21",
8
8
  "names": []
9
9
  }
@@ -0,0 +1,4 @@
1
+ import{D as G}from"./chunk-w3fnjh04.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.code===535)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;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)))}}
2
+ export{K as v};
3
+
4
+ //# debugId=BFA284A1AC0693B364756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/transports/retry.ts"],
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 { SMTPError } from \"../core/smtp.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 SMTPError && err.code === 535) {\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"
6
+ ],
7
+ "mappings": "wCA0BA,IAAM,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,GAAa,EAAI,OAAS,IAC3C,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,CAiB5B,MAGA,OAlBF,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": "BFA284A1AC0693B364756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,4 +1,4 @@
1
1
  class y{rateDelta;rateLimit;now;static defaultNow=Date.now;tokens;lastRefill;waiters=[];constructor(h,x,j=y.defaultNow){this.rateDelta=h;this.rateLimit=x;this.now=j;this.tokens=h,this.lastRefill=j()}async acquire(){for(;;){if(this.refill(),this.tokens>0){this.tokens-=1;return}if(this.now===y.defaultNow){let h=this.now()-this.lastRefill,x=Math.max(1,this.rateLimit-h);await new Promise((j)=>setTimeout(j,x));continue}await new Promise((h)=>{this.waiters.push(h)})}}notify(){this.refill()}refill(){let x=this.now()-this.lastRefill;if(x>=this.rateLimit){let j=Math.floor(x/this.rateLimit);this.tokens=Math.min(this.rateDelta,this.tokens+j*this.rateDelta),this.lastRefill+=j*this.rateLimit;while(this.tokens>0&&this.waiters.length>0)this.tokens-=1,this.waiters.shift()?.()}}}
2
- export{y as A};
2
+ export{y as w};
3
3
 
4
- //# debugId=FABB2451969B5F3264756E2164756E21
4
+ //# debugId=7255BEE518CBAF7E64756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "/**\n * Token bucket rate limiter with lazy refill on acquire.\n */\nexport class RateLimiter {\n private static readonly defaultNow = Date.now;\n\n /** Remaining tokens in the current rate-limit window. */\n private tokens: number;\n /** Timestamp (ms) of the last token refill. */\n private lastRefill: number;\n /** Resolvers waiting for a token when the bucket is empty. */\n private waiters: Array<() => void> = [];\n\n /** Creates a rate limiter with the given burst size and window duration. */\n constructor(\n private readonly rateDelta: number,\n private readonly rateLimit: number,\n private readonly now: () => number = RateLimiter.defaultNow,\n ) {\n this.tokens = rateDelta;\n this.lastRefill = now();\n }\n\n /** Wait until a token is available, then consume one. */\n async acquire(): Promise<void> {\n for (;;) {\n this.refill();\n if (this.tokens > 0) {\n this.tokens -= 1;\n return;\n }\n\n if (this.now === RateLimiter.defaultNow) {\n const elapsed = this.now() - this.lastRefill;\n const waitMs = Math.max(1, this.rateLimit - elapsed);\n await new Promise<void>((resolve) => setTimeout(resolve, waitMs));\n continue;\n }\n\n await new Promise<void>((resolve) => {\n this.waiters.push(resolve);\n });\n }\n }\n\n /** Wake waiters after the clock advances (for testing). */\n notify(): void {\n this.refill();\n }\n\n /** Refills tokens based on elapsed time and wakes waiting acquirers. */\n private refill(): void {\n const t = this.now();\n const elapsed = t - this.lastRefill;\n if (elapsed >= this.rateLimit) {\n const periods = Math.floor(elapsed / this.rateLimit);\n this.tokens = Math.min(this.rateDelta, this.tokens + periods * this.rateDelta);\n this.lastRefill += periods * this.rateLimit;\n while (this.tokens > 0 && this.waiters.length > 0) {\n this.tokens -= 1;\n const next = this.waiters.shift();\n next?.();\n }\n }\n }\n}\n"
6
6
  ],
7
7
  "mappings": "AAGO,MAAM,CAAY,CAYJ,UACA,UACA,UAbK,YAAa,KAAK,IAGlC,OAEA,WAEA,QAA6B,CAAC,EAGtC,WAAW,CACQ,EACA,EACA,EAAoB,EAAY,WACjD,CAHiB,iBACA,iBACA,WAEjB,KAAK,OAAS,EACd,KAAK,WAAa,EAAI,OAIlB,QAAO,EAAkB,CAC7B,OAAS,CAEP,GADA,KAAK,OAAO,EACR,KAAK,OAAS,EAAG,CACnB,KAAK,QAAU,EACf,OAGF,GAAI,KAAK,MAAQ,EAAY,WAAY,CACvC,IAAM,EAAU,KAAK,IAAI,EAAI,KAAK,WAC5B,EAAS,KAAK,IAAI,EAAG,KAAK,UAAY,CAAO,EACnD,MAAM,IAAI,QAAc,CAAC,IAAY,WAAW,EAAS,CAAM,CAAC,EAChE,SAGF,MAAM,IAAI,QAAc,CAAC,IAAY,CACnC,KAAK,QAAQ,KAAK,CAAO,EAC1B,GAKL,MAAM,EAAS,CACb,KAAK,OAAO,EAIN,MAAM,EAAS,CAErB,IAAM,EADI,KAAK,IAAI,EACC,KAAK,WACzB,GAAI,GAAW,KAAK,UAAW,CAC7B,IAAM,EAAU,KAAK,MAAM,EAAU,KAAK,SAAS,EACnD,KAAK,OAAS,KAAK,IAAI,KAAK,UAAW,KAAK,OAAS,EAAU,KAAK,SAAS,EAC7E,KAAK,YAAc,EAAU,KAAK,UAClC,MAAO,KAAK,OAAS,GAAK,KAAK,QAAQ,OAAS,EAC9C,KAAK,QAAU,EACF,KAAK,QAAQ,MAAM,IACzB,GAIf",
8
- "debugId": "FABB2451969B5F3264756E2164756E21",
8
+ "debugId": "7255BEE518CBAF7E64756E2164756E21",
9
9
  "names": []
10
10
  }