sently 0.9.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +39 -34
  4. package/SECURITY.md +59 -0
  5. package/dist/adapters/bun.js +1 -1
  6. package/dist/adapters/cf.js +2 -2
  7. package/dist/adapters/cf.js.map +1 -1
  8. package/dist/adapters/deno.js +1 -1
  9. package/dist/adapters/node.js +1 -1
  10. package/dist/auth/oauth2.js +2 -2
  11. package/dist/auth/oauth2.js.map +1 -1
  12. package/dist/channel-result.d.ts +6 -0
  13. package/dist/channel-result.js +1 -0
  14. package/dist/{chunk-skqhj0rm.js → chunk-1ke3nmyg.js} +3 -3
  15. package/dist/{chunk-skqhj0rm.js.map → chunk-1ke3nmyg.js.map} +1 -1
  16. package/dist/{chunk-7gy2q9sh.js → chunk-5es0v1j7.js} +2 -2
  17. package/dist/{chunk-7gy2q9sh.js.map → chunk-5es0v1j7.js.map} +1 -1
  18. package/dist/{chunk-0qxws3kj.js → chunk-7wtfjx0d.js} +2 -2
  19. package/dist/{chunk-0qxws3kj.js.map → chunk-7wtfjx0d.js.map} +1 -1
  20. package/dist/chunk-8kpgbrba.js +4 -0
  21. package/dist/{chunk-93vqxxj2.js.map → chunk-8kpgbrba.js.map} +2 -2
  22. package/dist/{chunk-vjpds2pf.js → chunk-a057jzsy.js} +2 -2
  23. package/dist/{chunk-vjpds2pf.js.map → chunk-a057jzsy.js.map} +1 -1
  24. package/dist/{chunk-8njyga62.js → chunk-bhr7w3gg.js} +2 -2
  25. package/dist/{chunk-8njyga62.js.map → chunk-bhr7w3gg.js.map} +1 -1
  26. package/dist/chunk-cs1tvehn.js +4 -0
  27. package/dist/chunk-cs1tvehn.js.map +10 -0
  28. package/dist/{chunk-29z7fkzy.js → chunk-d5aw5vag.js} +3 -3
  29. package/dist/{chunk-29z7fkzy.js.map → chunk-d5aw5vag.js.map} +1 -1
  30. package/dist/chunk-f5x8vesr.js +4 -0
  31. package/dist/{chunk-b602dhck.js.map → chunk-f5x8vesr.js.map} +2 -2
  32. package/dist/chunk-fv0bemgf.js +4 -0
  33. package/dist/{chunk-yp311efr.js.map → chunk-fv0bemgf.js.map} +3 -3
  34. package/dist/{chunk-ywq6s10g.js → chunk-hnzmn4s4.js} +2 -2
  35. package/dist/{chunk-ywq6s10g.js.map → chunk-hnzmn4s4.js.map} +1 -1
  36. package/dist/chunk-htdg075t.js +4 -0
  37. package/dist/{chunk-qtkd5bak.js.map → chunk-htdg075t.js.map} +4 -3
  38. package/dist/{chunk-0gqy32pe.js → chunk-q2mahnb7.js} +3 -3
  39. package/dist/chunk-q2mahnb7.js.map +10 -0
  40. package/dist/chunk-sbgt4165.js +4 -0
  41. package/dist/chunk-sbgt4165.js.map +10 -0
  42. package/dist/{chunk-6npp3x3c.js → chunk-th4cwrpb.js} +2 -2
  43. package/dist/{chunk-6npp3x3c.js.map → chunk-th4cwrpb.js.map} +1 -1
  44. package/dist/{chunk-tamww15j.js → chunk-ttbwyxmh.js} +2 -2
  45. package/dist/{chunk-tamww15j.js.map → chunk-ttbwyxmh.js.map} +1 -1
  46. package/dist/chunk-w4c9kj70.js +13 -0
  47. package/dist/{chunk-n0qeyzqm.js.map → chunk-w4c9kj70.js.map} +2 -2
  48. package/dist/chunk-w9sabmb0.js +4 -0
  49. package/dist/chunk-w9sabmb0.js.map +10 -0
  50. package/dist/{chunk-8dty8c4p.js → chunk-yg3kj723.js} +3 -3
  51. package/dist/{chunk-8dty8c4p.js.map → chunk-yg3kj723.js.map} +2 -2
  52. package/dist/chunk-z1589fjk.js +4 -0
  53. package/dist/chunk-z1589fjk.js.map +10 -0
  54. package/dist/core/channel-result.d.ts +36 -0
  55. package/dist/core/channel-result.js +3 -0
  56. package/dist/core/channel-result.js.map +10 -0
  57. package/dist/core/decorated-transport.d.ts +30 -0
  58. package/dist/core/decorator-hooks.d.ts +17 -0
  59. package/dist/core/errors.js +2 -2
  60. package/dist/core/errors.js.map +1 -1
  61. package/dist/core/provider-label.d.ts +7 -3
  62. package/dist/core/push-endpoint.d.ts +5 -0
  63. package/dist/core/push-types.d.ts +48 -14
  64. package/dist/core/sms-types.d.ts +12 -0
  65. package/dist/core/smtp.js +2 -2
  66. package/dist/core/smtp.js.map +1 -1
  67. package/dist/core/whatsapp-types.d.ts +12 -0
  68. package/dist/detect.js +2 -2
  69. package/dist/detect.js.map +1 -1
  70. package/dist/dkim.js +2 -2
  71. package/dist/dkim.js.map +2 -2
  72. package/dist/idempotency.js +2 -2
  73. package/dist/idempotency.js.map +1 -1
  74. package/dist/index.d.ts +4 -0
  75. package/dist/index.js +5 -0
  76. package/dist/mailer.js +2 -2
  77. package/dist/mailer.js.map +1 -1
  78. package/dist/observability/console.js +1 -1
  79. package/dist/plugins/react.js +2 -2
  80. package/dist/plugins/react.js.map +1 -1
  81. package/dist/plugins/template.js +1 -1
  82. package/dist/pool/pool.js +2 -2
  83. package/dist/pool/pool.js.map +1 -1
  84. package/dist/push.d.ts +1 -0
  85. package/dist/push.js +2 -2
  86. package/dist/push.js.map +3 -3
  87. package/dist/sms.d.ts +1 -0
  88. package/dist/sms.js +2 -2
  89. package/dist/sms.js.map +3 -3
  90. package/dist/smtp-mailer.js +2 -2
  91. package/dist/smtp-mailer.js.map +1 -1
  92. package/dist/transports/brevo.js +2 -2
  93. package/dist/transports/brevo.js.map +2 -2
  94. package/dist/transports/cloudflare-email.js +2 -2
  95. package/dist/transports/cloudflare-email.js.map +2 -2
  96. package/dist/transports/fallback.d.ts +32 -6
  97. package/dist/transports/fallback.js +2 -2
  98. package/dist/transports/fallback.js.map +1 -1
  99. package/dist/transports/fcm.d.ts +55 -0
  100. package/dist/transports/fcm.js +4 -0
  101. package/dist/transports/fcm.js.map +10 -0
  102. package/dist/transports/loops.js +2 -2
  103. package/dist/transports/loops.js.map +1 -1
  104. package/dist/transports/mailersend.js +2 -2
  105. package/dist/transports/mailersend.js.map +2 -2
  106. package/dist/transports/mailgun.js +2 -2
  107. package/dist/transports/mailgun.js.map +2 -2
  108. package/dist/transports/mailpit.d.ts +166 -0
  109. package/dist/transports/mailpit.js +3 -0
  110. package/dist/transports/mailpit.js.map +10 -0
  111. package/dist/transports/mailtrap.js +2 -2
  112. package/dist/transports/mailtrap.js.map +2 -2
  113. package/dist/transports/msegat.js +2 -2
  114. package/dist/transports/msegat.js.map +1 -1
  115. package/dist/transports/plunk.js +2 -2
  116. package/dist/transports/plunk.js.map +1 -1
  117. package/dist/transports/postmark.js +2 -2
  118. package/dist/transports/postmark.js.map +2 -2
  119. package/dist/transports/preview.js +2 -2
  120. package/dist/transports/preview.js.map +1 -1
  121. package/dist/transports/resend.js +2 -2
  122. package/dist/transports/resend.js.map +2 -2
  123. package/dist/transports/retry.d.ts +28 -9
  124. package/dist/transports/retry.js +2 -2
  125. package/dist/transports/retry.js.map +3 -3
  126. package/dist/transports/sendgrid.js +2 -2
  127. package/dist/transports/sendgrid.js.map +2 -2
  128. package/dist/transports/ses.js +2 -2
  129. package/dist/transports/ses.js.map +2 -2
  130. package/dist/transports/smtp.js +2 -2
  131. package/dist/transports/smtp.js.map +1 -1
  132. package/dist/transports/sndr.js +2 -2
  133. package/dist/transports/sndr.js.map +1 -1
  134. package/dist/transports/sparkpost.js +2 -2
  135. package/dist/transports/sparkpost.js.map +2 -2
  136. package/dist/transports/taqnyat-mail.js +2 -2
  137. package/dist/transports/taqnyat-mail.js.map +1 -1
  138. package/dist/transports/taqnyat-sms.js +2 -2
  139. package/dist/transports/taqnyat-sms.js.map +1 -1
  140. package/dist/transports/taqnyat-whatsapp.js +2 -2
  141. package/dist/transports/taqnyat-whatsapp.js.map +1 -1
  142. package/dist/transports/twilio-sms.js +2 -2
  143. package/dist/transports/twilio-sms.js.map +2 -2
  144. package/dist/transports/unifonic.d.ts +81 -0
  145. package/dist/transports/unifonic.js +3 -0
  146. package/dist/transports/unifonic.js.map +10 -0
  147. package/dist/transports/webpush.js +2 -2
  148. package/dist/transports/webpush.js.map +3 -3
  149. package/dist/transports/weighted-fallback.d.ts +11 -9
  150. package/dist/transports/weighted-fallback.js +2 -2
  151. package/dist/transports/weighted-fallback.js.map +3 -3
  152. package/dist/transports/whatsapp-cloud.js +2 -2
  153. package/dist/transports/whatsapp-cloud.js.map +1 -1
  154. package/dist/webhooks/brevo.js +2 -2
  155. package/dist/webhooks/brevo.js.map +1 -1
  156. package/dist/webhooks/mailgun.js +2 -2
  157. package/dist/webhooks/mailgun.js.map +1 -1
  158. package/dist/webhooks/postmark.js +2 -2
  159. package/dist/webhooks/postmark.js.map +1 -1
  160. package/dist/webhooks/resend.js +2 -2
  161. package/dist/webhooks/resend.js.map +1 -1
  162. package/dist/webhooks/sendgrid.js +2 -2
  163. package/dist/webhooks/sendgrid.js.map +1 -1
  164. package/dist/webhooks/ses.js +2 -2
  165. package/dist/webhooks/ses.js.map +1 -1
  166. package/dist/webhooks/sndr.js +2 -2
  167. package/dist/webhooks/sndr.js.map +1 -1
  168. package/dist/webhooks/timing-safe-equal.js +1 -1
  169. package/dist/webhooks/twilio-sms.d.ts +20 -0
  170. package/dist/webhooks/twilio-sms.js +3 -0
  171. package/dist/webhooks/twilio-sms.js.map +10 -0
  172. package/dist/webhooks/types.d.ts +37 -3
  173. package/dist/webhooks/types.js +3 -0
  174. package/dist/webhooks/types.js.map +9 -0
  175. package/dist/webhooks/unifonic.d.ts +13 -0
  176. package/dist/webhooks/unifonic.js +3 -0
  177. package/dist/webhooks/unifonic.js.map +10 -0
  178. package/dist/webhooks/whatsapp-cloud.d.ts +14 -0
  179. package/dist/webhooks/whatsapp-cloud.js +3 -0
  180. package/dist/webhooks/whatsapp-cloud.js.map +10 -0
  181. package/dist/webhooks.d.ts +10 -5
  182. package/dist/webhooks.js +11 -0
  183. package/dist/whatsapp.d.ts +1 -0
  184. package/dist/whatsapp.js +2 -2
  185. package/dist/whatsapp.js.map +3 -3
  186. package/package.json +39 -2
  187. package/site/content/docs/ai/llms-txt.mdx +47 -2
  188. package/site/content/docs/channels/email.mdx +3 -0
  189. package/site/content/docs/channels/hooks.mdx +6 -5
  190. package/site/content/docs/channels/index.mdx +5 -3
  191. package/site/content/docs/channels/push.mdx +31 -21
  192. package/site/content/docs/channels/sms.mdx +4 -1
  193. package/site/content/docs/decorators/fallback.mdx +45 -13
  194. package/site/content/docs/decorators/index.mdx +31 -13
  195. package/site/content/docs/decorators/preview.mdx +52 -9
  196. package/site/content/docs/decorators/retry.mdx +32 -9
  197. package/site/content/docs/decorators/weighted-fallback.mdx +27 -6
  198. package/site/content/docs/get-started/entrypoints.mdx +11 -8
  199. package/site/content/docs/get-started/index.mdx +8 -2
  200. package/site/content/docs/get-started/installation.mdx +3 -2
  201. package/site/content/docs/get-started/introduction.mdx +7 -3
  202. package/site/content/docs/get-started/meta.json +4 -1
  203. package/site/content/docs/get-started/migrate-nodemailer.mdx +132 -5
  204. package/site/content/docs/get-started/non-goals.mdx +57 -0
  205. package/site/content/docs/get-started/stability.mdx +69 -0
  206. package/site/content/docs/get-started/support-matrix.mdx +62 -0
  207. package/site/content/docs/guides/compare.mdx +87 -0
  208. package/site/content/docs/guides/failover.mdx +121 -0
  209. package/site/content/docs/guides/index.mdx +2 -0
  210. package/site/content/docs/guides/meta.json +2 -0
  211. package/site/content/docs/guides/security.mdx +38 -8
  212. package/site/content/docs/guides/webhooks.mdx +35 -20
  213. package/site/content/docs/index.mdx +78 -4
  214. package/site/content/docs/meta.json +0 -1
  215. package/site/content/docs/reference/channel-result.mdx +82 -0
  216. package/site/content/docs/reference/exports.mdx +9 -4
  217. package/site/content/docs/reference/meta.json +1 -0
  218. package/site/content/docs/reference/push-options.mdx +58 -3
  219. package/site/content/docs/reference/sms-options.mdx +18 -2
  220. package/site/content/docs/reference/webhook-events.mdx +38 -14
  221. package/site/content/docs/reference/whatsapp-options.mdx +19 -1
  222. package/site/content/docs/transports/fcm.mdx +84 -0
  223. package/site/content/docs/transports/index.mdx +11 -3
  224. package/site/content/docs/transports/mailpit.mdx +123 -0
  225. package/site/content/docs/transports/meta.json +4 -1
  226. package/site/content/docs/transports/smtp.mdx +61 -22
  227. package/site/content/docs/transports/unifonic.mdx +85 -0
  228. package/dist/chunk-0gqy32pe.js.map +0 -10
  229. package/dist/chunk-5915vbcj.js +0 -4
  230. package/dist/chunk-5915vbcj.js.map +0 -10
  231. package/dist/chunk-93vqxxj2.js +0 -4
  232. package/dist/chunk-b602dhck.js +0 -4
  233. package/dist/chunk-gp4fwrs8.js +0 -4
  234. package/dist/chunk-gp4fwrs8.js.map +0 -10
  235. package/dist/chunk-n0qeyzqm.js +0 -13
  236. package/dist/chunk-q8k3m6qe.js +0 -4
  237. package/dist/chunk-q8k3m6qe.js.map +0 -10
  238. package/dist/chunk-qtkd5bak.js +0 -4
  239. package/dist/chunk-yp311efr.js +0 -4
  240. package/site/content/docs/quick-start/email.mdx +0 -40
  241. package/site/content/docs/quick-start/index.mdx +0 -15
  242. package/site/content/docs/quick-start/meta.json +0 -5
  243. package/site/content/docs/quick-start/push.mdx +0 -36
  244. package/site/content/docs/quick-start/sms.mdx +0 -36
  245. package/site/content/docs/quick-start/whatsapp.mdx +0 -38
@@ -4,7 +4,7 @@
4
4
  "sourcesContent": [
5
5
  "/**\n * @module\n * Twilio Messages API transport for SMS.\n *\n * Aligned with Twilio Programmable Messaging\n * ([Message resource](https://www.twilio.com/docs/messaging/api/message-resource)):\n * `POST /2010-04-01/Accounts/{AccountSid}/Messages.json` with\n * `application/x-www-form-urlencoded` and HTTP Basic auth.\n *\n * Sender must be {@link TwilioSmsConfig.from} / `options.from` **or**\n * {@link TwilioSmsConfig.messagingServiceSid} (Twilio selects from the Sender Pool\n * when `From` is omitted).\n *\n * Sently-first: wire into {@link createSmsSender}. Twilio-only products (Verify,\n * Voice, Conversations, etc.) stay off this transport unless added as concrete\n * helpers later.\n *\n * Production auth: set `apiKey` + `authToken` (API Key Secret) with `accountSid`\n * still required in the URL path. Account SID + Auth Token alone is fine for\n * local testing — never hardcode credentials in source.\n *\n * @example\n * ```ts\n * import { createSmsSender } from \"sently/sms\";\n * import { TwilioSmsTransport } from \"sently/transports/twilio-sms\";\n *\n * const sms = createSmsSender({\n * transport: new TwilioSmsTransport({\n * accountSid: process.env.TWILIO_ACCOUNT_SID!,\n * apiKey: process.env.TWILIO_API_KEY!,\n * authToken: process.env.TWILIO_API_KEY_SECRET!,\n * from: \"+15557654321\",\n * }),\n * });\n * ```\n */\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type { SmsOptions, SmsSendResult, SmsTransport } from \"../core/sms-types.js\";\nimport type { VerifyResult } from \"../core/types.js\";\n\n/** Twilio Messages API configuration. */\nexport interface TwilioSmsConfig {\n /**\n * Twilio Account SID — always used in the Messages API URL path.\n * Store in environment variables — never hardcode.\n */\n accountSid: string;\n /**\n * Basic-auth password: API Key Secret when {@link apiKey} is set, otherwise Auth Token.\n * Store in environment variables — never hardcode.\n */\n authToken: string;\n /**\n * Optional Twilio API Key SID used as Basic-auth username (recommended in production).\n * When omitted, {@link accountSid} is used as the Basic-auth username.\n */\n apiKey?: string;\n /**\n * Default From phone number, alphanumeric sender ID, short code, or channel address\n * (E.164 recommended, e.g. `+15557654321`). Either this / `options.from` or\n * {@link messagingServiceSid} is required.\n */\n from?: string;\n /**\n * Messaging Service SID (`MG…`). When set and `From` is omitted, Twilio selects\n * the optimal sender from the service's Sender Pool.\n */\n messagingServiceSid?: string;\n /**\n * Optional status callback URL (`StatusCallback`). Twilio POSTs delivery updates here.\n */\n statusCallback?: string;\n}\n\n/** Error thrown when the Twilio SMS API returns a non-success response. */\nexport class TwilioSmsError extends SentlyError {\n /** Creates a Twilio SMS API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"twilio-sms\",\n cause: apiError,\n });\n this.name = \"TwilioSmsError\";\n }\n}\n\n/**\n * Twilio SMS transport via the Messages REST API (form-urlencoded).\n */\nexport class TwilioSmsTransport implements SmsTransport {\n readonly provider = \"twilio-sms\";\n\n private readonly accountSid: string;\n private readonly authToken: string;\n private readonly apiKey?: string;\n private readonly from?: string;\n private readonly messagingServiceSid?: string;\n private readonly statusCallback?: string;\n\n /** Creates a Twilio SMS transport. */\n constructor(config: TwilioSmsConfig) {\n this.accountSid = config.accountSid;\n this.authToken = config.authToken;\n if (config.apiKey !== undefined) {\n this.apiKey = config.apiKey;\n }\n if (config.from !== undefined) {\n this.from = config.from;\n }\n if (config.messagingServiceSid !== undefined) {\n this.messagingServiceSid = config.messagingServiceSid;\n }\n if (config.statusCallback !== undefined) {\n this.statusCallback = config.statusCallback;\n }\n }\n\n /** Sends an SMS via Twilio Messages API. */\n async send(options: SmsOptions): Promise<SmsSendResult> {\n const from = options.from ?? this.from;\n if (!from && !this.messagingServiceSid) {\n throw new TwilioSmsError(\"Twilio SMS requires From or MessagingServiceSid\", 400, {\n message: \"Missing from / messagingServiceSid\",\n });\n }\n\n // Keep E.164 `+` — Twilio expects it (unlike Taqnyat/Msegat).\n const body = new URLSearchParams({\n To: options.to,\n Body: options.body,\n });\n if (from) {\n body.set(\"From\", from);\n }\n if (this.messagingServiceSid) {\n body.set(\"MessagingServiceSid\", this.messagingServiceSid);\n }\n if (this.statusCallback) {\n body.set(\"StatusCallback\", this.statusCallback);\n }\n\n const url = `https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n ...this.authHeaders(),\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body,\n });\n\n const payload = await this.readJson(response);\n\n if (!response.ok) {\n const message =\n typeof payload === \"object\" &&\n payload !== null &&\n \"message\" in payload &&\n typeof (payload as { message: unknown }).message === \"string\"\n ? (payload as { message: string }).message\n : \"Twilio SMS API error\";\n throw new TwilioSmsError(message, response.status, payload);\n }\n\n const record = payload as {\n sid?: string;\n status?: string;\n };\n\n return {\n messageId: record.sid ?? options.messageId ?? \"\",\n to: options.to,\n status: record.status ?? \"queued\",\n response: record.status ?? \"queued\",\n provider: \"twilio-sms\",\n };\n }\n\n /**\n * Validates credentials via Twilio Account fetch\n * (`GET /2010-04-01/Accounts/{AccountSid}.json`).\n */\n async verify(): Promise<VerifyResult> {\n if (!this.accountSid || !this.authToken) {\n return { ok: false, provider: \"twilio-sms\", message: \"Missing credentials\" };\n }\n\n try {\n const url = `https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}.json`;\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n ...this.authHeaders(),\n Accept: \"application/json\",\n },\n });\n\n if (!response.ok) {\n const payload = await this.readJson(response);\n const message =\n typeof payload === \"object\" &&\n payload !== null &&\n \"message\" in payload &&\n typeof (payload as { message: unknown }).message === \"string\"\n ? (payload as { message: string }).message\n : `HTTP ${response.status}`;\n return { ok: false, provider: \"twilio-sms\", message };\n }\n\n const payload = (await this.readJson(response)) as {\n friendly_name?: string;\n status?: string;\n };\n const label = payload.friendly_name ?? payload.status ?? \"ok\";\n return { ok: true, provider: \"twilio-sms\", message: `Account ${label}` };\n } catch (error) {\n return {\n ok: false,\n provider: \"twilio-sms\",\n message: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n private authHeaders(): { Authorization: string } {\n const authUser = this.apiKey ?? this.accountSid;\n const credentials = encodeBase64(`${authUser}:${this.authToken}`).replace(/\\r\\n/g, \"\");\n return { Authorization: `Basic ${credentials}` };\n }\n\n private async readJson(response: Response): Promise<unknown> {\n return response.json().catch(() => ({}));\n }\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "54563D7C95D0712D64756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "68862E4E64033D4164756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @module
3
+ * Unifonic SMS REST transport (el.cloud).
4
+ *
5
+ * Aligned with Unifonic Send Message
6
+ * (`POST https://el.cloud.unifonic.com/rest/SMS/messages`):
7
+ * JSON body with `AppSid`, `SenderID`, `Recipient`, `Body`.
8
+ *
9
+ * Sently-first: wire into {@link createSmsSender}.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { createSmsSender } from "sently/sms";
14
+ * import { UnifonicTransport } from "sently/transports/unifonic";
15
+ *
16
+ * const sms = createSmsSender({
17
+ * transport: new UnifonicTransport({
18
+ * appSid: process.env.UNIFONIC_APPSID!,
19
+ * senderId: "MyBrand",
20
+ * }),
21
+ * });
22
+ *
23
+ * await sms.send({ to: "+9665xxxxxxxx", body: "Hello" });
24
+ * ```
25
+ */
26
+ import { SentlyError } from "../core/errors.js";
27
+ import type { SmsOptions, SmsSendResult, SmsTransport } from "../core/sms-types.js";
28
+ import type { VerifyResult } from "../core/types.js";
29
+ /** Unifonic SMS API configuration. */
30
+ export interface UnifonicConfig {
31
+ /**
32
+ * Application SID (`AppSid`) from the Unifonic dashboard.
33
+ * Store in environment variables — never hardcode.
34
+ */
35
+ appSid: string;
36
+ /**
37
+ * Default SenderID (brand / approved sender). Required unless every send
38
+ * passes `options.from` (mapped to `SenderID`).
39
+ */
40
+ senderId?: string;
41
+ /**
42
+ * Optional delivery status callback URL forwarded as `statusCallback`.
43
+ * Unifonic requires a public HTTPS URL (el.cloud OpenAPI).
44
+ */
45
+ statusCallback?: string;
46
+ /**
47
+ * When true, request async execution (`async: true`). Default: false.
48
+ */
49
+ async?: boolean;
50
+ }
51
+ /** Error thrown when the Unifonic SMS API returns a non-success response. */
52
+ export declare class UnifonicError extends SentlyError {
53
+ readonly statusCode: number;
54
+ readonly apiError: unknown;
55
+ /** Creates a Unifonic SMS API error with status code and response payload. */
56
+ constructor(message: string, statusCode: number, apiError: unknown);
57
+ }
58
+ /**
59
+ * Normalize a phone number for Unifonic: international digits without `+` or
60
+ * leading `00`.
61
+ */
62
+ export declare function normalizeUnifonicPhone(to: string): string;
63
+ /**
64
+ * Unifonic SMS transport via el.cloud REST (`AppSid` + JSON).
65
+ */
66
+ export declare class UnifonicTransport implements SmsTransport {
67
+ readonly provider = "unifonic";
68
+ private readonly appSid;
69
+ private readonly senderId;
70
+ private readonly statusCallback;
71
+ private readonly async;
72
+ /** Creates an Unifonic SMS transport. */
73
+ constructor(config: UnifonicConfig);
74
+ /**
75
+ * Sends an SMS via `POST https://el.cloud.unifonic.com/rest/SMS/messages`
76
+ * (JSON: `AppSid`, `SenderID`, `Recipient`, `Body`; optional `CorrelationID`).
77
+ */
78
+ send(options: SmsOptions): Promise<SmsSendResult>;
79
+ /** Lightweight credential check. */
80
+ verify(): Promise<VerifyResult>;
81
+ }
@@ -0,0 +1,3 @@
1
+ import{ja as V,ka as W}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";var Z="https://el.cloud.unifonic.com/rest/SMS/messages";class G extends V{statusCode;apiError;constructor(q,v,K){super(q,W(v>=100&&v<600?v:400),{statusCode:v,provider:"unifonic",cause:K});this.statusCode=v;this.apiError=K;this.name="UnifonicError"}}function $(q){let v=q.trim();if(v.startsWith("+"))return v.slice(1).replace(/\D/g,"");if(v.startsWith("00"))return v.slice(2).replace(/\D/g,"");return v.replace(/\D/g,"")}function j(q){let v;try{v=new URL(q)}catch{throw new G("Unifonic statusCallback must be a valid HTTPS URL",400,{field:"statusCallback",value:q})}if(v.protocol!=="https:")throw new G("Unifonic statusCallback must use https:",400,{field:"statusCallback",protocol:v.protocol});if(v.username||v.password)throw new G("Unifonic statusCallback must not include credentials",400,{field:"statusCallback"})}class w{provider="unifonic";appSid;senderId;statusCallback;async;constructor(q){if(this.appSid=q.appSid,this.senderId=q.senderId,q.statusCallback!==void 0)j(q.statusCallback),this.statusCallback=q.statusCallback;else this.statusCallback=void 0;this.async=q.async??!1}async send(q){let v=q.from??this.senderId;if(v===void 0||v.length===0)throw new G("Unifonic requires SenderID via config.senderId or options.from",400,{field:"SenderID"});let K={AppSid:this.appSid,SenderID:v,Recipient:$(q.to),Body:q.body,responseType:"JSON",async:this.async};if(q.messageId!==void 0)K.CorrelationID=q.messageId;if(this.statusCallback!==void 0)K.statusCallback=this.statusCallback;let J=await fetch(Z,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(K)}),M=await J.text(),A;try{A=M.length>0?JSON.parse(M):{}}catch{throw new G("Unifonic returned non-JSON response",J.status,M)}let X=A.success===!0||A.success==="true",Y=A.errorCode==="ER-00"||A.errorCode==="ER00";if(!(J.ok&&(X||Y)))throw new G(A.message||`Unifonic SMS API error (${A.errorCode??J.status})`,J.status>=400?J.status:400,A);let B=A.data??{},Q=typeof B.Status==="string"?B.Status:"accepted";return{messageId:String(B.MessageID??q.messageId??B.CorrelationID??""),to:q.to,status:Q.toLowerCase()==="sent"?"accepted":Q,response:B.Cost!==void 0?`cost: ${B.Cost} ${B.CurrencyCode??""}`.trim():A.message??Q,provider:"unifonic"}}async verify(){let q=Boolean(this.appSid);return{ok:q,provider:"unifonic",message:q?"Credentials present":"Missing appSid"}}}export{$ as normalizeUnifonicPhone,w as UnifonicTransport,G as UnifonicError};
2
+
3
+ //# debugId=EDBC65716C96690264756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/transports/unifonic.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @module\n * Unifonic SMS REST transport (el.cloud).\n *\n * Aligned with Unifonic Send Message\n * (`POST https://el.cloud.unifonic.com/rest/SMS/messages`):\n * JSON body with `AppSid`, `SenderID`, `Recipient`, `Body`.\n *\n * Sently-first: wire into {@link createSmsSender}.\n *\n * @example\n * ```ts\n * import { createSmsSender } from \"sently/sms\";\n * import { UnifonicTransport } from \"sently/transports/unifonic\";\n *\n * const sms = createSmsSender({\n * transport: new UnifonicTransport({\n * appSid: process.env.UNIFONIC_APPSID!,\n * senderId: \"MyBrand\",\n * }),\n * });\n *\n * await sms.send({ to: \"+9665xxxxxxxx\", body: \"Hello\" });\n * ```\n */\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type { SmsOptions, SmsSendResult, SmsTransport } from \"../core/sms-types.js\";\nimport type { VerifyResult } from \"../core/types.js\";\n\nconst UNIFONIC_SEND_URL = \"https://el.cloud.unifonic.com/rest/SMS/messages\";\n\n/** Unifonic SMS API configuration. */\nexport interface UnifonicConfig {\n /**\n * Application SID (`AppSid`) from the Unifonic dashboard.\n * Store in environment variables — never hardcode.\n */\n appSid: string;\n /**\n * Default SenderID (brand / approved sender). Required unless every send\n * passes `options.from` (mapped to `SenderID`).\n */\n senderId?: string;\n /**\n * Optional delivery status callback URL forwarded as `statusCallback`.\n * Unifonic requires a public HTTPS URL (el.cloud OpenAPI).\n */\n statusCallback?: string;\n /**\n * When true, request async execution (`async: true`). Default: false.\n */\n async?: boolean;\n}\n\n/** Error thrown when the Unifonic SMS API returns a non-success response. */\nexport class UnifonicError extends SentlyError {\n /** Creates a Unifonic SMS API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(\n message,\n httpStatusToSentlyCode(statusCode >= 100 && statusCode < 600 ? statusCode : 400),\n {\n statusCode,\n provider: \"unifonic\",\n cause: apiError,\n },\n );\n this.name = \"UnifonicError\";\n }\n}\n\n/**\n * Normalize a phone number for Unifonic: international digits without `+` or\n * leading `00`.\n */\nexport function normalizeUnifonicPhone(to: string): string {\n const trimmed = to.trim();\n if (trimmed.startsWith(\"+\")) {\n return trimmed.slice(1).replace(/\\D/g, \"\");\n }\n if (trimmed.startsWith(\"00\")) {\n return trimmed.slice(2).replace(/\\D/g, \"\");\n }\n return trimmed.replace(/\\D/g, \"\");\n}\n\n/** Unifonic OpenAPI: statusCallback must be a public HTTPS URL. */\nfunction assertHttpsStatusCallback(url: string): void {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new UnifonicError(\"Unifonic statusCallback must be a valid HTTPS URL\", 400, {\n field: \"statusCallback\",\n value: url,\n });\n }\n if (parsed.protocol !== \"https:\") {\n throw new UnifonicError(\"Unifonic statusCallback must use https:\", 400, {\n field: \"statusCallback\",\n protocol: parsed.protocol,\n });\n }\n if (parsed.username || parsed.password) {\n throw new UnifonicError(\"Unifonic statusCallback must not include credentials\", 400, {\n field: \"statusCallback\",\n });\n }\n}\n\n/**\n * Unifonic SMS transport via el.cloud REST (`AppSid` + JSON).\n */\nexport class UnifonicTransport implements SmsTransport {\n readonly provider = \"unifonic\";\n\n private readonly appSid: string;\n private readonly senderId: string | undefined;\n private readonly statusCallback: string | undefined;\n private readonly async: boolean;\n\n /** Creates an Unifonic SMS transport. */\n constructor(config: UnifonicConfig) {\n this.appSid = config.appSid;\n this.senderId = config.senderId;\n if (config.statusCallback !== undefined) {\n assertHttpsStatusCallback(config.statusCallback);\n this.statusCallback = config.statusCallback;\n } else {\n this.statusCallback = undefined;\n }\n this.async = config.async ?? false;\n }\n\n /**\n * Sends an SMS via `POST https://el.cloud.unifonic.com/rest/SMS/messages`\n * (JSON: `AppSid`, `SenderID`, `Recipient`, `Body`; optional `CorrelationID`).\n */\n async send(options: SmsOptions): Promise<SmsSendResult> {\n const senderId = options.from ?? this.senderId;\n if (senderId === undefined || senderId.length === 0) {\n throw new UnifonicError(\n \"Unifonic requires SenderID via config.senderId or options.from\",\n 400,\n { field: \"SenderID\" },\n );\n }\n\n const body: Record<string, unknown> = {\n AppSid: this.appSid,\n SenderID: senderId,\n Recipient: normalizeUnifonicPhone(options.to),\n Body: options.body,\n responseType: \"JSON\",\n async: this.async,\n };\n\n if (options.messageId !== undefined) {\n body.CorrelationID = options.messageId;\n }\n if (this.statusCallback !== undefined) {\n body.statusCallback = this.statusCallback;\n }\n\n const response = await fetch(UNIFONIC_SEND_URL, {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const text = await response.text();\n let payload: {\n /** OpenAPI examples use both boolean `true` and string `\"true\"`. */\n success?: boolean | string;\n message?: string;\n errorCode?: string;\n data?: {\n MessageID?: number | string;\n CorrelationID?: string;\n Status?: string;\n Cost?: number;\n Balance?: number;\n Recipient?: string;\n CurrencyCode?: string;\n };\n };\n try {\n payload = text.length > 0 ? (JSON.parse(text) as typeof payload) : {};\n } catch {\n throw new UnifonicError(\"Unifonic returned non-JSON response\", response.status, text);\n }\n\n // OpenAPI examples use both boolean `true` and string `\"true\"` for success.\n const successFlag = payload.success === true || payload.success === \"true\";\n const successCode = payload.errorCode === \"ER-00\" || payload.errorCode === \"ER00\";\n const success = response.ok && (successFlag || successCode);\n\n if (!success) {\n throw new UnifonicError(\n payload.message || `Unifonic SMS API error (${payload.errorCode ?? response.status})`,\n response.status >= 400 ? response.status : 400,\n payload,\n );\n }\n\n const data = payload.data ?? {};\n const status = typeof data.Status === \"string\" ? data.Status : \"accepted\";\n\n return {\n messageId: String(data.MessageID ?? options.messageId ?? data.CorrelationID ?? \"\"),\n to: options.to,\n status: status.toLowerCase() === \"sent\" ? \"accepted\" : status,\n response:\n data.Cost !== undefined\n ? `cost: ${data.Cost} ${data.CurrencyCode ?? \"\"}`.trim()\n : (payload.message ?? status),\n provider: \"unifonic\",\n };\n }\n\n /** Lightweight credential check. */\n async verify(): Promise<VerifyResult> {\n const ok = Boolean(this.appSid);\n return {\n ok,\n provider: \"unifonic\",\n message: ok ? \"Credentials present\" : \"Missing appSid\",\n };\n }\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "EDBC65716C96690264756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,3 +1,3 @@
1
- import{U as T}from"../chunk-yp311efr.js";import{$ as N,aa as H,ba as J}from"../chunk-ywq6s10g.js";import{ca as m,da as v}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class V extends m{statusCode;apiError;constructor(q,z,Q){super(q,v(z),{statusCode:z,provider:"webpush",cause:Q});this.statusCode=z;this.apiError=Q;this.name="WebPushError"}}var k=2419200,U=4096,C=4096;function X(q){return q.buffer.slice(q.byteOffset,q.byteOffset+q.byteLength)}function j(...q){let z=q.reduce((G,Z)=>G+Z.length,0),Q=new Uint8Array(z),$=0;for(let G of q)Q.set(G,$),$+=G.length;return Q}function g(q){let z=new Uint8Array(4);return new DataView(z.buffer).setUint32(0,q,!1),z}function K(q){if(q.length!==65||q[0]!==4)throw new V("Invalid P-256 uncompressed public key",400,{length:q.length});return{kty:"EC",crv:"P-256",x:N(q.subarray(1,33)),y:N(q.subarray(33,65))}}function E(q,z){return{...K(q),d:N(z)}}async function _(q,z){let Q=await crypto.subtle.importKey("raw",X(q),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),$=await crypto.subtle.sign("HMAC",Q,X(z));return new Uint8Array($)}async function w(q,z,Q,$){let G=await _(q,z);return(await _(G,j(Q,new Uint8Array([1])))).subarray(0,$)}async function B(q,z){let Q=H(q.keys.p256dh),$=H(q.keys.auth);if(Q.length!==65||Q[0]!==4)throw new V("Invalid subscription p256dh key",400,{length:Q.length});if($.length<16)throw new V("Invalid subscription auth key",400,{length:$.length});let G=await crypto.subtle.generateKey({name:"ECDH",namedCurve:"P-256"},!0,["deriveBits"]),Z=new Uint8Array(await crypto.subtle.exportKey("raw",G.publicKey)),F=await crypto.subtle.importKey("raw",X(Q),{name:"ECDH",namedCurve:"P-256"},!1,[]),M=new Uint8Array(await crypto.subtle.deriveBits({name:"ECDH",public:F},G.privateKey,256)),L=j(J("WebPush: info\x00"),Q,Z),x=await w($,M,L,32),O=crypto.getRandomValues(new Uint8Array(16)),I=J("Content-Encoding: aes128gcm\x00"),R=J("Content-Encoding: nonce\x00"),Y=await w(O,x,I,16),D=await w(O,x,R,12),A=j(z,new Uint8Array([2])),W=await crypto.subtle.importKey("raw",X(Y),"AES-GCM",!1,["encrypt"]),S=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:X(D)},W,X(A)));return j(O,g(U),new Uint8Array([Z.length]),Z,S)}async function P(q,z,Q,$){let G=H(Q),Z=H($),F=E(G,Z),M=await crypto.subtle.importKey("jwk",F,{name:"ECDSA",namedCurve:"P-256"},!1,["sign"]),L=new URL(q).origin,x={typ:"JWT",alg:"ES256"},O={aud:L,exp:Math.floor(Date.now()/1000)+43200,sub:z},I=N(J(JSON.stringify(x))),R=N(J(JSON.stringify(O))),Y=`${I}.${R}`,D=new Uint8Array(await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},M,X(J(Y))));return`${Y}.${N(D)}`}class h{provider="webpush";vapidPublicKey;vapidPrivateKey;subject;allowedEndpointHosts;constructor(q){this.vapidPublicKey=q.vapidPublicKey,this.vapidPrivateKey=q.vapidPrivateKey,this.subject=q.subject,this.allowedEndpointHosts=q.allowedEndpointHosts??[]}async send(q){try{T(q.subscription.endpoint,this.allowedEndpointHosts)}catch(M){let L;try{L=new URL(q.subscription.endpoint).hostname}catch{L=void 0}throw new V(M instanceof Error?M.message:"Unsafe push subscription endpoint",400,{endpointHost:L})}let z={title:q.title,body:q.body,...q.data!==void 0?{data:q.data}:{},...q.icon!==void 0?{icon:q.icon}:{}},Q=J(JSON.stringify(z)),$=await B(q.subscription,Q);if($.length>C)throw new V(`Encrypted push payload exceeds ${C} bytes (RFC 8291)`,413,{size:$.length});let G=await P(q.subscription.endpoint,this.subject,this.vapidPublicKey,this.vapidPrivateKey),Z=q.ttl??k,F=await fetch(q.subscription.endpoint,{method:"POST",redirect:"manual",headers:{Authorization:`vapid t=${G}, k=${this.vapidPublicKey}`,"Content-Encoding":"aes128gcm",TTL:String(Z),"Content-Type":"application/octet-stream"},body:X($)});if(F.status>=300&&F.status<400)throw new V(`Push service returned redirect ${F.status}; redirects are not followed`,F.status,{location:F.headers.get("location")});if(!F.ok){let M=await F.text().catch(()=>"");throw new V(M||`Web Push request failed with status ${F.status}`,F.status,M||{status:F.status})}return{messageId:q.messageId??crypto.randomUUID(),status:"accepted",response:String(F.status),provider:"webpush"}}async verify(){try{let q=H(this.vapidPublicKey),z=H(this.vapidPrivateKey),Q=q.length===65&&z.length===32&&this.subject.length>0;return{ok:Q,provider:"webpush",message:Q?"VAPID credentials present":"Invalid VAPID key lengths"}}catch(q){return{ok:!1,provider:"webpush",message:q instanceof Error?q.message:"Invalid VAPID credentials"}}}}export{h as WebPushTransport,V as WebPushError};
1
+ import{W as T}from"../chunk-fv0bemgf.js";import{_ as A}from"../chunk-z1589fjk.js";import{ga as N,ha as H,ia as J}from"../chunk-hnzmn4s4.js";import{ja as m,ka as v}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";class V extends m{statusCode;apiError;constructor(q,z,Q){super(q,v(z),{statusCode:z,provider:"webpush",cause:Q});this.statusCode=z;this.apiError=Q;this.name="WebPushError"}}var U=2419200,g=4096,C=4096;function X(q){return q.buffer.slice(q.byteOffset,q.byteOffset+q.byteLength)}function I(...q){let z=q.reduce((G,Z)=>G+Z.length,0),Q=new Uint8Array(z),$=0;for(let G of q)Q.set(G,$),$+=G.length;return Q}function K(q){let z=new Uint8Array(4);return new DataView(z.buffer).setUint32(0,q,!1),z}function B(q){if(q.length!==65||q[0]!==4)throw new V("Invalid P-256 uncompressed public key",400,{length:q.length});return{kty:"EC",crv:"P-256",x:N(q.subarray(1,33)),y:N(q.subarray(33,65))}}function E(q,z){return{...B(q),d:N(z)}}async function _(q,z){let Q=await crypto.subtle.importKey("raw",X(q),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),$=await crypto.subtle.sign("HMAC",Q,X(z));return new Uint8Array($)}async function w(q,z,Q,$){let G=await _(q,z);return(await _(G,I(Q,new Uint8Array([1])))).subarray(0,$)}async function P(q,z){let Q=H(q.keys.p256dh),$=H(q.keys.auth);if(Q.length!==65||Q[0]!==4)throw new V("Invalid subscription p256dh key",400,{length:Q.length});if($.length<16)throw new V("Invalid subscription auth key",400,{length:$.length});let G=await crypto.subtle.generateKey({name:"ECDH",namedCurve:"P-256"},!0,["deriveBits"]),Z=new Uint8Array(await crypto.subtle.exportKey("raw",G.publicKey)),F=await crypto.subtle.importKey("raw",X(Q),{name:"ECDH",namedCurve:"P-256"},!1,[]),M=new Uint8Array(await crypto.subtle.deriveBits({name:"ECDH",public:F},G.privateKey,256)),L=I(J("WebPush: info\x00"),Q,Z),x=await w($,M,L,32),O=crypto.getRandomValues(new Uint8Array(16)),R=J("Content-Encoding: aes128gcm\x00"),j=J("Content-Encoding: nonce\x00"),Y=await w(O,x,R,16),D=await w(O,x,j,12),S=I(z,new Uint8Array([2])),W=await crypto.subtle.importKey("raw",X(Y),"AES-GCM",!1,["encrypt"]),k=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:X(D)},W,X(S)));return I(O,K(g),new Uint8Array([Z.length]),Z,k)}async function h(q,z,Q,$){let G=H(Q),Z=H($),F=E(G,Z),M=await crypto.subtle.importKey("jwk",F,{name:"ECDSA",namedCurve:"P-256"},!1,["sign"]),L=new URL(q).origin,x={typ:"JWT",alg:"ES256"},O={aud:L,exp:Math.floor(Date.now()/1000)+43200,sub:z},R=N(J(JSON.stringify(x))),j=N(J(JSON.stringify(O))),Y=`${R}.${j}`,D=new Uint8Array(await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},M,X(J(Y))));return`${Y}.${N(D)}`}class f{provider="webpush";vapidPublicKey;vapidPrivateKey;subject;allowedEndpointHosts;constructor(q){this.vapidPublicKey=q.vapidPublicKey,this.vapidPrivateKey=q.vapidPrivateKey,this.subject=q.subject,this.allowedEndpointHosts=q.allowedEndpointHosts??[]}async send(q){if(!A(q))throw new V("WebPushTransport requires PushOptions.subscription; use FcmTransport for device tokens",400,{hint:"fcm"});try{T(q.subscription.endpoint,this.allowedEndpointHosts)}catch(M){let L;try{L=new URL(q.subscription.endpoint).hostname}catch{L=void 0}throw new V(M instanceof Error?M.message:"Unsafe push subscription endpoint",400,{endpointHost:L})}let z={title:q.title,body:q.body,...q.data!==void 0?{data:q.data}:{},...q.icon!==void 0?{icon:q.icon}:{}},Q=J(JSON.stringify(z)),$=await P(q.subscription,Q);if($.length>C)throw new V(`Encrypted push payload exceeds ${C} bytes (RFC 8291)`,413,{size:$.length});let G=await h(q.subscription.endpoint,this.subject,this.vapidPublicKey,this.vapidPrivateKey),Z=q.ttl??U,F=await fetch(q.subscription.endpoint,{method:"POST",redirect:"manual",headers:{Authorization:`vapid t=${G}, k=${this.vapidPublicKey}`,"Content-Encoding":"aes128gcm",TTL:String(Z),"Content-Type":"application/octet-stream"},body:X($)});if(F.status>=300&&F.status<400)throw new V(`Push service returned redirect ${F.status}; redirects are not followed`,F.status,{location:F.headers.get("location")});if(!F.ok){let M=await F.text().catch(()=>"");throw new V(M||`Web Push request failed with status ${F.status}`,F.status,M||{status:F.status})}return{messageId:q.messageId??crypto.randomUUID(),status:"accepted",response:String(F.status),provider:"webpush"}}async verify(){try{let q=H(this.vapidPublicKey),z=H(this.vapidPrivateKey),Q=q.length===65&&z.length===32&&this.subject.length>0;return{ok:Q,provider:"webpush",message:Q?"VAPID credentials present":"Invalid VAPID key lengths"}}catch(q){return{ok:!1,provider:"webpush",message:q instanceof Error?q.message:"Invalid VAPID credentials"}}}}export{f as WebPushTransport,V as WebPushError};
2
2
 
3
- //# debugId=43FC53C9208BBE5564756E2164756E21
3
+ //# debugId=DFA1A46FC61CEB2264756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/transports/webpush.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @module\n * Web Push transport (RFC 8292 VAPID + RFC 8291 aes128gcm).\n *\n * VAPID keys are the common web-push raw format: base64url-encoded\n * uncompressed P-256 public key (65 bytes) and raw private key (32 bytes).\n *\n * Store `vapidPrivateKey` in a secrets manager / environment variable — never\n * hardcode it or commit it to source control.\n *\n * @example\n * ```ts\n * import { createPushSender } from \"sently/push\";\n * import { WebPushTransport } from \"sently/transports/webpush\";\n *\n * const push = createPushSender({\n * transport: new WebPushTransport({\n * vapidPublicKey: process.env.VAPID_PUBLIC_KEY!,\n * vapidPrivateKey: process.env.VAPID_PRIVATE_KEY!,\n * subject: \"mailto:you@example.com\",\n * }),\n * });\n * ```\n */\nimport { decodeBase64Url, encodeBase64Url, encodeUtf8 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport { assertSafePushEndpoint } from \"../core/push-endpoint.js\";\nimport type {\n PushOptions,\n PushSendResult,\n PushSubscription,\n PushTransport,\n} from \"../core/push-types.js\";\nimport type { VerifyResult } from \"../core/types.js\";\n\n/** Web Push / VAPID configuration. */\nexport interface WebPushConfig {\n /** Base64url-encoded uncompressed P-256 public key (65 bytes). */\n vapidPublicKey: string;\n /**\n * Base64url-encoded raw P-256 private key (32 bytes).\n * Treat as a tier-1 secret — inject from env / secrets manager only.\n */\n vapidPrivateKey: string;\n /** Contact URI for the VAPID `sub` claim (`mailto:` or `https:`). */\n subject: string;\n /**\n * Extra exact hostnames allowed for `subscription.endpoint` beyond the\n * built-in FCM / Mozilla / Apple / WNS allowlist. Use for private push relays.\n */\n allowedEndpointHosts?: string[];\n}\n\n/** Error thrown when a push service rejects the request. */\nexport class WebPushError extends SentlyError {\n /** Creates a Web Push error with HTTP status and response body. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"webpush\",\n cause: apiError,\n });\n this.name = \"WebPushError\";\n }\n}\n\n/** Default TTL (28 days) in seconds. */\nconst DEFAULT_TTL_SECONDS = 2419200;\n/** RFC 8188 / Web Push record size. */\nconst RECORD_SIZE = 4096;\n/**\n * Maximum encrypted body size push services are required to accept (RFC 8291).\n * Includes salt, rs, keyid, and ciphertext+tag.\n */\nconst MAX_ENCRYPTED_BODY_BYTES = 4096;\n\nfunction toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n\nfunction concatBytes(...parts: Uint8Array[]): Uint8Array {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nfunction uint32Be(value: number): Uint8Array {\n const out = new Uint8Array(4);\n new DataView(out.buffer).setUint32(0, value, false);\n return out;\n}\n\nfunction ecPublicToJwk(publicKey: Uint8Array): JsonWebKey {\n if (publicKey.length !== 65 || publicKey[0] !== 0x04) {\n throw new WebPushError(\"Invalid P-256 uncompressed public key\", 400, {\n length: publicKey.length,\n });\n }\n return {\n kty: \"EC\",\n crv: \"P-256\",\n x: encodeBase64Url(publicKey.subarray(1, 33)),\n y: encodeBase64Url(publicKey.subarray(33, 65)),\n };\n}\n\nfunction ecKeyPairToJwk(publicKey: Uint8Array, privateKey: Uint8Array): JsonWebKey {\n return {\n ...ecPublicToJwk(publicKey),\n d: encodeBase64Url(privateKey),\n };\n}\n\nasync function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n toArrayBuffer(key),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await crypto.subtle.sign(\"HMAC\", cryptoKey, toArrayBuffer(data));\n return new Uint8Array(sig);\n}\n\n/** HKDF-Extract + Expand (HMAC-SHA-256) for a single block (L ≤ 32). */\nasync function hkdf(\n salt: Uint8Array,\n ikm: Uint8Array,\n info: Uint8Array,\n length: number,\n): Promise<Uint8Array> {\n const prk = await hmacSha256(salt, ikm);\n const block = await hmacSha256(prk, concatBytes(info, new Uint8Array([0x01])));\n return block.subarray(0, length);\n}\n\n/**\n * Encrypt a payload per RFC 8291 (aes128gcm content coding).\n *\n * Confidence note: Implemented from RFC 8291 / RFC 8188. Tests assert header\n * shapes, framing, and size limits; full known-vector fixtures were not\n * available in-repo — treat as high-scrutiny crypto.\n */\nasync function encryptAes128Gcm(\n subscription: PushSubscription,\n plaintext: Uint8Array,\n): Promise<Uint8Array> {\n const uaPublic = decodeBase64Url(subscription.keys.p256dh);\n const authSecret = decodeBase64Url(subscription.keys.auth);\n\n if (uaPublic.length !== 65 || uaPublic[0] !== 0x04) {\n throw new WebPushError(\"Invalid subscription p256dh key\", 400, {\n length: uaPublic.length,\n });\n }\n if (authSecret.length < 16) {\n throw new WebPushError(\"Invalid subscription auth key\", 400, {\n length: authSecret.length,\n });\n }\n\n const asKeyPair = await crypto.subtle.generateKey({ name: \"ECDH\", namedCurve: \"P-256\" }, true, [\n \"deriveBits\",\n ]);\n const asPublicRaw = new Uint8Array(await crypto.subtle.exportKey(\"raw\", asKeyPair.publicKey));\n\n const uaKey = await crypto.subtle.importKey(\n \"raw\",\n toArrayBuffer(uaPublic),\n { name: \"ECDH\", namedCurve: \"P-256\" },\n false,\n [],\n );\n\n const ecdhSecret = new Uint8Array(\n await crypto.subtle.deriveBits({ name: \"ECDH\", public: uaKey }, asKeyPair.privateKey, 256),\n );\n\n const keyInfo = concatBytes(encodeUtf8(\"WebPush: info\\0\"), uaPublic, asPublicRaw);\n const ikm = await hkdf(authSecret, ecdhSecret, keyInfo, 32);\n\n const salt = crypto.getRandomValues(new Uint8Array(16));\n const cekInfo = encodeUtf8(\"Content-Encoding: aes128gcm\\0\");\n const nonceInfo = encodeUtf8(\"Content-Encoding: nonce\\0\");\n const cek = await hkdf(salt, ikm, cekInfo, 16);\n const nonce = await hkdf(salt, ikm, nonceInfo, 12);\n\n // RFC 8188 final-record padding delimiter\n const padded = concatBytes(plaintext, new Uint8Array([0x02]));\n\n const aesKey = await crypto.subtle.importKey(\"raw\", toArrayBuffer(cek), \"AES-GCM\", false, [\n \"encrypt\",\n ]);\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv: toArrayBuffer(nonce) },\n aesKey,\n toArrayBuffer(padded),\n ),\n );\n\n // salt || rs || idlen || keyid || ciphertext\n return concatBytes(\n salt,\n uint32Be(RECORD_SIZE),\n new Uint8Array([asPublicRaw.length]),\n asPublicRaw,\n ciphertext,\n );\n}\n\nasync function signVapidJwt(\n endpoint: string,\n subject: string,\n vapidPublicKey: string,\n vapidPrivateKey: string,\n): Promise<string> {\n const publicKey = decodeBase64Url(vapidPublicKey);\n const privateKey = decodeBase64Url(vapidPrivateKey);\n const jwk = ecKeyPairToJwk(publicKey, privateKey);\n\n const key = await crypto.subtle.importKey(\n \"jwk\",\n jwk,\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n false,\n [\"sign\"],\n );\n\n const origin = new URL(endpoint).origin;\n const header = { typ: \"JWT\", alg: \"ES256\" };\n const payload = {\n aud: origin,\n exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60,\n sub: subject,\n };\n\n const encodedHeader = encodeBase64Url(encodeUtf8(JSON.stringify(header)));\n const encodedPayload = encodeBase64Url(encodeUtf8(JSON.stringify(payload)));\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n\n const signature = new Uint8Array(\n await crypto.subtle.sign(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n key,\n toArrayBuffer(encodeUtf8(signingInput)),\n ),\n );\n\n return `${signingInput}.${encodeBase64Url(signature)}`;\n}\n\n/**\n * Web Push transport — VAPID auth + RFC 8291 payload encryption.\n *\n * Endpoint URLs are validated against an allowlist before fetch to mitigate SSRF.\n * Redirects are not followed.\n */\nexport class WebPushTransport implements PushTransport {\n readonly provider = \"webpush\";\n\n private readonly vapidPublicKey: string;\n private readonly vapidPrivateKey: string;\n private readonly subject: string;\n private readonly allowedEndpointHosts: readonly string[];\n\n /** Creates a Web Push transport with VAPID credentials. */\n constructor(config: WebPushConfig) {\n this.vapidPublicKey = config.vapidPublicKey;\n this.vapidPrivateKey = config.vapidPrivateKey;\n this.subject = config.subject;\n this.allowedEndpointHosts = config.allowedEndpointHosts ?? [];\n }\n\n /** Encrypts and POSTs a notification to the subscription endpoint. */\n async send(options: PushOptions): Promise<PushSendResult> {\n try {\n assertSafePushEndpoint(options.subscription.endpoint, this.allowedEndpointHosts);\n } catch (error) {\n let endpointHost: string | undefined;\n try {\n endpointHost = new URL(options.subscription.endpoint).hostname;\n } catch {\n endpointHost = undefined;\n }\n throw new WebPushError(\n error instanceof Error ? error.message : \"Unsafe push subscription endpoint\",\n 400,\n { endpointHost },\n );\n }\n\n const notification = {\n title: options.title,\n body: options.body,\n ...(options.data !== undefined ? { data: options.data } : {}),\n ...(options.icon !== undefined ? { icon: options.icon } : {}),\n };\n const plaintext = encodeUtf8(JSON.stringify(notification));\n const encrypted = await encryptAes128Gcm(options.subscription, plaintext);\n\n if (encrypted.length > MAX_ENCRYPTED_BODY_BYTES) {\n throw new WebPushError(\n `Encrypted push payload exceeds ${MAX_ENCRYPTED_BODY_BYTES} bytes (RFC 8291)`,\n 413,\n { size: encrypted.length },\n );\n }\n\n const jwt = await signVapidJwt(\n options.subscription.endpoint,\n this.subject,\n this.vapidPublicKey,\n this.vapidPrivateKey,\n );\n\n const ttl = options.ttl ?? DEFAULT_TTL_SECONDS;\n const response = await fetch(options.subscription.endpoint, {\n method: \"POST\",\n redirect: \"manual\",\n headers: {\n Authorization: `vapid t=${jwt}, k=${this.vapidPublicKey}`,\n \"Content-Encoding\": \"aes128gcm\",\n TTL: String(ttl),\n \"Content-Type\": \"application/octet-stream\",\n },\n body: toArrayBuffer(encrypted),\n });\n\n // Opaque redirect responses (3xx) must not be followed — SSRF mitigation.\n if (response.status >= 300 && response.status < 400) {\n throw new WebPushError(\n `Push service returned redirect ${response.status}; redirects are not followed`,\n response.status,\n { location: response.headers.get(\"location\") },\n );\n }\n\n if (!response.ok) {\n const bodyText = await response.text().catch(() => \"\");\n throw new WebPushError(\n bodyText || `Web Push request failed with status ${response.status}`,\n response.status,\n bodyText || { status: response.status },\n );\n }\n\n return {\n messageId: options.messageId ?? crypto.randomUUID(),\n status: \"accepted\",\n response: String(response.status),\n provider: \"webpush\",\n };\n }\n\n /** Lightweight VAPID credential shape check. */\n async verify(): Promise<VerifyResult> {\n try {\n const pub = decodeBase64Url(this.vapidPublicKey);\n const priv = decodeBase64Url(this.vapidPrivateKey);\n const ok = pub.length === 65 && priv.length === 32 && this.subject.length > 0;\n return {\n ok,\n provider: \"webpush\",\n message: ok ? \"VAPID credentials present\" : \"Invalid VAPID key lengths\",\n };\n } catch (error) {\n return {\n ok: false,\n provider: \"webpush\",\n message: error instanceof Error ? error.message : \"Invalid VAPID credentials\",\n };\n }\n }\n}\n"
5
+ "/**\n * @module\n * Web Push transport (RFC 8292 VAPID + RFC 8291 aes128gcm).\n *\n * VAPID keys are the common web-push raw format: base64url-encoded\n * uncompressed P-256 public key (65 bytes) and raw private key (32 bytes).\n *\n * Store `vapidPrivateKey` in a secrets manager / environment variable — never\n * hardcode it or commit it to source control.\n *\n * @example\n * ```ts\n * import { createPushSender } from \"sently/push\";\n * import { WebPushTransport } from \"sently/transports/webpush\";\n *\n * const push = createPushSender({\n * transport: new WebPushTransport({\n * vapidPublicKey: process.env.VAPID_PUBLIC_KEY!,\n * vapidPrivateKey: process.env.VAPID_PRIVATE_KEY!,\n * subject: \"mailto:you@example.com\",\n * }),\n * });\n * ```\n */\nimport { decodeBase64Url, encodeBase64Url, encodeUtf8 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport { assertSafePushEndpoint } from \"../core/push-endpoint.js\";\nimport type {\n PushOptions,\n PushSendResult,\n PushSubscription,\n PushTransport,\n} from \"../core/push-types.js\";\nimport { isWebPushOptions } from \"../core/push-types.js\";\nimport type { VerifyResult } from \"../core/types.js\";\n\n/** Web Push / VAPID configuration. */\nexport interface WebPushConfig {\n /** Base64url-encoded uncompressed P-256 public key (65 bytes). */\n vapidPublicKey: string;\n /**\n * Base64url-encoded raw P-256 private key (32 bytes).\n * Treat as a tier-1 secret — inject from env / secrets manager only.\n */\n vapidPrivateKey: string;\n /** Contact URI for the VAPID `sub` claim (`mailto:` or `https:`). */\n subject: string;\n /**\n * Extra exact hostnames allowed for `subscription.endpoint` beyond the\n * built-in FCM / Mozilla / Apple / WNS allowlist. Use for private push relays.\n */\n allowedEndpointHosts?: string[];\n}\n\n/** Error thrown when a push service rejects the request. */\nexport class WebPushError extends SentlyError {\n /** Creates a Web Push error with HTTP status and response body. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"webpush\",\n cause: apiError,\n });\n this.name = \"WebPushError\";\n }\n}\n\n/** Default TTL (28 days) in seconds. */\nconst DEFAULT_TTL_SECONDS = 2419200;\n/** RFC 8188 / Web Push record size. */\nconst RECORD_SIZE = 4096;\n/**\n * Maximum encrypted body size push services are required to accept (RFC 8291).\n * Includes salt, rs, keyid, and ciphertext+tag.\n */\nconst MAX_ENCRYPTED_BODY_BYTES = 4096;\n\nfunction toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n\nfunction concatBytes(...parts: Uint8Array[]): Uint8Array {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nfunction uint32Be(value: number): Uint8Array {\n const out = new Uint8Array(4);\n new DataView(out.buffer).setUint32(0, value, false);\n return out;\n}\n\nfunction ecPublicToJwk(publicKey: Uint8Array): JsonWebKey {\n if (publicKey.length !== 65 || publicKey[0] !== 0x04) {\n throw new WebPushError(\"Invalid P-256 uncompressed public key\", 400, {\n length: publicKey.length,\n });\n }\n return {\n kty: \"EC\",\n crv: \"P-256\",\n x: encodeBase64Url(publicKey.subarray(1, 33)),\n y: encodeBase64Url(publicKey.subarray(33, 65)),\n };\n}\n\nfunction ecKeyPairToJwk(publicKey: Uint8Array, privateKey: Uint8Array): JsonWebKey {\n return {\n ...ecPublicToJwk(publicKey),\n d: encodeBase64Url(privateKey),\n };\n}\n\nasync function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n toArrayBuffer(key),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await crypto.subtle.sign(\"HMAC\", cryptoKey, toArrayBuffer(data));\n return new Uint8Array(sig);\n}\n\n/** HKDF-Extract + Expand (HMAC-SHA-256) for a single block (L ≤ 32). */\nasync function hkdf(\n salt: Uint8Array,\n ikm: Uint8Array,\n info: Uint8Array,\n length: number,\n): Promise<Uint8Array> {\n const prk = await hmacSha256(salt, ikm);\n const block = await hmacSha256(prk, concatBytes(info, new Uint8Array([0x01])));\n return block.subarray(0, length);\n}\n\n/**\n * Encrypt a payload per RFC 8291 (aes128gcm content coding).\n *\n * Confidence note: Implemented from RFC 8291 / RFC 8188. Tests assert header\n * shapes, framing, and size limits; full known-vector fixtures were not\n * available in-repo — treat as high-scrutiny crypto.\n */\nasync function encryptAes128Gcm(\n subscription: PushSubscription,\n plaintext: Uint8Array,\n): Promise<Uint8Array> {\n const uaPublic = decodeBase64Url(subscription.keys.p256dh);\n const authSecret = decodeBase64Url(subscription.keys.auth);\n\n if (uaPublic.length !== 65 || uaPublic[0] !== 0x04) {\n throw new WebPushError(\"Invalid subscription p256dh key\", 400, {\n length: uaPublic.length,\n });\n }\n if (authSecret.length < 16) {\n throw new WebPushError(\"Invalid subscription auth key\", 400, {\n length: authSecret.length,\n });\n }\n\n const asKeyPair = await crypto.subtle.generateKey({ name: \"ECDH\", namedCurve: \"P-256\" }, true, [\n \"deriveBits\",\n ]);\n const asPublicRaw = new Uint8Array(await crypto.subtle.exportKey(\"raw\", asKeyPair.publicKey));\n\n const uaKey = await crypto.subtle.importKey(\n \"raw\",\n toArrayBuffer(uaPublic),\n { name: \"ECDH\", namedCurve: \"P-256\" },\n false,\n [],\n );\n\n const ecdhSecret = new Uint8Array(\n await crypto.subtle.deriveBits({ name: \"ECDH\", public: uaKey }, asKeyPair.privateKey, 256),\n );\n\n const keyInfo = concatBytes(encodeUtf8(\"WebPush: info\\0\"), uaPublic, asPublicRaw);\n const ikm = await hkdf(authSecret, ecdhSecret, keyInfo, 32);\n\n const salt = crypto.getRandomValues(new Uint8Array(16));\n const cekInfo = encodeUtf8(\"Content-Encoding: aes128gcm\\0\");\n const nonceInfo = encodeUtf8(\"Content-Encoding: nonce\\0\");\n const cek = await hkdf(salt, ikm, cekInfo, 16);\n const nonce = await hkdf(salt, ikm, nonceInfo, 12);\n\n // RFC 8188 final-record padding delimiter\n const padded = concatBytes(plaintext, new Uint8Array([0x02]));\n\n const aesKey = await crypto.subtle.importKey(\"raw\", toArrayBuffer(cek), \"AES-GCM\", false, [\n \"encrypt\",\n ]);\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv: toArrayBuffer(nonce) },\n aesKey,\n toArrayBuffer(padded),\n ),\n );\n\n // salt || rs || idlen || keyid || ciphertext\n return concatBytes(\n salt,\n uint32Be(RECORD_SIZE),\n new Uint8Array([asPublicRaw.length]),\n asPublicRaw,\n ciphertext,\n );\n}\n\nasync function signVapidJwt(\n endpoint: string,\n subject: string,\n vapidPublicKey: string,\n vapidPrivateKey: string,\n): Promise<string> {\n const publicKey = decodeBase64Url(vapidPublicKey);\n const privateKey = decodeBase64Url(vapidPrivateKey);\n const jwk = ecKeyPairToJwk(publicKey, privateKey);\n\n const key = await crypto.subtle.importKey(\n \"jwk\",\n jwk,\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n false,\n [\"sign\"],\n );\n\n const origin = new URL(endpoint).origin;\n const header = { typ: \"JWT\", alg: \"ES256\" };\n const payload = {\n aud: origin,\n exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60,\n sub: subject,\n };\n\n const encodedHeader = encodeBase64Url(encodeUtf8(JSON.stringify(header)));\n const encodedPayload = encodeBase64Url(encodeUtf8(JSON.stringify(payload)));\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n\n const signature = new Uint8Array(\n await crypto.subtle.sign(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n key,\n toArrayBuffer(encodeUtf8(signingInput)),\n ),\n );\n\n return `${signingInput}.${encodeBase64Url(signature)}`;\n}\n\n/**\n * Web Push transport — VAPID auth + RFC 8291 payload encryption.\n *\n * Endpoint URLs are validated against an allowlist before fetch to mitigate SSRF.\n * Redirects are not followed.\n */\nexport class WebPushTransport implements PushTransport {\n readonly provider = \"webpush\";\n\n private readonly vapidPublicKey: string;\n private readonly vapidPrivateKey: string;\n private readonly subject: string;\n private readonly allowedEndpointHosts: readonly string[];\n\n /** Creates a Web Push transport with VAPID credentials. */\n constructor(config: WebPushConfig) {\n this.vapidPublicKey = config.vapidPublicKey;\n this.vapidPrivateKey = config.vapidPrivateKey;\n this.subject = config.subject;\n this.allowedEndpointHosts = config.allowedEndpointHosts ?? [];\n }\n\n /** Encrypts and POSTs a notification to the subscription endpoint. */\n async send(options: PushOptions): Promise<PushSendResult> {\n if (!isWebPushOptions(options)) {\n throw new WebPushError(\n \"WebPushTransport requires PushOptions.subscription; use FcmTransport for device tokens\",\n 400,\n { hint: \"fcm\" },\n );\n }\n\n try {\n assertSafePushEndpoint(options.subscription.endpoint, this.allowedEndpointHosts);\n } catch (error) {\n let endpointHost: string | undefined;\n try {\n endpointHost = new URL(options.subscription.endpoint).hostname;\n } catch {\n endpointHost = undefined;\n }\n throw new WebPushError(\n error instanceof Error ? error.message : \"Unsafe push subscription endpoint\",\n 400,\n { endpointHost },\n );\n }\n\n const notification = {\n title: options.title,\n body: options.body,\n ...(options.data !== undefined ? { data: options.data } : {}),\n ...(options.icon !== undefined ? { icon: options.icon } : {}),\n };\n const plaintext = encodeUtf8(JSON.stringify(notification));\n const encrypted = await encryptAes128Gcm(options.subscription, plaintext);\n\n if (encrypted.length > MAX_ENCRYPTED_BODY_BYTES) {\n throw new WebPushError(\n `Encrypted push payload exceeds ${MAX_ENCRYPTED_BODY_BYTES} bytes (RFC 8291)`,\n 413,\n { size: encrypted.length },\n );\n }\n\n const jwt = await signVapidJwt(\n options.subscription.endpoint,\n this.subject,\n this.vapidPublicKey,\n this.vapidPrivateKey,\n );\n\n const ttl = options.ttl ?? DEFAULT_TTL_SECONDS;\n const response = await fetch(options.subscription.endpoint, {\n method: \"POST\",\n redirect: \"manual\",\n headers: {\n Authorization: `vapid t=${jwt}, k=${this.vapidPublicKey}`,\n \"Content-Encoding\": \"aes128gcm\",\n TTL: String(ttl),\n \"Content-Type\": \"application/octet-stream\",\n },\n body: toArrayBuffer(encrypted),\n });\n\n // Opaque redirect responses (3xx) must not be followed — SSRF mitigation.\n if (response.status >= 300 && response.status < 400) {\n throw new WebPushError(\n `Push service returned redirect ${response.status}; redirects are not followed`,\n response.status,\n { location: response.headers.get(\"location\") },\n );\n }\n\n if (!response.ok) {\n const bodyText = await response.text().catch(() => \"\");\n throw new WebPushError(\n bodyText || `Web Push request failed with status ${response.status}`,\n response.status,\n bodyText || { status: response.status },\n );\n }\n\n return {\n messageId: options.messageId ?? crypto.randomUUID(),\n status: \"accepted\",\n response: String(response.status),\n provider: \"webpush\",\n };\n }\n\n /** Lightweight VAPID credential shape check. */\n async verify(): Promise<VerifyResult> {\n try {\n const pub = decodeBase64Url(this.vapidPublicKey);\n const priv = decodeBase64Url(this.vapidPrivateKey);\n const ok = pub.length === 65 && priv.length === 32 && this.subject.length > 0;\n return {\n ok,\n provider: \"webpush\",\n message: ok ? \"VAPID credentials present\" : \"Invalid VAPID key lengths\",\n };\n } catch (error) {\n return {\n ok: false,\n provider: \"webpush\",\n message: error instanceof Error ? error.message : \"Invalid VAPID credentials\",\n };\n }\n }\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "43FC53C9208BBE5564756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "DFA1A46FC61CEB2264756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -2,7 +2,7 @@
2
2
  * @module
3
3
  * WeightedFallbackTransport — routes each send through a weighted-random
4
4
  * primary provider, then fails over through the remaining providers on error.
5
- * Useful for gradual traffic shifting, migrations, and A/B testing providers.
5
+ * Works with email, SMS, WhatsApp, and push transports.
6
6
  *
7
7
  * @example
8
8
  * ```ts
@@ -19,20 +19,22 @@
19
19
  * );
20
20
  * ```
21
21
  */
22
- import type { MailOptions, SendResult, Transport, VerifyResult } from "../core/types.js";
22
+ import type { DecoratedTransport, FallbackAugmentedResult } from "../core/decorated-transport.js";
23
+ import type { MailOptions, SendResult, VerifyResult } from "../core/types.js";
23
24
  import { type FallbackOptions, FallbackTransport, type FallbackVerifyAllResult } from "./fallback.js";
24
25
  /** A transport entry with a relative routing weight. */
25
- export interface WeightedTransportEntry {
26
+ export interface WeightedTransportEntry<TOptions = MailOptions, TResult = SendResult> {
26
27
  /** Transport instance. */
27
- transport: Transport;
28
+ transport: DecoratedTransport<TOptions, TResult>;
28
29
  /** Relative weight (e.g. 80 vs 20 → ~80% of sends try this provider first). */
29
30
  weight: number;
30
31
  }
31
32
  type MailerOnFallback = Parameters<FallbackTransport["setMailerOnFallback"]>[0];
32
33
  /**
33
34
  * Weighted provider routing with failover on error.
35
+ * Defaults preserve email {@link Transport} compatibility.
34
36
  */
35
- export declare class WeightedFallbackTransport implements Transport {
37
+ export declare class WeightedFallbackTransport<TOptions = MailOptions, TResult = SendResult> implements DecoratedTransport<TOptions, FallbackAugmentedResult<TResult>> {
36
38
  readonly provider = "weighted-fallback";
37
39
  private readonly entries;
38
40
  private readonly fallbackOptions;
@@ -43,18 +45,18 @@ export declare class WeightedFallbackTransport implements Transport {
43
45
  * Creates a weighted fallback transport.
44
46
  * @throws {Error} When entries is empty or total weight is zero.
45
47
  */
46
- constructor(entries: WeightedTransportEntry[], options?: FallbackOptions & {
48
+ constructor(entries: WeightedTransportEntry<TOptions, TResult>[], options?: FallbackOptions & {
47
49
  random?: () => number;
48
50
  });
49
51
  /**
50
- * Register a per-send onFallback callback from mailer lifecycle hooks.
51
- * @internal Used by {@link MailerImpl}; cleared after each send.
52
+ * Register a per-send onFallback callback from sender lifecycle hooks.
53
+ * @internal Cleared after each send.
52
54
  */
53
55
  setMailerOnFallback(callback: MailerOnFallback | undefined): void;
54
56
  private buildOrder;
55
57
  private pickWeightedIndex;
56
58
  /** Sends via a weighted-random primary provider, failing over on error. */
57
- send(message: MailOptions): Promise<SendResult>;
59
+ send(message: TOptions): Promise<FallbackAugmentedResult<TResult>>;
58
60
  /** Returns the first healthy provider in the chain. */
59
61
  verify(): Promise<VerifyResult>;
60
62
  /** Verifies every weighted provider in definition order. */
@@ -1,3 +1,3 @@
1
- import{k as E}from"../chunk-0gqy32pe.js";import"../chunk-gp4fwrs8.js";import"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class J{provider="weighted-fallback";entries;fallbackOptions;random;inner;mailerOnFallback;constructor(h,z){if(h.length===0)throw Error("WeightedFallbackTransport requires at least one entry");if(h.reduce((D,H)=>D+H.weight,0)<=0)throw Error("WeightedFallbackTransport requires positive total weight");this.entries=h,this.random=z?.random??Math.random;let q=new Map,{random:B,...G}=z??{};this.fallbackOptions={...G,cooldownState:q},this.inner=new E(h.map((D)=>D.transport),this.fallbackOptions)}setMailerOnFallback(h){this.mailerOnFallback=h}buildOrder(){let h=this.pickWeightedIndex(),z=[this.entries[h]?.transport],A=this.entries.map((q,B)=>({entry:q,index:B})).filter(({index:q})=>q!==h).sort((q,B)=>B.entry.weight-q.entry.weight);for(let{entry:q}of A)z.push(q.transport);return z}pickWeightedIndex(){let h=this.entries.reduce((A,q)=>A+q.weight,0),z=this.random()*h;for(let A=0;A<this.entries.length;A++){let q=this.entries[A];if(z-=q.weight,z<=0)return A}return this.entries.length-1}async send(h){let z=new E(this.buildOrder(),this.fallbackOptions);if(this.mailerOnFallback!==void 0)z.setMailerOnFallback(this.mailerOnFallback);return z.send(h)}async verify(){return this.inner.verify()}async verifyAll(){return this.inner.verifyAll()}async close(){await this.inner.close()}}export{J as WeightedFallbackTransport};
1
+ import{m as E}from"../chunk-q2mahnb7.js";import"../chunk-cs1tvehn.js";import"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";class J{provider="weighted-fallback";entries;fallbackOptions;random;inner;mailerOnFallback;constructor(h,z){if(h.length===0)throw Error("WeightedFallbackTransport requires at least one entry");if(h.reduce((D,H)=>D+H.weight,0)<=0)throw Error("WeightedFallbackTransport requires positive total weight");this.entries=h,this.random=z?.random??Math.random;let q=new Map,{random:B,...G}=z??{};this.fallbackOptions={...G,cooldownState:q},this.inner=new E(h.map((D)=>D.transport),this.fallbackOptions)}setMailerOnFallback(h){this.mailerOnFallback=h}buildOrder(){let h=this.pickWeightedIndex(),z=[this.entries[h]?.transport],A=this.entries.map((q,B)=>({entry:q,index:B})).filter(({index:q})=>q!==h).sort((q,B)=>B.entry.weight-q.entry.weight);for(let{entry:q}of A)z.push(q.transport);return z}pickWeightedIndex(){let h=this.entries.reduce((A,q)=>A+q.weight,0),z=this.random()*h;for(let A=0;A<this.entries.length;A++){let q=this.entries[A];if(z-=q.weight,z<=0)return A}return this.entries.length-1}async send(h){let z=new E(this.buildOrder(),this.fallbackOptions);if(this.mailerOnFallback!==void 0)z.setMailerOnFallback(this.mailerOnFallback);return z.send(h)}async verify(){return this.inner.verify()}async verifyAll(){return this.inner.verifyAll()}async close(){await this.inner.close()}}export{J as WeightedFallbackTransport};
2
2
 
3
- //# debugId=1AE5329B4BB01DFC64756E2164756E21
3
+ //# debugId=462DBBA4FC2C14C364756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/transports/weighted-fallback.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @module\n * WeightedFallbackTransport — routes each send through a weighted-random\n * primary provider, then fails over through the remaining providers on error.\n * Useful for gradual traffic shifting, migrations, and A/B testing providers.\n *\n * @example\n * ```ts\n * import { WeightedFallbackTransport } from \"sently/transports/weighted-fallback\";\n * import { ResendTransport } from \"sently/transports/resend\";\n * import { SESTransport } from \"sently/transports/ses\";\n *\n * const transport = new WeightedFallbackTransport(\n * [\n * { transport: new ResendTransport({ apiKey }), weight: 80 },\n * { transport: new SESTransport({ accessKeyId, secretAccessKey }), weight: 20 },\n * ],\n * { cooldownMs: 300_000 },\n * );\n * ```\n */\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\nimport {\n type FallbackOptions,\n FallbackTransport,\n type FallbackVerifyAllResult,\n} from \"./fallback.js\";\n\n/** A transport entry with a relative routing weight. */\nexport interface WeightedTransportEntry {\n /** Transport instance. */\n transport: Transport;\n /** Relative weight (e.g. 80 vs 20 → ~80% of sends try this provider first). */\n weight: number;\n}\n\ntype MailerOnFallback = Parameters<FallbackTransport[\"setMailerOnFallback\"]>[0];\n\n/**\n * Weighted provider routing with failover on error.\n */\nexport class WeightedFallbackTransport implements Transport {\n readonly provider = \"weighted-fallback\";\n\n private readonly entries: WeightedTransportEntry[];\n private readonly fallbackOptions: FallbackOptions;\n private readonly random: () => number;\n private readonly inner: FallbackTransport;\n private mailerOnFallback: MailerOnFallback | undefined;\n\n /**\n * Creates a weighted fallback transport.\n * @throws {Error} When entries is empty or total weight is zero.\n */\n constructor(\n entries: WeightedTransportEntry[],\n options?: FallbackOptions & { random?: () => number },\n ) {\n if (entries.length === 0) {\n throw new Error(\"WeightedFallbackTransport requires at least one entry\");\n }\n\n const totalWeight = entries.reduce((sum, entry) => sum + entry.weight, 0);\n if (totalWeight <= 0) {\n throw new Error(\"WeightedFallbackTransport requires positive total weight\");\n }\n\n this.entries = entries;\n this.random = options?.random ?? Math.random;\n\n const cooldownState = new Map<string, number>();\n const { random: _random, ...rest } = options ?? {};\n this.fallbackOptions = { ...rest, cooldownState };\n\n this.inner = new FallbackTransport(\n entries.map((entry) => entry.transport),\n this.fallbackOptions,\n );\n }\n\n /**\n * Register a per-send onFallback callback from mailer lifecycle hooks.\n * @internal Used by {@link MailerImpl}; cleared after each send.\n */\n setMailerOnFallback(callback: MailerOnFallback | undefined): void {\n this.mailerOnFallback = callback;\n }\n\n private buildOrder(): Transport[] {\n const primaryIndex = this.pickWeightedIndex();\n const order: Transport[] = [this.entries[primaryIndex]?.transport as Transport];\n\n const remaining = this.entries\n .map((entry, index) => ({ entry, index }))\n .filter(({ index }) => index !== primaryIndex)\n .sort((a, b) => b.entry.weight - a.entry.weight);\n\n for (const { entry } of remaining) {\n order.push(entry.transport);\n }\n\n return order;\n }\n\n private pickWeightedIndex(): number {\n const total = this.entries.reduce((sum, entry) => sum + entry.weight, 0);\n let roll = this.random() * total;\n\n for (let i = 0; i < this.entries.length; i++) {\n const entry = this.entries[i] as WeightedTransportEntry;\n roll -= entry.weight;\n if (roll <= 0) {\n return i;\n }\n }\n\n return this.entries.length - 1;\n }\n\n /** Sends via a weighted-random primary provider, failing over on error. */\n async send(message: MailOptions): Promise<SendResult> {\n const transport = new FallbackTransport(this.buildOrder(), this.fallbackOptions);\n if (this.mailerOnFallback !== undefined) {\n transport.setMailerOnFallback(this.mailerOnFallback);\n }\n return transport.send(message);\n }\n\n /** Returns the first healthy provider in the chain. */\n async verify(): Promise<VerifyResult> {\n return this.inner.verify();\n }\n\n /** Verifies every weighted provider in definition order. */\n async verifyAll(): Promise<FallbackVerifyAllResult> {\n return this.inner.verifyAll();\n }\n\n /** Closes every underlying transport. */\n async close(): Promise<void> {\n await this.inner.close();\n }\n}\n"
5
+ "/**\n * @module\n * WeightedFallbackTransport — routes each send through a weighted-random\n * primary provider, then fails over through the remaining providers on error.\n * Works with email, SMS, WhatsApp, and push transports.\n *\n * @example\n * ```ts\n * import { WeightedFallbackTransport } from \"sently/transports/weighted-fallback\";\n * import { ResendTransport } from \"sently/transports/resend\";\n * import { SESTransport } from \"sently/transports/ses\";\n *\n * const transport = new WeightedFallbackTransport(\n * [\n * { transport: new ResendTransport({ apiKey }), weight: 80 },\n * { transport: new SESTransport({ accessKeyId, secretAccessKey }), weight: 20 },\n * ],\n * { cooldownMs: 300_000 },\n * );\n * ```\n */\nimport type { DecoratedTransport, FallbackAugmentedResult } from \"../core/decorated-transport.js\";\nimport type { MailOptions, SendResult, VerifyResult } from \"../core/types.js\";\nimport {\n type FallbackOptions,\n FallbackTransport,\n type FallbackVerifyAllResult,\n} from \"./fallback.js\";\n\n/** A transport entry with a relative routing weight. */\nexport interface WeightedTransportEntry<TOptions = MailOptions, TResult = SendResult> {\n /** Transport instance. */\n transport: DecoratedTransport<TOptions, TResult>;\n /** Relative weight (e.g. 80 vs 20 → ~80% of sends try this provider first). */\n weight: number;\n}\n\ntype MailerOnFallback = Parameters<FallbackTransport[\"setMailerOnFallback\"]>[0];\n\n/**\n * Weighted provider routing with failover on error.\n * Defaults preserve email {@link Transport} compatibility.\n */\nexport class WeightedFallbackTransport<TOptions = MailOptions, TResult = SendResult>\n implements DecoratedTransport<TOptions, FallbackAugmentedResult<TResult>>\n{\n readonly provider = \"weighted-fallback\";\n\n private readonly entries: WeightedTransportEntry<TOptions, TResult>[];\n private readonly fallbackOptions: FallbackOptions;\n private readonly random: () => number;\n private readonly inner: FallbackTransport<TOptions, TResult>;\n private mailerOnFallback: MailerOnFallback | undefined;\n\n /**\n * Creates a weighted fallback transport.\n * @throws {Error} When entries is empty or total weight is zero.\n */\n constructor(\n entries: WeightedTransportEntry<TOptions, TResult>[],\n options?: FallbackOptions & { random?: () => number },\n ) {\n if (entries.length === 0) {\n throw new Error(\"WeightedFallbackTransport requires at least one entry\");\n }\n\n const totalWeight = entries.reduce((sum, entry) => sum + entry.weight, 0);\n if (totalWeight <= 0) {\n throw new Error(\"WeightedFallbackTransport requires positive total weight\");\n }\n\n this.entries = entries;\n this.random = options?.random ?? Math.random;\n\n const cooldownState = new Map<string, number>();\n const { random: _random, ...rest } = options ?? {};\n this.fallbackOptions = { ...rest, cooldownState };\n\n this.inner = new FallbackTransport(\n entries.map((entry) => entry.transport),\n this.fallbackOptions,\n );\n }\n\n /**\n * Register a per-send onFallback callback from sender lifecycle hooks.\n * @internal Cleared after each send.\n */\n setMailerOnFallback(callback: MailerOnFallback | undefined): void {\n this.mailerOnFallback = callback;\n }\n\n private buildOrder(): DecoratedTransport<TOptions, TResult>[] {\n const primaryIndex = this.pickWeightedIndex();\n const order: DecoratedTransport<TOptions, TResult>[] = [\n this.entries[primaryIndex]?.transport as DecoratedTransport<TOptions, TResult>,\n ];\n\n const remaining = this.entries\n .map((entry, index) => ({ entry, index }))\n .filter(({ index }) => index !== primaryIndex)\n .sort((a, b) => b.entry.weight - a.entry.weight);\n\n for (const { entry } of remaining) {\n order.push(entry.transport);\n }\n\n return order;\n }\n\n private pickWeightedIndex(): number {\n const total = this.entries.reduce((sum, entry) => sum + entry.weight, 0);\n let roll = this.random() * total;\n\n for (let i = 0; i < this.entries.length; i++) {\n const entry = this.entries[i] as WeightedTransportEntry<TOptions, TResult>;\n roll -= entry.weight;\n if (roll <= 0) {\n return i;\n }\n }\n\n return this.entries.length - 1;\n }\n\n /** Sends via a weighted-random primary provider, failing over on error. */\n async send(message: TOptions): Promise<FallbackAugmentedResult<TResult>> {\n const transport = new FallbackTransport(this.buildOrder(), this.fallbackOptions);\n if (this.mailerOnFallback !== undefined) {\n transport.setMailerOnFallback(this.mailerOnFallback);\n }\n return transport.send(message);\n }\n\n /** Returns the first healthy provider in the chain. */\n async verify(): Promise<VerifyResult> {\n return this.inner.verify();\n }\n\n /** Verifies every weighted provider in definition order. */\n async verifyAll(): Promise<FallbackVerifyAllResult> {\n return this.inner.verifyAll();\n }\n\n /** Closes every underlying transport. */\n async close(): Promise<void> {\n await this.inner.close();\n }\n}\n"
6
6
  ],
7
- "mappings": "+IAyCO,CAAM,JAA+C,JACjD,SAAW,oBAEH,QACA,gBACA,OACA,MACT,iBAMR,WAAW,CACT,EACA,EACA,CACA,GAAI,EAAQ,SAAW,EACrB,MAAU,MAAM,uDAAuD,EAIzE,GADoB,EAAQ,OAAO,CAAC,EAAK,IAAU,EAAM,EAAM,OAAQ,CAAC,GACrD,EACjB,MAAU,MAAM,0DAA0D,EAG5E,KAAK,QAAU,EACf,KAAK,OAAS,GAAS,QAAU,KAAK,OAEtC,IAAM,EAAgB,IAAI,KAClB,OAAQ,KAAY,GAAS,GAAW,CAAC,EACjD,KAAK,gBAAkB,IAAK,EAAM,eAAc,EAEhD,KAAK,MAAQ,IAAI,EACf,EAAQ,IAAI,CAAC,IAAU,EAAM,SAAS,EACtC,KAAK,eACP,EAOF,mBAAmB,CAAC,EAA8C,CAChE,KAAK,iBAAmB,EAGlB,UAAU,EAAgB,CAChC,IAAM,EAAe,KAAK,kBAAkB,EACtC,EAAqB,CAAC,KAAK,QAAQ,IAAe,SAAsB,EAExE,EAAY,KAAK,QACpB,IAAI,CAAC,EAAO,KAAW,CAAE,QAAO,OAAM,EAAE,EACxC,OAAO,EAAG,WAAY,IAAU,CAAY,EAC5C,KAAK,CAAC,EAAG,IAAM,EAAE,MAAM,OAAS,EAAE,MAAM,MAAM,EAEjD,QAAa,WAAW,EACtB,EAAM,KAAK,EAAM,SAAS,EAG5B,OAAO,EAGD,iBAAiB,EAAW,CAClC,IAAM,EAAQ,KAAK,QAAQ,OAAO,CAAC,EAAK,IAAU,EAAM,EAAM,OAAQ,CAAC,EACnE,EAAO,KAAK,OAAO,EAAI,EAE3B,QAAS,EAAI,EAAG,EAAI,KAAK,QAAQ,OAAQ,IAAK,CAC5C,IAAM,EAAQ,KAAK,QAAQ,GAE3B,GADA,GAAQ,EAAM,OACV,GAAQ,EACV,OAAO,EAIX,OAAO,KAAK,QAAQ,OAAS,OAIzB,KAAI,CAAC,EAA2C,CACpD,IAAM,EAAY,IAAI,EAAkB,KAAK,WAAW,EAAG,KAAK,eAAe,EAC/E,GAAI,KAAK,mBAAqB,OAC5B,EAAU,oBAAoB,KAAK,gBAAgB,EAErD,OAAO,EAAU,KAAK,CAAO,OAIzB,OAAM,EAA0B,CACpC,OAAO,KAAK,MAAM,OAAO,OAIrB,UAAS,EAAqC,CAClD,OAAO,KAAK,MAAM,UAAU,OAIxB,MAAK,EAAkB,CAC3B,MAAM,KAAK,MAAM,MAAM,EAE3B",
8
- "debugId": "1AE5329B4BB01DFC64756E2164756E21",
7
+ "mappings": "+IA2CO,CAAM,JAEb,JACW,SAAW,oBAEH,QACA,gBACA,OACA,MACT,iBAMR,WAAW,CACT,EACA,EACA,CACA,GAAI,EAAQ,SAAW,EACrB,MAAU,MAAM,uDAAuD,EAIzE,GADoB,EAAQ,OAAO,CAAC,EAAK,IAAU,EAAM,EAAM,OAAQ,CAAC,GACrD,EACjB,MAAU,MAAM,0DAA0D,EAG5E,KAAK,QAAU,EACf,KAAK,OAAS,GAAS,QAAU,KAAK,OAEtC,IAAM,EAAgB,IAAI,KAClB,OAAQ,KAAY,GAAS,GAAW,CAAC,EACjD,KAAK,gBAAkB,IAAK,EAAM,eAAc,EAEhD,KAAK,MAAQ,IAAI,EACf,EAAQ,IAAI,CAAC,IAAU,EAAM,SAAS,EACtC,KAAK,eACP,EAOF,mBAAmB,CAAC,EAA8C,CAChE,KAAK,iBAAmB,EAGlB,UAAU,EAA4C,CAC5D,IAAM,EAAe,KAAK,kBAAkB,EACtC,EAAiD,CACrD,KAAK,QAAQ,IAAe,SAC9B,EAEM,EAAY,KAAK,QACpB,IAAI,CAAC,EAAO,KAAW,CAAE,QAAO,OAAM,EAAE,EACxC,OAAO,EAAG,WAAY,IAAU,CAAY,EAC5C,KAAK,CAAC,EAAG,IAAM,EAAE,MAAM,OAAS,EAAE,MAAM,MAAM,EAEjD,QAAa,WAAW,EACtB,EAAM,KAAK,EAAM,SAAS,EAG5B,OAAO,EAGD,iBAAiB,EAAW,CAClC,IAAM,EAAQ,KAAK,QAAQ,OAAO,CAAC,EAAK,IAAU,EAAM,EAAM,OAAQ,CAAC,EACnE,EAAO,KAAK,OAAO,EAAI,EAE3B,QAAS,EAAI,EAAG,EAAI,KAAK,QAAQ,OAAQ,IAAK,CAC5C,IAAM,EAAQ,KAAK,QAAQ,GAE3B,GADA,GAAQ,EAAM,OACV,GAAQ,EACV,OAAO,EAIX,OAAO,KAAK,QAAQ,OAAS,OAIzB,KAAI,CAAC,EAA8D,CACvE,IAAM,EAAY,IAAI,EAAkB,KAAK,WAAW,EAAG,KAAK,eAAe,EAC/E,GAAI,KAAK,mBAAqB,OAC5B,EAAU,oBAAoB,KAAK,gBAAgB,EAErD,OAAO,EAAU,KAAK,CAAO,OAIzB,OAAM,EAA0B,CACpC,OAAO,KAAK,MAAM,OAAO,OAIrB,UAAS,EAAqC,CAClD,OAAO,KAAK,MAAM,UAAU,OAIxB,MAAK,EAAkB,CAC3B,MAAM,KAAK,MAAM,MAAM,EAE3B",
8
+ "debugId": "462DBBA4FC2C14C364756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,3 +1,3 @@
1
- import{ca as x,da as z}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";var G="v26.0";class B extends x{statusCode;apiError;constructor(j,k,q){super(j,z(k),{statusCode:k,provider:"whatsapp-cloud",cause:q});this.statusCode=k;this.apiError=q;this.name="WhatsAppCloudError"}}function J(j){return"template"in j}class K{provider="whatsapp-cloud";accessToken;phoneNumberId;apiVersion;constructor(j){this.accessToken=j.accessToken,this.phoneNumberId=j.phoneNumberId,this.apiVersion=j.apiVersion??G}async send(j){let k=`https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`,q=J(j)?{messaging_product:"whatsapp",to:j.to,type:"template",template:{name:j.template.name,language:{code:j.template.language},...j.template.components?{components:j.template.components}:{}}}:{messaging_product:"whatsapp",to:j.to,type:"text",text:{body:j.text}},v=await fetch(k,{method:"POST",headers:{Authorization:`Bearer ${this.accessToken}`,"Content-Type":"application/json"},body:JSON.stringify(q)}),w=await v.json();if(!v.ok)throw new B(w.error?.message??"WhatsApp Cloud API error",v.status,w);return{messageId:w.messages?.[0]?.id??j.messageId??"",to:j.to,status:"accepted",response:"Message sent",provider:"whatsapp-cloud"}}async verify(){return{ok:Boolean(this.accessToken&&this.phoneNumberId),provider:"whatsapp-cloud",message:this.accessToken&&this.phoneNumberId?"Credentials present":"Missing credentials"}}}export{K as WhatsAppCloudTransport,B as WhatsAppCloudError,G as WHATSAPP_CLOUD_DEFAULT_API_VERSION};
1
+ import{ja as x,ka as z}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";var G="v26.0";class B extends x{statusCode;apiError;constructor(j,k,q){super(j,z(k),{statusCode:k,provider:"whatsapp-cloud",cause:q});this.statusCode=k;this.apiError=q;this.name="WhatsAppCloudError"}}function J(j){return"template"in j}class K{provider="whatsapp-cloud";accessToken;phoneNumberId;apiVersion;constructor(j){this.accessToken=j.accessToken,this.phoneNumberId=j.phoneNumberId,this.apiVersion=j.apiVersion??G}async send(j){let k=`https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`,q=J(j)?{messaging_product:"whatsapp",to:j.to,type:"template",template:{name:j.template.name,language:{code:j.template.language},...j.template.components?{components:j.template.components}:{}}}:{messaging_product:"whatsapp",to:j.to,type:"text",text:{body:j.text}},v=await fetch(k,{method:"POST",headers:{Authorization:`Bearer ${this.accessToken}`,"Content-Type":"application/json"},body:JSON.stringify(q)}),w=await v.json();if(!v.ok)throw new B(w.error?.message??"WhatsApp Cloud API error",v.status,w);return{messageId:w.messages?.[0]?.id??j.messageId??"",to:j.to,status:"accepted",response:"Message sent",provider:"whatsapp-cloud"}}async verify(){return{ok:Boolean(this.accessToken&&this.phoneNumberId),provider:"whatsapp-cloud",message:this.accessToken&&this.phoneNumberId?"Credentials present":"Missing credentials"}}}export{K as WhatsAppCloudTransport,B as WhatsAppCloudError,G as WHATSAPP_CLOUD_DEFAULT_API_VERSION};
2
2
 
3
- //# debugId=D51060C2D4F4831264756E2164756E21
3
+ //# debugId=B754BE00883090E464756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "/**\n * @module\n * Meta WhatsApp Cloud API transport.\n *\n * Sently-first: wire into {@link createWhatsAppSender}. Other WhatsApp providers\n * (e.g. `TaqnyatWhatsAppTransport`) are separate transports with the same contract.\n *\n * @example\n * ```ts\n * import { createWhatsAppSender } from \"sently/whatsapp\";\n * import { WhatsAppCloudTransport } from \"sently/transports/whatsapp-cloud\";\n *\n * const wa = createWhatsAppSender({\n * transport: new WhatsAppCloudTransport({\n * accessToken: process.env.WHATSAPP_TOKEN!,\n * phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,\n * }),\n * });\n * ```\n */\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type { VerifyResult } from \"../core/types.js\";\nimport type {\n WhatsAppOptions,\n WhatsAppSendResult,\n WhatsAppTransport,\n} from \"../core/whatsapp-types.js\";\n\n/** Default Graph API version (current as of 2026-08). */\nexport const WHATSAPP_CLOUD_DEFAULT_API_VERSION = \"v26.0\";\n\n/** WhatsApp Cloud API configuration. */\nexport interface WhatsAppCloudConfig {\n /** Meta Graph API access token. */\n accessToken: string;\n /** WhatsApp Business phone number ID. */\n phoneNumberId: string;\n /** Graph API version (e.g. `\"v26.0\"`). Default: {@link WHATSAPP_CLOUD_DEFAULT_API_VERSION}. */\n apiVersion?: string;\n}\n\n/** Error thrown when the WhatsApp Cloud API returns a non-success response. */\nexport class WhatsAppCloudError extends SentlyError {\n /** Creates a WhatsApp Cloud API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"whatsapp-cloud\",\n cause: apiError,\n });\n this.name = \"WhatsAppCloudError\";\n }\n}\n\nfunction isTemplateMessage(\n options: WhatsAppOptions,\n): options is Extract<WhatsAppOptions, { template: unknown }> {\n return \"template\" in options;\n}\n\n/**\n * WhatsApp Cloud API transport (JSON Bearer).\n */\nexport class WhatsAppCloudTransport implements WhatsAppTransport {\n readonly provider = \"whatsapp-cloud\";\n\n private readonly accessToken: string;\n private readonly phoneNumberId: string;\n private readonly apiVersion: string;\n\n /** Creates a WhatsApp Cloud API transport. */\n constructor(config: WhatsAppCloudConfig) {\n this.accessToken = config.accessToken;\n this.phoneNumberId = config.phoneNumberId;\n this.apiVersion = config.apiVersion ?? WHATSAPP_CLOUD_DEFAULT_API_VERSION;\n }\n\n /** Sends a WhatsApp template or text message. */\n async send(options: WhatsAppOptions): Promise<WhatsAppSendResult> {\n const url = `https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`;\n\n const body = isTemplateMessage(options)\n ? {\n messaging_product: \"whatsapp\",\n to: options.to,\n type: \"template\",\n template: {\n name: options.template.name,\n language: { code: options.template.language },\n ...(options.template.components ? { components: options.template.components } : {}),\n },\n }\n : {\n messaging_product: \"whatsapp\",\n to: options.to,\n type: \"text\",\n text: { body: options.text },\n };\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const payload = (await response.json()) as {\n messages?: Array<{ id?: string }>;\n error?: {\n message?: string;\n code?: number;\n error_subcode?: number;\n fbtrace_id?: string;\n };\n };\n\n if (!response.ok) {\n throw new WhatsAppCloudError(\n payload.error?.message ?? \"WhatsApp Cloud API error\",\n response.status,\n payload,\n );\n }\n\n const id = payload.messages?.[0]?.id ?? options.messageId ?? \"\";\n\n return {\n messageId: id,\n to: options.to,\n status: \"accepted\",\n response: \"Message sent\",\n provider: \"whatsapp-cloud\",\n };\n }\n\n /** Lightweight credential check. */\n async verify(): Promise<VerifyResult> {\n return {\n ok: Boolean(this.accessToken && this.phoneNumberId),\n provider: \"whatsapp-cloud\",\n message:\n this.accessToken && this.phoneNumberId ? \"Credentials present\" : \"Missing credentials\",\n };\n }\n}\n"
6
6
  ],
7
7
  "mappings": "oFA6BO,DAAM,EAAqC,QAa3C,MAAM,UAA2B,CAAY,CAIhC,WACA,SAHlB,WAAW,CACT,EACgB,EACA,EAChB,CACA,MAAM,EAAS,EAAuB,CAAU,EAAG,CACjD,aACA,SAAU,iBACV,MAAO,CACT,CAAC,EAPe,kBACA,gBAOhB,KAAK,KAAO,qBAEhB,CAEA,SAAS,CAAiB,CACxB,EAC4D,CAC5D,MAAO,aAAc,EAMhB,MAAM,CAAoD,CACtD,SAAW,iBAEH,YACA,cACA,WAGjB,WAAW,CAAC,EAA6B,CACvC,KAAK,YAAc,EAAO,YAC1B,KAAK,cAAgB,EAAO,cAC5B,KAAK,WAAa,EAAO,YAAc,OAInC,KAAI,CAAC,EAAuD,CAChE,IAAM,EAAM,8BAA8B,KAAK,cAAc,KAAK,yBAE5D,EAAO,EAAkB,CAAO,EAClC,CACE,kBAAmB,WACnB,GAAI,EAAQ,GACZ,KAAM,WACN,SAAU,CACR,KAAM,EAAQ,SAAS,KACvB,SAAU,CAAE,KAAM,EAAQ,SAAS,QAAS,KACxC,EAAQ,SAAS,WAAa,CAAE,WAAY,EAAQ,SAAS,UAAW,EAAI,CAAC,CACnF,CACF,EACA,CACE,kBAAmB,WACnB,GAAI,EAAQ,GACZ,KAAM,OACN,KAAM,CAAE,KAAM,EAAQ,IAAK,CAC7B,EAEE,EAAW,MAAM,MAAM,EAAK,CAChC,OAAQ,OACR,QAAS,CACP,cAAe,UAAU,KAAK,cAC9B,eAAgB,kBAClB,EACA,KAAM,KAAK,UAAU,CAAI,CAC3B,CAAC,EAEK,EAAW,MAAM,EAAS,KAAK,EAUrC,GAAI,CAAC,EAAS,GACZ,MAAM,IAAI,EACR,EAAQ,OAAO,SAAW,2BAC1B,EAAS,OACT,CACF,EAKF,MAAO,CACL,UAHS,EAAQ,WAAW,IAAI,IAAM,EAAQ,WAAa,GAI3D,GAAI,EAAQ,GACZ,OAAQ,WACR,SAAU,eACV,SAAU,gBACZ,OAII,OAAM,EAA0B,CACpC,MAAO,CACL,GAAI,QAAQ,KAAK,aAAe,KAAK,aAAa,EAClD,SAAU,iBACV,QACE,KAAK,aAAe,KAAK,cAAgB,sBAAwB,qBACrE,EAEJ",
8
- "debugId": "D51060C2D4F4831264756E2164756E21",
8
+ "debugId": "B754BE00883090E464756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,3 +1,3 @@
1
- import{d as F,e as u,f as G,g as H}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var K={delivered:"delivered",hard_bounce:"bounced",hardBounce:"bounced",soft_bounce:"bounced",soft_bounced:"bounced",softBounce:"bounced",blocked:"bounced",invalid:"bounced",invalid_email:"bounced",deferred:"deferred",spam:"complained",opened:"opened",unique_opened:"opened",uniqueOpened:"opened",click:"clicked",clicked:"clicked",unique_click:"clicked",uniqueClicked:"clicked",sent:"unknown",request:"unknown",unsubscribe:"complained",unsubscribed:"complained"};function L(f){return u(f.ts_event)??u(f.ts)??u(f.date_event)??u(f.date)??u(f.ts_epoch!==void 0?Number(f.ts_epoch):void 0)}function Q(f){if(typeof f["message-id"]==="string")return f["message-id"];if(typeof f.messageId==="string")return f.messageId;return}function U(f){return G(f).flatMap((x)=>{if(typeof x!=="object"||x===null)return[];let q=x,J=typeof q.event==="string"?q.event:"",C=L(q),D=Q(q);return[H({provider:"brevo",type:F(J,K),...D!==void 0?{messageId:D}:{},...typeof q.email==="string"?{recipient:q.email}:{},...C!==void 0?{timestamp:C}:{},raw:x})]})}export{U as parse};
1
+ import{f as F,g as u,h as G,i as H}from"../chunk-sbgt4165.js";import"../chunk-th4cwrpb.js";var K={delivered:"delivered",hard_bounce:"bounced",hardBounce:"bounced",soft_bounce:"bounced",soft_bounced:"bounced",softBounce:"bounced",blocked:"bounced",invalid:"bounced",invalid_email:"bounced",deferred:"deferred",spam:"complained",opened:"opened",unique_opened:"opened",uniqueOpened:"opened",click:"clicked",clicked:"clicked",unique_click:"clicked",uniqueClicked:"clicked",sent:"unknown",request:"unknown",unsubscribe:"complained",unsubscribed:"complained"};function L(f){return u(f.ts_event)??u(f.ts)??u(f.date_event)??u(f.date)??u(f.ts_epoch!==void 0?Number(f.ts_epoch):void 0)}function Q(f){if(typeof f["message-id"]==="string")return f["message-id"];if(typeof f.messageId==="string")return f.messageId;return}function U(f){return G(f).flatMap((x)=>{if(typeof x!=="object"||x===null)return[];let q=x,J=typeof q.event==="string"?q.event:"",C=L(q),D=Q(q);return[H({provider:"brevo",type:F(J,K),...D!==void 0?{messageId:D}:{},...typeof q.email==="string"?{recipient:q.email}:{},...C!==void 0?{timestamp:C}:{},raw:x})]})}export{U as parse};
2
2
 
3
- //# debugId=916BC5D0AADEB5C764756E2164756E21
3
+ //# debugId=F6BBB0F16174F70764756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "import type { EmailEvent } from \"./types.js\";\nimport { asArray, emailEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst BREVO_EVENT_MAP: Record<string, EmailEvent[\"type\"]> = {\n delivered: \"delivered\",\n hard_bounce: \"bounced\",\n hardBounce: \"bounced\",\n soft_bounce: \"bounced\",\n soft_bounced: \"bounced\",\n softBounce: \"bounced\",\n blocked: \"bounced\",\n invalid: \"bounced\",\n invalid_email: \"bounced\",\n deferred: \"deferred\",\n spam: \"complained\",\n opened: \"opened\",\n unique_opened: \"opened\",\n uniqueOpened: \"opened\",\n click: \"clicked\",\n clicked: \"clicked\",\n unique_click: \"clicked\",\n uniqueClicked: \"clicked\",\n sent: \"unknown\",\n request: \"unknown\",\n unsubscribe: \"complained\",\n unsubscribed: \"complained\",\n};\n\nfunction resolveBrevoTimestamp(record: Record<string, unknown>): Date | undefined {\n return (\n parseTimestamp(record.ts_event) ??\n parseTimestamp(record.ts) ??\n parseTimestamp(record.date_event) ??\n parseTimestamp(record.date) ??\n parseTimestamp(record.ts_epoch !== undefined ? Number(record.ts_epoch) : undefined)\n );\n}\n\nfunction resolveBrevoMessageId(record: Record<string, unknown>): string | undefined {\n if (typeof record[\"message-id\"] === \"string\") {\n return record[\"message-id\"];\n }\n if (typeof record.messageId === \"string\") {\n return record.messageId;\n }\n return undefined;\n}\n\n/** Parse a Brevo webhook payload into normalized {@link EmailEvent} records. */\nexport function parse(payload: unknown): EmailEvent[] {\n return asArray(payload).flatMap((item) => {\n if (typeof item !== \"object\" || item === null) {\n return [];\n }\n\n const record = item as Record<string, unknown>;\n const event = typeof record.event === \"string\" ? record.event : \"\";\n const timestamp = resolveBrevoTimestamp(record);\n const messageId = resolveBrevoMessageId(record);\n\n return [\n emailEvent({\n provider: \"brevo\",\n type: mapEventType(event, BREVO_EVENT_MAP),\n ...(messageId !== undefined ? { messageId } : {}),\n ...(typeof record.email === \"string\" ? { recipient: record.email } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n raw: item,\n }),\n ];\n });\n}\n"
6
6
  ],
7
7
  "mappings": "gGAGA,DAAM,EAAsD,CAC1D,UAAW,YACX,YAAa,UACb,WAAY,UACZ,YAAa,UACb,aAAc,UACd,WAAY,UACZ,QAAS,UACT,QAAS,UACT,cAAe,UACf,SAAU,WACV,KAAM,aACN,OAAQ,SACR,cAAe,SACf,aAAc,SACd,MAAO,UACP,QAAS,UACT,aAAc,UACd,cAAe,UACf,KAAM,UACN,QAAS,UACT,YAAa,aACb,aAAc,YAChB,EAEA,SAAS,CAAqB,CAAC,EAAmD,CAChF,OACE,EAAe,EAAO,QAAQ,GAC9B,EAAe,EAAO,EAAE,GACxB,EAAe,EAAO,UAAU,GAChC,EAAe,EAAO,IAAI,GAC1B,EAAe,EAAO,WAAa,OAAY,OAAO,EAAO,QAAQ,EAAI,MAAS,EAItF,SAAS,CAAqB,CAAC,EAAqD,CAClF,GAAI,OAAO,EAAO,gBAAkB,SAClC,OAAO,EAAO,cAEhB,GAAI,OAAO,EAAO,YAAc,SAC9B,OAAO,EAAO,UAEhB,OAIK,SAAS,CAAK,CAAC,EAAgC,CACpD,OAAO,EAAQ,CAAO,EAAE,QAAQ,CAAC,IAAS,CACxC,GAAI,OAAO,IAAS,UAAY,IAAS,KACvC,MAAO,CAAC,EAGV,IAAM,EAAS,EACT,EAAQ,OAAO,EAAO,QAAU,SAAW,EAAO,MAAQ,GAC1D,EAAY,EAAsB,CAAM,EACxC,EAAY,EAAsB,CAAM,EAE9C,MAAO,CACL,EAAW,CACT,SAAU,QACV,KAAM,EAAa,EAAO,CAAe,KACrC,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,KAC3C,OAAO,EAAO,QAAU,SAAW,CAAE,UAAW,EAAO,KAAM,EAAI,CAAC,KAClE,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,EAC/C,IAAK,CACP,CAAC,CACH,EACD",
8
- "debugId": "916BC5D0AADEB5C764756E2164756E21",
8
+ "debugId": "F6BBB0F16174F70764756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,3 +1,3 @@
1
- import{a as G,c as H}from"../chunk-pqp843mh.js";import{d as Y,e as Z,g as $}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var R={accepted:"unknown",delivered:"delivered",rejected:"bounced",complained:"complained",opened:"opened",clicked:"clicked",stored:"unknown",unsubscribed:"unknown"};function U(z){let J=typeof z.event==="string"?z.event:"";if(J==="failed")return z.severity==="temporary"?"deferred":"bounced";return Y(J,R)}function b(z){if(!z)return;let J=z.headers;if(typeof J==="object"&&J!==null){let C=J["message-id"];if(typeof C==="string")return C}return typeof z["message-id"]==="string"?z["message-id"]:void 0}function N(z){if(typeof z!=="object"||z===null)return[];let J=z,C=typeof J["event-data"]==="object"&&J["event-data"]!==null?J["event-data"]:J,O=typeof C.message==="object"&&C.message!==null?C.message:void 0,Q=b(O),W=Z(C.timestamp);return[$({provider:"mailgun",type:U(C),...Q!==void 0?{messageId:Q}:{},...typeof C.recipient==="string"?{recipient:C.recipient}:{},...W!==void 0?{timestamp:W}:{},raw:z})]}async function q(z,J,C,O){let Q=await crypto.subtle.importKey("raw",new TextEncoder().encode(O),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),W=new TextEncoder().encode(z+J),L=await crypto.subtle.sign("HMAC",Q,W),P=new Uint8Array(L),X=H(C);if(X===null)return!1;return G(P,X)}async function S(z,J){if(typeof z!=="object"||z===null)return!1;let C=z.signature;if(typeof C!=="object"||C===null)return!1;let O=C;if(typeof O.timestamp!=="string"||typeof O.token!=="string"||typeof O.signature!=="string")return!1;return q(O.timestamp,O.token,O.signature,J)}export{q as verifySignature,S as verifyPayload,N as parse};
1
+ import{a as G,c as H}from"../chunk-pqp843mh.js";import{f as Y,g as Z,i as $}from"../chunk-sbgt4165.js";import"../chunk-th4cwrpb.js";var R={accepted:"unknown",delivered:"delivered",rejected:"bounced",complained:"complained",opened:"opened",clicked:"clicked",stored:"unknown",unsubscribed:"unknown"};function U(z){let J=typeof z.event==="string"?z.event:"";if(J==="failed")return z.severity==="temporary"?"deferred":"bounced";return Y(J,R)}function b(z){if(!z)return;let J=z.headers;if(typeof J==="object"&&J!==null){let C=J["message-id"];if(typeof C==="string")return C}return typeof z["message-id"]==="string"?z["message-id"]:void 0}function N(z){if(typeof z!=="object"||z===null)return[];let J=z,C=typeof J["event-data"]==="object"&&J["event-data"]!==null?J["event-data"]:J,O=typeof C.message==="object"&&C.message!==null?C.message:void 0,Q=b(O),W=Z(C.timestamp);return[$({provider:"mailgun",type:U(C),...Q!==void 0?{messageId:Q}:{},...typeof C.recipient==="string"?{recipient:C.recipient}:{},...W!==void 0?{timestamp:W}:{},raw:z})]}async function q(z,J,C,O){let Q=await crypto.subtle.importKey("raw",new TextEncoder().encode(O),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),W=new TextEncoder().encode(z+J),L=await crypto.subtle.sign("HMAC",Q,W),P=new Uint8Array(L),X=H(C);if(X===null)return!1;return G(P,X)}async function S(z,J){if(typeof z!=="object"||z===null)return!1;let C=z.signature;if(typeof C!=="object"||C===null)return!1;let O=C;if(typeof O.timestamp!=="string"||typeof O.token!=="string"||typeof O.signature!=="string")return!1;return q(O.timestamp,O.token,O.signature,J)}export{q as verifySignature,S as verifyPayload,N as parse};
2
2
 
3
- //# debugId=7FB21CD9E29B4FE264756E2164756E21
3
+ //# debugId=57A0A59DBAEA409B64756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "import { decodeHexBytes, timingSafeEqual } from \"./timing-safe-equal.js\";\nimport type { EmailEvent } from \"./types.js\";\nimport { emailEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst MAILGUN_EVENT_MAP: Record<string, EmailEvent[\"type\"]> = {\n accepted: \"unknown\",\n delivered: \"delivered\",\n rejected: \"bounced\",\n complained: \"complained\",\n opened: \"opened\",\n clicked: \"clicked\",\n stored: \"unknown\",\n unsubscribed: \"unknown\",\n};\n\n/** Map a Mailgun event (and failed severity) to a normalized type. */\nfunction mapMailgunType(eventData: Record<string, unknown>): EmailEvent[\"type\"] {\n const event = typeof eventData.event === \"string\" ? eventData.event : \"\";\n if (event === \"failed\") {\n return eventData.severity === \"temporary\" ? \"deferred\" : \"bounced\";\n }\n return mapEventType(event, MAILGUN_EVENT_MAP);\n}\n\nfunction extractMailgunMessageId(message: Record<string, unknown> | undefined): string | undefined {\n if (!message) {\n return undefined;\n }\n const headers = message.headers;\n if (typeof headers === \"object\" && headers !== null) {\n const messageId = (headers as Record<string, unknown>)[\"message-id\"];\n if (typeof messageId === \"string\") {\n return messageId;\n }\n }\n return typeof message[\"message-id\"] === \"string\" ? message[\"message-id\"] : undefined;\n}\n\n/** Parse a Mailgun webhook payload into normalized {@link EmailEvent} records. */\nexport function parse(payload: unknown): EmailEvent[] {\n if (typeof payload !== \"object\" || payload === null) {\n return [];\n }\n\n const record = payload as Record<string, unknown>;\n const eventData =\n typeof record[\"event-data\"] === \"object\" && record[\"event-data\"] !== null\n ? (record[\"event-data\"] as Record<string, unknown>)\n : record;\n\n const message =\n typeof eventData.message === \"object\" && eventData.message !== null\n ? (eventData.message as Record<string, unknown>)\n : undefined;\n\n const messageId = extractMailgunMessageId(message);\n const timestamp = parseTimestamp(eventData.timestamp);\n\n return [\n emailEvent({\n provider: \"mailgun\",\n type: mapMailgunType(eventData),\n ...(messageId !== undefined ? { messageId } : {}),\n ...(typeof eventData.recipient === \"string\" ? { recipient: eventData.recipient } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n raw: payload,\n }),\n ];\n}\n\n/**\n * Verify a Mailgun webhook signature using HMAC-SHA256 (Web Crypto).\n * signingKey is the Mailgun webhook signing key.\n */\nexport async function verifySignature(\n timestamp: string,\n token: string,\n signature: string,\n signingKey: string,\n): Promise<boolean> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(signingKey),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n\n const data = new TextEncoder().encode(timestamp + token);\n const expected = await crypto.subtle.sign(\"HMAC\", key, data);\n const expectedBytes = new Uint8Array(expected);\n const providedBytes = decodeHexBytes(signature);\n\n if (providedBytes === null) {\n return false;\n }\n\n return timingSafeEqual(expectedBytes, providedBytes);\n}\n\n/**\n * Verify a Mailgun webhook payload using the nested `signature` object.\n * Returns false when the payload shape is invalid.\n */\nexport async function verifyPayload(payload: unknown, signingKey: string): Promise<boolean> {\n if (typeof payload !== \"object\" || payload === null) {\n return false;\n }\n\n const signatureField = (payload as Record<string, unknown>).signature;\n if (typeof signatureField !== \"object\" || signatureField === null) {\n return false;\n }\n\n const signature = signatureField as Record<string, unknown>;\n if (\n typeof signature.timestamp !== \"string\" ||\n typeof signature.token !== \"string\" ||\n typeof signature.signature !== \"string\"\n ) {\n return false;\n }\n\n return verifySignature(signature.timestamp, signature.token, signature.signature, signingKey);\n}\n"
6
6
  ],
7
7
  "mappings": "8IAIA,DAAM,HAAwD,CAC5D,SAAU,UACV,UAAW,YACX,SAAU,UACV,WAAY,aACZ,OAAQ,SACR,QAAS,UACT,OAAQ,UACR,aAAc,SAChB,EAGA,SAAS,CAAc,CAAC,EAAwD,CAC9E,IAAM,EAAQ,OAAO,EAAU,QAAU,SAAW,EAAU,MAAQ,GACtE,GAAI,IAAU,SACZ,OAAO,EAAU,WAAa,YAAc,WAAa,UAE3D,OAAO,EAAa,EAAO,CAAiB,EAG9C,SAAS,CAAuB,CAAC,EAAkE,CACjG,GAAI,CAAC,EACH,OAEF,IAAM,EAAU,EAAQ,QACxB,GAAI,OAAO,IAAY,UAAY,IAAY,KAAM,CACnD,IAAM,EAAa,EAAoC,cACvD,GAAI,OAAO,IAAc,SACvB,OAAO,EAGX,OAAO,OAAO,EAAQ,gBAAkB,SAAW,EAAQ,cAAgB,OAItE,SAAS,CAAK,CAAC,EAAgC,CACpD,GAAI,OAAO,IAAY,UAAY,IAAY,KAC7C,MAAO,CAAC,EAGV,IAAM,EAAS,EACT,EACJ,OAAO,EAAO,gBAAkB,UAAY,EAAO,gBAAkB,KAChE,EAAO,cACR,EAEA,EACJ,OAAO,EAAU,UAAY,UAAY,EAAU,UAAY,KAC1D,EAAU,QACX,OAEA,EAAY,EAAwB,CAAO,EAC3C,EAAY,EAAe,EAAU,SAAS,EAEpD,MAAO,CACL,EAAW,CACT,SAAU,UACV,KAAM,EAAe,CAAS,KAC1B,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,KAC3C,OAAO,EAAU,YAAc,SAAW,CAAE,UAAW,EAAU,SAAU,EAAI,CAAC,KAChF,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,EAC/C,IAAK,CACP,CAAC,CACH,EAOF,eAAsB,CAAe,CACnC,EACA,EACA,EACA,EACkB,CAClB,IAAM,EAAM,MAAM,OAAO,OAAO,UAC9B,MACA,IAAI,YAAY,EAAE,OAAO,CAAU,EACnC,CAAE,KAAM,OAAQ,KAAM,SAAU,EAChC,GACA,CAAC,MAAM,CACT,EAEM,EAAO,IAAI,YAAY,EAAE,OAAO,EAAY,CAAK,EACjD,EAAW,MAAM,OAAO,OAAO,KAAK,OAAQ,EAAK,CAAI,EACrD,EAAgB,IAAI,WAAW,CAAQ,EACvC,EAAgB,EAAe,CAAS,EAE9C,GAAI,IAAkB,KACpB,MAAO,GAGT,OAAO,EAAgB,EAAe,CAAa,EAOrD,eAAsB,CAAa,CAAC,EAAkB,EAAsC,CAC1F,GAAI,OAAO,IAAY,UAAY,IAAY,KAC7C,MAAO,GAGT,IAAM,EAAkB,EAAoC,UAC5D,GAAI,OAAO,IAAmB,UAAY,IAAmB,KAC3D,MAAO,GAGT,IAAM,EAAY,EAClB,GACE,OAAO,EAAU,YAAc,UAC/B,OAAO,EAAU,QAAU,UAC3B,OAAO,EAAU,YAAc,SAE/B,MAAO,GAGT,OAAO,EAAgB,EAAU,UAAW,EAAU,MAAO,EAAU,UAAW,CAAU",
8
- "debugId": "7FB21CD9E29B4FE264756E2164756E21",
8
+ "debugId": "57A0A59DBAEA409B64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,3 +1,3 @@
1
- import{d as u,e as q,g as x}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var C={Delivery:"delivered",Bounce:"bounced",SpamComplaint:"complained",Open:"opened",Click:"clicked",SubscriptionChange:"unknown",Inbound:"unknown"};function F(g){if(typeof g!=="object"||g===null)return[];let f=g,B=typeof f.RecordType==="string"?f.RecordType:"";return[x({provider:"postmark",type:u(B,C),...typeof f.MessageID==="string"?{messageId:f.MessageID}:{},...typeof f.Recipient==="string"?{recipient:f.Recipient}:typeof f.Email==="string"?{recipient:f.Email}:{},...q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)!==void 0?{timestamp:q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)}:{},raw:g})]}export{F as parse};
1
+ import{f as u,g as q,i as x}from"../chunk-sbgt4165.js";import"../chunk-th4cwrpb.js";var C={Delivery:"delivered",Bounce:"bounced",SpamComplaint:"complained",Open:"opened",Click:"clicked",SubscriptionChange:"unknown",Inbound:"unknown"};function F(g){if(typeof g!=="object"||g===null)return[];let f=g,B=typeof f.RecordType==="string"?f.RecordType:"";return[x({provider:"postmark",type:u(B,C),...typeof f.MessageID==="string"?{messageId:f.MessageID}:{},...typeof f.Recipient==="string"?{recipient:f.Recipient}:typeof f.Email==="string"?{recipient:f.Email}:{},...q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)!==void 0?{timestamp:q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)}:{},raw:g})]}export{F as parse};
2
2
 
3
- //# debugId=7CD05BB54D484C3864756E2164756E21
3
+ //# debugId=E542E926C34E828A64756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "import type { EmailEvent } from \"./types.js\";\nimport { emailEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst POSTMARK_EVENT_MAP: Record<string, EmailEvent[\"type\"]> = {\n Delivery: \"delivered\",\n Bounce: \"bounced\",\n SpamComplaint: \"complained\",\n Open: \"opened\",\n Click: \"clicked\",\n SubscriptionChange: \"unknown\",\n Inbound: \"unknown\",\n};\n\n/** Parse a Postmark webhook payload into normalized {@link EmailEvent} records. */\nexport function parse(payload: unknown): EmailEvent[] {\n if (typeof payload !== \"object\" || payload === null) {\n return [];\n }\n\n const record = payload as Record<string, unknown>;\n const recordType = typeof record.RecordType === \"string\" ? record.RecordType : \"\";\n\n return [\n emailEvent({\n provider: \"postmark\",\n type: mapEventType(recordType, POSTMARK_EVENT_MAP),\n ...(typeof record.MessageID === \"string\" ? { messageId: record.MessageID } : {}),\n ...(typeof record.Recipient === \"string\"\n ? { recipient: record.Recipient }\n : typeof record.Email === \"string\"\n ? { recipient: record.Email }\n : {}),\n ...(parseTimestamp(\n record.DeliveredAt ?? record.BouncedAt ?? record.ReceivedAt ?? record.ChangedAt,\n ) !== undefined\n ? {\n timestamp: parseTimestamp(\n record.DeliveredAt ?? record.BouncedAt ?? record.ReceivedAt ?? record.ChangedAt,\n ),\n }\n : {}),\n raw: payload,\n }),\n ];\n}\n"
6
6
  ],
7
7
  "mappings": "yFAGA,DAAM,EAAyD,CAC7D,SAAU,YACV,OAAQ,UACR,cAAe,aACf,KAAM,SACN,MAAO,UACP,mBAAoB,UACpB,QAAS,SACX,EAGO,SAAS,CAAK,CAAC,EAAgC,CACpD,GAAI,OAAO,IAAY,UAAY,IAAY,KAC7C,MAAO,CAAC,EAGV,IAAM,EAAS,EACT,EAAa,OAAO,EAAO,aAAe,SAAW,EAAO,WAAa,GAE/E,MAAO,CACL,EAAW,CACT,SAAU,WACV,KAAM,EAAa,EAAY,CAAkB,KAC7C,OAAO,EAAO,YAAc,SAAW,CAAE,UAAW,EAAO,SAAU,EAAI,CAAC,KAC1E,OAAO,EAAO,YAAc,SAC5B,CAAE,UAAW,EAAO,SAAU,EAC9B,OAAO,EAAO,QAAU,SACtB,CAAE,UAAW,EAAO,KAAM,EAC1B,CAAC,KACH,EACF,EAAO,aAAe,EAAO,WAAa,EAAO,YAAc,EAAO,SACxE,IAAM,OACF,CACE,UAAW,EACT,EAAO,aAAe,EAAO,WAAa,EAAO,YAAc,EAAO,SACxE,CACF,EACA,CAAC,EACL,IAAK,CACP,CAAC,CACH",
8
- "debugId": "7CD05BB54D484C3864756E2164756E21",
8
+ "debugId": "E542E926C34E828A64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,3 +1,3 @@
1
- import{a as $,b as A}from"../chunk-pqp843mh.js";import{d as X,e as Y,g as Z}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var j={"email.sent":"unknown","email.scheduled":"unknown","email.delivered":"delivered","email.delivery_delayed":"deferred","email.bounced":"bounced","email.failed":"bounced","email.complained":"complained","email.opened":"opened","email.clicked":"clicked","email.received":"unknown","email.suppressed":"unknown"};function k(G){let F=G.startsWith("whsec_")?G.slice(6):G,K=atob(F),J=new Uint8Array(K.length);for(let z=0;z<K.length;z++)J[z]=K.charCodeAt(z);return J.buffer}function H(G){if(typeof G!=="object"||G===null)return[];let F=G,K=typeof F.type==="string"?F.type:"",J=typeof F.data==="object"&&F.data!==null?F.data:{},z=J.to,L=Array.isArray(z)?typeof z[0]==="string"?z[0]:void 0:typeof z==="string"?z:void 0,O=Y(F.created_at??J.created_at);return[Z({provider:"resend",type:X(K,j),...typeof J.email_id==="string"?{messageId:J.email_id}:{},...L!==void 0?{recipient:L}:{},...O!==void 0?{timestamp:O}:{},raw:G})]}async function I(G,F,K){let J=F["svix-id"],z=F["svix-timestamp"],L=F["svix-signature"];if(!J||!z||!L)return!1;let O=`${J}.${z}.${G}`,D=await crypto.subtle.importKey("raw",k(K),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),M=await crypto.subtle.sign("HMAC",D,new TextEncoder().encode(O)),P=new Uint8Array(M);for(let W of L.split(" ")){let Q=W.split(",",2)[1];if(Q===void 0)continue;let U=A(Q);if(U===null)continue;if($(P,U))return!0}return!1}export{I as verifySignature,H as parse};
1
+ import{a as $,b as A}from"../chunk-pqp843mh.js";import{f as X,g as Y,i as Z}from"../chunk-sbgt4165.js";import"../chunk-th4cwrpb.js";var j={"email.sent":"unknown","email.scheduled":"unknown","email.delivered":"delivered","email.delivery_delayed":"deferred","email.bounced":"bounced","email.failed":"bounced","email.complained":"complained","email.opened":"opened","email.clicked":"clicked","email.received":"unknown","email.suppressed":"unknown"};function k(G){let F=G.startsWith("whsec_")?G.slice(6):G,K=atob(F),J=new Uint8Array(K.length);for(let z=0;z<K.length;z++)J[z]=K.charCodeAt(z);return J.buffer}function H(G){if(typeof G!=="object"||G===null)return[];let F=G,K=typeof F.type==="string"?F.type:"",J=typeof F.data==="object"&&F.data!==null?F.data:{},z=J.to,L=Array.isArray(z)?typeof z[0]==="string"?z[0]:void 0:typeof z==="string"?z:void 0,O=Y(F.created_at??J.created_at);return[Z({provider:"resend",type:X(K,j),...typeof J.email_id==="string"?{messageId:J.email_id}:{},...L!==void 0?{recipient:L}:{},...O!==void 0?{timestamp:O}:{},raw:G})]}async function I(G,F,K){let J=F["svix-id"],z=F["svix-timestamp"],L=F["svix-signature"];if(!J||!z||!L)return!1;let O=`${J}.${z}.${G}`,D=await crypto.subtle.importKey("raw",k(K),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),M=await crypto.subtle.sign("HMAC",D,new TextEncoder().encode(O)),P=new Uint8Array(M);for(let W of L.split(" ")){let Q=W.split(",",2)[1];if(Q===void 0)continue;let U=A(Q);if(U===null)continue;if($(P,U))return!0}return!1}export{I as verifySignature,H as parse};
2
2
 
3
- //# debugId=2EA62D07463E258664756E2164756E21
3
+ //# debugId=47D7FE30A17F641864756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "import { decodeBase64Bytes, timingSafeEqual } from \"./timing-safe-equal.js\";\nimport type { EmailEvent } from \"./types.js\";\nimport { emailEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst RESEND_EVENT_MAP: Record<string, EmailEvent[\"type\"]> = {\n \"email.sent\": \"unknown\",\n \"email.scheduled\": \"unknown\",\n \"email.delivered\": \"delivered\",\n \"email.delivery_delayed\": \"deferred\",\n \"email.bounced\": \"bounced\",\n \"email.failed\": \"bounced\",\n \"email.complained\": \"complained\",\n \"email.opened\": \"opened\",\n \"email.clicked\": \"clicked\",\n \"email.received\": \"unknown\",\n \"email.suppressed\": \"unknown\",\n};\n\n/** Decode a Svix signing secret (`whsec_…`) to raw key bytes. */\nfunction decodeSvixSecret(secret: string): ArrayBuffer {\n const encoded = secret.startsWith(\"whsec_\") ? secret.slice(6) : secret;\n const binary = atob(encoded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/** Parse a Resend webhook payload into normalized {@link EmailEvent} records. */\nexport function parse(payload: unknown): EmailEvent[] {\n if (typeof payload !== \"object\" || payload === null) {\n return [];\n }\n\n const record = payload as Record<string, unknown>;\n const type = typeof record.type === \"string\" ? record.type : \"\";\n const data =\n typeof record.data === \"object\" && record.data !== null\n ? (record.data as Record<string, unknown>)\n : {};\n\n const to = data.to;\n const recipient = Array.isArray(to)\n ? typeof to[0] === \"string\"\n ? to[0]\n : undefined\n : typeof to === \"string\"\n ? to\n : undefined;\n\n const timestamp = parseTimestamp(record.created_at ?? data.created_at);\n\n return [\n emailEvent({\n provider: \"resend\",\n type: mapEventType(type, RESEND_EVENT_MAP),\n ...(typeof data.email_id === \"string\" ? { messageId: data.email_id } : {}),\n ...(recipient !== undefined ? { recipient } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n raw: payload,\n }),\n ];\n}\n\n/**\n * Verify a Resend webhook signature (Svix-style) using Web Crypto.\n * Returns false when verification cannot be performed or the signature is invalid.\n */\nexport async function verifySignature(\n payload: string,\n headers: { \"svix-id\"?: string; \"svix-timestamp\"?: string; \"svix-signature\"?: string },\n secret: string,\n): Promise<boolean> {\n const msgId = headers[\"svix-id\"];\n const timestamp = headers[\"svix-timestamp\"];\n const signatureHeader = headers[\"svix-signature\"];\n\n if (!msgId || !timestamp || !signatureHeader) {\n return false;\n }\n\n const signedContent = `${msgId}.${timestamp}.${payload}`;\n const key = await crypto.subtle.importKey(\n \"raw\",\n decodeSvixSecret(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n\n const expected = await crypto.subtle.sign(\"HMAC\", key, new TextEncoder().encode(signedContent));\n const expectedBytes = new Uint8Array(expected);\n\n for (const part of signatureHeader.split(\" \")) {\n const versioned = part.split(\",\", 2);\n const sig = versioned[1];\n if (sig === undefined) {\n continue;\n }\n const providedBytes = decodeBase64Bytes(sig);\n if (providedBytes === null) {\n continue;\n }\n if (timingSafeEqual(expectedBytes, providedBytes)) {\n return true;\n }\n }\n\n return false;\n}\n"
6
6
  ],
7
7
  "mappings": "8IAIA,DAAM,HAAuD,CAC3D,aAAc,UACd,kBAAmB,UACnB,kBAAmB,YACnB,yBAA0B,WAC1B,gBAAiB,UACjB,eAAgB,UAChB,mBAAoB,aACpB,eAAgB,SAChB,gBAAiB,UACjB,iBAAkB,UAClB,mBAAoB,SACtB,EAGA,SAAS,CAAgB,CAAC,EAA6B,CACrD,IAAM,EAAU,EAAO,WAAW,QAAQ,EAAI,EAAO,MAAM,CAAC,EAAI,EAC1D,EAAS,KAAK,CAAO,EACrB,EAAQ,IAAI,WAAW,EAAO,MAAM,EAC1C,QAAS,EAAI,EAAG,EAAI,EAAO,OAAQ,IACjC,EAAM,GAAK,EAAO,WAAW,CAAC,EAEhC,OAAO,EAAM,OAIR,SAAS,CAAK,CAAC,EAAgC,CACpD,GAAI,OAAO,IAAY,UAAY,IAAY,KAC7C,MAAO,CAAC,EAGV,IAAM,EAAS,EACT,EAAO,OAAO,EAAO,OAAS,SAAW,EAAO,KAAO,GACvD,EACJ,OAAO,EAAO,OAAS,UAAY,EAAO,OAAS,KAC9C,EAAO,KACR,CAAC,EAED,EAAK,EAAK,GACV,EAAY,MAAM,QAAQ,CAAE,EAC9B,OAAO,EAAG,KAAO,SACf,EAAG,GACH,OACF,OAAO,IAAO,SACZ,EACA,OAEA,EAAY,EAAe,EAAO,YAAc,EAAK,UAAU,EAErE,MAAO,CACL,EAAW,CACT,SAAU,SACV,KAAM,EAAa,EAAM,CAAgB,KACrC,OAAO,EAAK,WAAa,SAAW,CAAE,UAAW,EAAK,QAAS,EAAI,CAAC,KACpE,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,KAC3C,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,EAC/C,IAAK,CACP,CAAC,CACH,EAOF,eAAsB,CAAe,CACnC,EACA,EACA,EACkB,CAClB,IAAM,EAAQ,EAAQ,WAChB,EAAY,EAAQ,kBACpB,EAAkB,EAAQ,kBAEhC,GAAI,CAAC,GAAS,CAAC,GAAa,CAAC,EAC3B,MAAO,GAGT,IAAM,EAAgB,GAAG,KAAS,KAAa,IACzC,EAAM,MAAM,OAAO,OAAO,UAC9B,MACA,EAAiB,CAAM,EACvB,CAAE,KAAM,OAAQ,KAAM,SAAU,EAChC,GACA,CAAC,MAAM,CACT,EAEM,EAAW,MAAM,OAAO,OAAO,KAAK,OAAQ,EAAK,IAAI,YAAY,EAAE,OAAO,CAAa,CAAC,EACxF,EAAgB,IAAI,WAAW,CAAQ,EAE7C,QAAW,KAAQ,EAAgB,MAAM,GAAG,EAAG,CAE7C,IAAM,EADY,EAAK,MAAM,IAAK,CAAC,EACb,GACtB,GAAI,IAAQ,OACV,SAEF,IAAM,EAAgB,EAAkB,CAAG,EAC3C,GAAI,IAAkB,KACpB,SAEF,GAAI,EAAgB,EAAe,CAAa,EAC9C,MAAO,GAIX,MAAO",
8
- "debugId": "2EA62D07463E258664756E2164756E21",
8
+ "debugId": "47D7FE30A17F641864756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,3 +1,3 @@
1
- import{d as w,e as x,f as B,g as C}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var F={processed:"unknown",delivered:"delivered",deferred:"deferred",bounce:"bounced",dropped:"bounced",spamreport:"complained",unsubscribe:"complained",group_unsubscribe:"complained",group_resubscribe:"unknown",open:"opened",click:"clicked"};function H(q){let g=typeof q.event==="string"?q.event:"";if(g==="bounce"&&q.type==="blocked")return"deferred";return w(g,F)}function K(q){return B(q).flatMap((g)=>{if(typeof g!=="object"||g===null)return[];let f=g,u=x(f.timestamp);return[C({provider:"sendgrid",type:H(f),...typeof f.sg_message_id==="string"?{messageId:f.sg_message_id}:typeof f["smtp-id"]==="string"?{messageId:f["smtp-id"]}:{},...typeof f.email==="string"?{recipient:f.email}:{},...u!==void 0?{timestamp:u}:{},raw:g})]})}export{K as parse};
1
+ import{f as w,g as x,h as B,i as C}from"../chunk-sbgt4165.js";import"../chunk-th4cwrpb.js";var F={processed:"unknown",delivered:"delivered",deferred:"deferred",bounce:"bounced",dropped:"bounced",spamreport:"complained",unsubscribe:"complained",group_unsubscribe:"complained",group_resubscribe:"unknown",open:"opened",click:"clicked"};function H(q){let g=typeof q.event==="string"?q.event:"";if(g==="bounce"&&q.type==="blocked")return"deferred";return w(g,F)}function K(q){return B(q).flatMap((g)=>{if(typeof g!=="object"||g===null)return[];let f=g,u=x(f.timestamp);return[C({provider:"sendgrid",type:H(f),...typeof f.sg_message_id==="string"?{messageId:f.sg_message_id}:typeof f["smtp-id"]==="string"?{messageId:f["smtp-id"]}:{},...typeof f.email==="string"?{recipient:f.email}:{},...u!==void 0?{timestamp:u}:{},raw:g})]})}export{K as parse};
2
2
 
3
- //# debugId=D4C7DDEE719D449764756E2164756E21
3
+ //# debugId=5C2871226967E78664756E2164756E21