sently 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +60 -0
- package/README.md +18 -16
- package/dist/adapters/bun.js +1 -1
- package/dist/adapters/cf.js +2 -2
- package/dist/adapters/cf.js.map +1 -1
- package/dist/adapters/deno.js +1 -1
- package/dist/adapters/node.js +1 -1
- package/dist/auth/oauth2.js +2 -2
- package/dist/auth/oauth2.js.map +1 -1
- package/dist/channel-result.d.ts +6 -0
- package/dist/channel-result.js +1 -0
- package/dist/{chunk-skqhj0rm.js → chunk-1ke3nmyg.js} +3 -3
- package/dist/{chunk-skqhj0rm.js.map → chunk-1ke3nmyg.js.map} +1 -1
- package/dist/{chunk-7gy2q9sh.js → chunk-5es0v1j7.js} +2 -2
- package/dist/{chunk-7gy2q9sh.js.map → chunk-5es0v1j7.js.map} +1 -1
- package/dist/{chunk-0qxws3kj.js → chunk-7wtfjx0d.js} +2 -2
- package/dist/{chunk-0qxws3kj.js.map → chunk-7wtfjx0d.js.map} +1 -1
- package/dist/chunk-8kpgbrba.js +4 -0
- package/dist/{chunk-93vqxxj2.js.map → chunk-8kpgbrba.js.map} +2 -2
- package/dist/{chunk-vjpds2pf.js → chunk-a057jzsy.js} +2 -2
- package/dist/{chunk-vjpds2pf.js.map → chunk-a057jzsy.js.map} +1 -1
- package/dist/{chunk-8njyga62.js → chunk-bhr7w3gg.js} +2 -2
- package/dist/{chunk-8njyga62.js.map → chunk-bhr7w3gg.js.map} +1 -1
- package/dist/chunk-cs1tvehn.js +4 -0
- package/dist/chunk-cs1tvehn.js.map +10 -0
- package/dist/{chunk-29z7fkzy.js → chunk-d5aw5vag.js} +3 -3
- package/dist/{chunk-29z7fkzy.js.map → chunk-d5aw5vag.js.map} +1 -1
- package/dist/chunk-f5x8vesr.js +4 -0
- package/dist/{chunk-b602dhck.js.map → chunk-f5x8vesr.js.map} +2 -2
- package/dist/chunk-fv0bemgf.js +4 -0
- package/dist/{chunk-yp311efr.js.map → chunk-fv0bemgf.js.map} +3 -3
- package/dist/{chunk-ywq6s10g.js → chunk-hnzmn4s4.js} +2 -2
- package/dist/{chunk-ywq6s10g.js.map → chunk-hnzmn4s4.js.map} +1 -1
- package/dist/chunk-htdg075t.js +4 -0
- package/dist/{chunk-qtkd5bak.js.map → chunk-htdg075t.js.map} +4 -3
- package/dist/{chunk-0gqy32pe.js → chunk-q2mahnb7.js} +3 -3
- package/dist/chunk-q2mahnb7.js.map +10 -0
- package/dist/chunk-sbgt4165.js +4 -0
- package/dist/chunk-sbgt4165.js.map +10 -0
- package/dist/{chunk-6npp3x3c.js → chunk-th4cwrpb.js} +2 -2
- package/dist/{chunk-6npp3x3c.js.map → chunk-th4cwrpb.js.map} +1 -1
- package/dist/{chunk-tamww15j.js → chunk-ttbwyxmh.js} +2 -2
- package/dist/{chunk-tamww15j.js.map → chunk-ttbwyxmh.js.map} +1 -1
- package/dist/chunk-w4c9kj70.js +13 -0
- package/dist/{chunk-n0qeyzqm.js.map → chunk-w4c9kj70.js.map} +2 -2
- package/dist/chunk-w9sabmb0.js +4 -0
- package/dist/chunk-w9sabmb0.js.map +10 -0
- package/dist/{chunk-8dty8c4p.js → chunk-yg3kj723.js} +3 -3
- package/dist/{chunk-8dty8c4p.js.map → chunk-yg3kj723.js.map} +2 -2
- package/dist/chunk-z1589fjk.js +4 -0
- package/dist/chunk-z1589fjk.js.map +10 -0
- package/dist/core/channel-result.d.ts +36 -0
- package/dist/core/channel-result.js +3 -0
- package/dist/core/channel-result.js.map +10 -0
- package/dist/core/decorated-transport.d.ts +30 -0
- package/dist/core/decorator-hooks.d.ts +17 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/core/provider-label.d.ts +7 -3
- package/dist/core/push-endpoint.d.ts +5 -0
- package/dist/core/push-types.d.ts +48 -14
- package/dist/core/sms-types.d.ts +12 -0
- package/dist/core/smtp.js +2 -2
- package/dist/core/smtp.js.map +1 -1
- package/dist/core/whatsapp-types.d.ts +12 -0
- package/dist/detect.js +2 -2
- package/dist/detect.js.map +1 -1
- package/dist/dkim.js +2 -2
- package/dist/dkim.js.map +2 -2
- package/dist/idempotency.js +2 -2
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/mailer.js +2 -2
- package/dist/mailer.js.map +1 -1
- package/dist/observability/console.js +1 -1
- package/dist/plugins/react.js +2 -2
- package/dist/plugins/react.js.map +1 -1
- package/dist/plugins/template.js +1 -1
- package/dist/pool/pool.js +2 -2
- package/dist/pool/pool.js.map +1 -1
- package/dist/push.d.ts +1 -0
- package/dist/push.js +2 -2
- package/dist/push.js.map +3 -3
- package/dist/sms.d.ts +1 -0
- package/dist/sms.js +2 -2
- package/dist/sms.js.map +3 -3
- package/dist/smtp-mailer.js +2 -2
- package/dist/smtp-mailer.js.map +1 -1
- package/dist/transports/brevo.js +2 -2
- package/dist/transports/brevo.js.map +2 -2
- package/dist/transports/cloudflare-email.js +2 -2
- package/dist/transports/cloudflare-email.js.map +2 -2
- package/dist/transports/fallback.d.ts +32 -6
- package/dist/transports/fallback.js +2 -2
- package/dist/transports/fallback.js.map +1 -1
- package/dist/transports/fcm.d.ts +55 -0
- package/dist/transports/fcm.js +4 -0
- package/dist/transports/fcm.js.map +10 -0
- package/dist/transports/loops.js +2 -2
- package/dist/transports/loops.js.map +1 -1
- package/dist/transports/mailersend.js +2 -2
- package/dist/transports/mailersend.js.map +2 -2
- package/dist/transports/mailgun.js +2 -2
- package/dist/transports/mailgun.js.map +2 -2
- package/dist/transports/mailtrap.js +2 -2
- package/dist/transports/mailtrap.js.map +2 -2
- package/dist/transports/msegat.js +2 -2
- package/dist/transports/msegat.js.map +1 -1
- package/dist/transports/plunk.js +2 -2
- package/dist/transports/plunk.js.map +1 -1
- package/dist/transports/postmark.js +2 -2
- package/dist/transports/postmark.js.map +2 -2
- package/dist/transports/preview.js +2 -2
- package/dist/transports/preview.js.map +1 -1
- package/dist/transports/resend.js +2 -2
- package/dist/transports/resend.js.map +2 -2
- package/dist/transports/retry.d.ts +28 -9
- package/dist/transports/retry.js +2 -2
- package/dist/transports/retry.js.map +3 -3
- package/dist/transports/sendgrid.js +2 -2
- package/dist/transports/sendgrid.js.map +2 -2
- package/dist/transports/ses.js +2 -2
- package/dist/transports/ses.js.map +2 -2
- package/dist/transports/smtp.js +2 -2
- package/dist/transports/smtp.js.map +1 -1
- package/dist/transports/sndr.js +2 -2
- package/dist/transports/sndr.js.map +1 -1
- package/dist/transports/sparkpost.js +2 -2
- package/dist/transports/sparkpost.js.map +2 -2
- package/dist/transports/taqnyat-mail.js +2 -2
- package/dist/transports/taqnyat-mail.js.map +1 -1
- package/dist/transports/taqnyat-sms.js +2 -2
- package/dist/transports/taqnyat-sms.js.map +1 -1
- package/dist/transports/taqnyat-whatsapp.js +2 -2
- package/dist/transports/taqnyat-whatsapp.js.map +1 -1
- package/dist/transports/twilio-sms.js +2 -2
- package/dist/transports/twilio-sms.js.map +2 -2
- package/dist/transports/unifonic.d.ts +81 -0
- package/dist/transports/unifonic.js +3 -0
- package/dist/transports/unifonic.js.map +10 -0
- package/dist/transports/webpush.js +2 -2
- package/dist/transports/webpush.js.map +3 -3
- package/dist/transports/weighted-fallback.d.ts +11 -9
- package/dist/transports/weighted-fallback.js +2 -2
- package/dist/transports/weighted-fallback.js.map +3 -3
- package/dist/transports/whatsapp-cloud.js +2 -2
- package/dist/transports/whatsapp-cloud.js.map +1 -1
- package/dist/webhooks/brevo.js +2 -2
- package/dist/webhooks/brevo.js.map +1 -1
- package/dist/webhooks/mailgun.js +2 -2
- package/dist/webhooks/mailgun.js.map +1 -1
- package/dist/webhooks/postmark.js +2 -2
- package/dist/webhooks/postmark.js.map +1 -1
- package/dist/webhooks/resend.js +2 -2
- package/dist/webhooks/resend.js.map +1 -1
- package/dist/webhooks/sendgrid.js +2 -2
- package/dist/webhooks/sendgrid.js.map +1 -1
- package/dist/webhooks/ses.js +2 -2
- package/dist/webhooks/ses.js.map +1 -1
- package/dist/webhooks/sndr.js +2 -2
- package/dist/webhooks/sndr.js.map +1 -1
- package/dist/webhooks/timing-safe-equal.js +1 -1
- package/dist/webhooks/twilio-sms.d.ts +20 -0
- package/dist/webhooks/twilio-sms.js +3 -0
- package/dist/webhooks/twilio-sms.js.map +10 -0
- package/dist/webhooks/types.d.ts +37 -3
- package/dist/webhooks/types.js +3 -0
- package/dist/webhooks/types.js.map +9 -0
- package/dist/webhooks/unifonic.d.ts +13 -0
- package/dist/webhooks/unifonic.js +3 -0
- package/dist/webhooks/unifonic.js.map +10 -0
- package/dist/webhooks/whatsapp-cloud.d.ts +14 -0
- package/dist/webhooks/whatsapp-cloud.js +3 -0
- package/dist/webhooks/whatsapp-cloud.js.map +10 -0
- package/dist/webhooks.d.ts +10 -5
- package/dist/webhooks.js +11 -0
- package/dist/whatsapp.d.ts +1 -0
- package/dist/whatsapp.js +2 -2
- package/dist/whatsapp.js.map +3 -3
- package/package.json +26 -2
- package/site/content/docs/channels/hooks.mdx +6 -5
- package/site/content/docs/channels/index.mdx +2 -2
- package/site/content/docs/channels/push.mdx +27 -21
- package/site/content/docs/channels/sms.mdx +4 -1
- package/site/content/docs/decorators/fallback.mdx +41 -14
- package/site/content/docs/decorators/index.mdx +29 -12
- package/site/content/docs/decorators/retry.mdx +32 -9
- package/site/content/docs/decorators/weighted-fallback.mdx +27 -6
- package/site/content/docs/get-started/entrypoints.mdx +6 -5
- package/site/content/docs/get-started/installation.mdx +3 -2
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/guides/webhooks.mdx +35 -20
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/quick-start/push.mdx +22 -5
- package/site/content/docs/reference/channel-result.mdx +82 -0
- package/site/content/docs/reference/exports.mdx +5 -3
- package/site/content/docs/reference/meta.json +1 -0
- package/site/content/docs/reference/push-options.mdx +44 -3
- package/site/content/docs/reference/sms-options.mdx +18 -2
- package/site/content/docs/reference/webhook-events.mdx +38 -14
- package/site/content/docs/reference/whatsapp-options.mdx +19 -1
- package/site/content/docs/transports/fcm.mdx +80 -0
- package/site/content/docs/transports/index.mdx +3 -1
- package/site/content/docs/transports/meta.json +3 -1
- package/site/content/docs/transports/unifonic.mdx +85 -0
- package/dist/chunk-0gqy32pe.js.map +0 -10
- package/dist/chunk-5915vbcj.js +0 -4
- package/dist/chunk-5915vbcj.js.map +0 -10
- package/dist/chunk-93vqxxj2.js +0 -4
- package/dist/chunk-b602dhck.js +0 -4
- package/dist/chunk-gp4fwrs8.js +0 -4
- package/dist/chunk-gp4fwrs8.js.map +0 -10
- package/dist/chunk-n0qeyzqm.js +0 -13
- package/dist/chunk-q8k3m6qe.js +0 -4
- package/dist/chunk-q8k3m6qe.js.map +0 -10
- package/dist/chunk-qtkd5bak.js +0 -4
- package/dist/chunk-yp311efr.js +0 -4
|
@@ -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 *
|
|
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": "+
|
|
8
|
-
"debugId": "
|
|
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{
|
|
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=
|
|
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": "
|
|
8
|
+
"debugId": "B754BE00883090E464756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/webhooks/brevo.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
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=
|
|
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": "
|
|
8
|
+
"debugId": "F6BBB0F16174F70764756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/webhooks/mailgun.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{a as G,c as H}from"../chunk-pqp843mh.js";import{
|
|
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=
|
|
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": "
|
|
8
|
+
"debugId": "57A0A59DBAEA409B64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
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=
|
|
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": "
|
|
8
|
+
"debugId": "E542E926C34E828A64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/webhooks/resend.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{a as $,b as A}from"../chunk-pqp843mh.js";import{
|
|
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=
|
|
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": "
|
|
8
|
+
"debugId": "47D7FE30A17F641864756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
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=
|
|
3
|
+
//# debugId=5C2871226967E78664756E2164756E21
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"import type { EmailEvent } from \"./types.js\";\nimport { asArray, emailEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst SENDGRID_EVENT_MAP: Record<string, EmailEvent[\"type\"]> = {\n processed: \"unknown\",\n delivered: \"delivered\",\n deferred: \"deferred\",\n bounce: \"bounced\",\n dropped: \"bounced\",\n spamreport: \"complained\",\n unsubscribe: \"complained\",\n group_unsubscribe: \"complained\",\n group_resubscribe: \"unknown\",\n open: \"opened\",\n click: \"clicked\",\n};\n\n/** Map a SendGrid event name (and bounce subtype) to a normalized type. */\nfunction mapSendGridType(record: Record<string, unknown>): EmailEvent[\"type\"] {\n const event = typeof record.event === \"string\" ? record.event : \"\";\n if (event === \"bounce\" && record.type === \"blocked\") {\n return \"deferred\";\n }\n return mapEventType(event, SENDGRID_EVENT_MAP);\n}\n\n/** Parse a SendGrid Event 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 timestamp = parseTimestamp(record.timestamp);\n\n return [\n emailEvent({\n provider: \"sendgrid\",\n type: mapSendGridType(record),\n ...(typeof record.sg_message_id === \"string\"\n ? { messageId: record.sg_message_id }\n : typeof record[\"smtp-id\"] === \"string\"\n ? { messageId: record[\"smtp-id\"] }\n : {}),\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,EAAyD,CAC7D,UAAW,UACX,UAAW,YACX,SAAU,WACV,OAAQ,UACR,QAAS,UACT,WAAY,aACZ,YAAa,aACb,kBAAmB,aACnB,kBAAmB,UACnB,KAAM,SACN,MAAO,SACT,EAGA,SAAS,CAAe,CAAC,EAAqD,CAC5E,IAAM,EAAQ,OAAO,EAAO,QAAU,SAAW,EAAO,MAAQ,GAChE,GAAI,IAAU,UAAY,EAAO,OAAS,UACxC,MAAO,WAET,OAAO,EAAa,EAAO,CAAkB,EAIxC,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,EAAY,EAAe,EAAO,SAAS,EAEjD,MAAO,CACL,EAAW,CACT,SAAU,WACV,KAAM,EAAgB,CAAM,KACxB,OAAO,EAAO,gBAAkB,SAChC,CAAE,UAAW,EAAO,aAAc,EAClC,OAAO,EAAO,aAAe,SAC3B,CAAE,UAAW,EAAO,UAAW,EAC/B,CAAC,KACH,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": "
|
|
8
|
+
"debugId": "5C2871226967E78664756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/webhooks/ses.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{f as Q,g as U,i as X}from"../chunk-sbgt4165.js";import"../chunk-th4cwrpb.js";var $={Delivery:"delivered",Bounce:"bounced",Complaint:"complained",Open:"opened",Click:"clicked",DeliveryDelay:"deferred",Send:"unknown",Reject:"bounced","Rendering Failure":"bounced",Subscription:"unknown"};function h(q){if(typeof q.eventType==="string")return q.eventType;if(typeof q.notificationType==="string")return q.notificationType;return""}function j(q){let J=typeof q.delivery==="object"&&q.delivery!==null?q.delivery:void 0;if(J&&Array.isArray(J.recipients)&&typeof J.recipients[0]==="string")return J.recipients[0];let K=typeof q.bounce==="object"&&q.bounce!==null?q.bounce:void 0;if(K&&Array.isArray(K.bouncedRecipients)){let I=K.bouncedRecipients[0];if(typeof I==="object"&&I!==null&&typeof I.emailAddress==="string")return I.emailAddress}let B=typeof q.complaint==="object"&&q.complaint!==null?q.complaint:void 0;if(B&&Array.isArray(B.complainedRecipients)){let I=B.complainedRecipients[0];if(typeof I==="object"&&I!==null&&typeof I.emailAddress==="string")return I.emailAddress}let G=typeof q.mail==="object"&&q.mail!==null?q.mail:void 0;if(Array.isArray(G?.destination)&&typeof G.destination[0]==="string")return G.destination[0];return}function w(q){let J=h(q),K=typeof q.mail==="object"&&q.mail!==null?q.mail:void 0,B=typeof q.bounce==="object"&&q.bounce!==null?q.bounce:void 0,G=typeof q.complaint==="object"&&q.complaint!==null?q.complaint:void 0,I=typeof q.open==="object"&&q.open!==null?q.open:void 0,Y=typeof q.click==="object"&&q.click!==null?q.click:void 0,Z=typeof q.deliveryDelay==="object"&&q.deliveryDelay!==null?q.deliveryDelay:void 0,L=j(q),O=U(q.timestamp??B?.timestamp??G?.timestamp??I?.timestamp??Y?.timestamp??Z?.timestamp??K?.timestamp);return[X({provider:"ses",type:Q(J,$),...typeof K?.messageId==="string"?{messageId:K.messageId}:{},...L!==void 0?{recipient:L}:{},...O!==void 0?{timestamp:O}:{},raw:q})]}function z(q){if(typeof q!=="object"||q===null)return[];let J=q,K=typeof J.Type==="string"?J.Type:"";if(K==="SubscriptionConfirmation")return[];if(K!=="Notification")return[];let B=J.Message,G;if(typeof B==="string")try{G=JSON.parse(B)}catch{return[{provider:"ses",type:"unknown",raw:q}]}else if(typeof B==="object"&&B!==null)G=B;else return[];if(typeof G!=="object"||G===null)return[{provider:"ses",type:"unknown",raw:q}];return w(G)}export{z as parse};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=F6556F44BF9DBD1B64756E2164756E21
|
package/dist/webhooks/ses.js.map
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"import type { EmailEvent } from \"./types.js\";\nimport { emailEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst SES_EVENT_MAP: Record<string, EmailEvent[\"type\"]> = {\n Delivery: \"delivered\",\n Bounce: \"bounced\",\n Complaint: \"complained\",\n Open: \"opened\",\n Click: \"clicked\",\n DeliveryDelay: \"deferred\",\n Send: \"unknown\",\n Reject: \"bounced\",\n \"Rendering Failure\": \"bounced\",\n Subscription: \"unknown\",\n};\n\nfunction resolveSesEventType(record: Record<string, unknown>): string {\n if (typeof record.eventType === \"string\") {\n return record.eventType;\n }\n if (typeof record.notificationType === \"string\") {\n return record.notificationType;\n }\n return \"\";\n}\n\nfunction extractSesRecipient(record: Record<string, unknown>): string | undefined {\n const delivery =\n typeof record.delivery === \"object\" && record.delivery !== null\n ? (record.delivery as Record<string, unknown>)\n : undefined;\n if (\n delivery &&\n Array.isArray(delivery.recipients) &&\n typeof delivery.recipients[0] === \"string\"\n ) {\n return delivery.recipients[0];\n }\n\n const bounce =\n typeof record.bounce === \"object\" && record.bounce !== null\n ? (record.bounce as Record<string, unknown>)\n : undefined;\n if (bounce && Array.isArray(bounce.bouncedRecipients)) {\n const first = bounce.bouncedRecipients[0];\n if (\n typeof first === \"object\" &&\n first !== null &&\n typeof (first as Record<string, unknown>).emailAddress === \"string\"\n ) {\n return (first as Record<string, unknown>).emailAddress as string;\n }\n }\n\n const complaint =\n typeof record.complaint === \"object\" && record.complaint !== null\n ? (record.complaint as Record<string, unknown>)\n : undefined;\n if (complaint && Array.isArray(complaint.complainedRecipients)) {\n const first = complaint.complainedRecipients[0];\n if (\n typeof first === \"object\" &&\n first !== null &&\n typeof (first as Record<string, unknown>).emailAddress === \"string\"\n ) {\n return (first as Record<string, unknown>).emailAddress as string;\n }\n }\n\n const mail =\n typeof record.mail === \"object\" && record.mail !== null\n ? (record.mail as Record<string, unknown>)\n : undefined;\n if (Array.isArray(mail?.destination) && typeof mail.destination[0] === \"string\") {\n return mail.destination[0];\n }\n\n return undefined;\n}\n\nfunction parseSesEvent(record: Record<string, unknown>): EmailEvent[] {\n const eventType = resolveSesEventType(record);\n const mail =\n typeof record.mail === \"object\" && record.mail !== null\n ? (record.mail as Record<string, unknown>)\n : undefined;\n\n const bounce =\n typeof record.bounce === \"object\" && record.bounce !== null\n ? (record.bounce as Record<string, unknown>)\n : undefined;\n const complaint =\n typeof record.complaint === \"object\" && record.complaint !== null\n ? (record.complaint as Record<string, unknown>)\n : undefined;\n const open =\n typeof record.open === \"object\" && record.open !== null\n ? (record.open as Record<string, unknown>)\n : undefined;\n const click =\n typeof record.click === \"object\" && record.click !== null\n ? (record.click as Record<string, unknown>)\n : undefined;\n const deliveryDelay =\n typeof record.deliveryDelay === \"object\" && record.deliveryDelay !== null\n ? (record.deliveryDelay as Record<string, unknown>)\n : undefined;\n\n const recipient = extractSesRecipient(record);\n const timestamp = parseTimestamp(\n record.timestamp ??\n bounce?.timestamp ??\n complaint?.timestamp ??\n open?.timestamp ??\n click?.timestamp ??\n deliveryDelay?.timestamp ??\n mail?.timestamp,\n );\n\n return [\n emailEvent({\n provider: \"ses\",\n type: mapEventType(eventType, SES_EVENT_MAP),\n ...(typeof mail?.messageId === \"string\" ? { messageId: mail.messageId } : {}),\n ...(recipient !== undefined ? { recipient } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n raw: record,\n }),\n ];\n}\n\n/**\n * Parse an AWS SES webhook payload (SNS envelope) into normalized {@link EmailEvent} records.\n * Handles SubscriptionConfirmation (returns `[]`) and double-encoded Notification messages.\n */\nexport function parse(payload: unknown): EmailEvent[] {\n if (typeof payload !== \"object\" || payload === null) {\n return [];\n }\n\n const envelope = payload as Record<string, unknown>;\n const snsType = typeof envelope.Type === \"string\" ? envelope.Type : \"\";\n\n if (snsType === \"SubscriptionConfirmation\") {\n return [];\n }\n\n if (snsType !== \"Notification\") {\n return [];\n }\n\n const messageField = envelope.Message;\n let inner: unknown;\n\n if (typeof messageField === \"string\") {\n try {\n inner = JSON.parse(messageField);\n } catch {\n return [\n {\n provider: \"ses\",\n type: \"unknown\" as const,\n raw: payload,\n },\n ];\n }\n } else if (typeof messageField === \"object\" && messageField !== null) {\n inner = messageField;\n } else {\n return [];\n }\n\n if (typeof inner !== \"object\" || inner === null) {\n return [\n {\n provider: \"ses\",\n type: \"unknown\" as const,\n raw: payload,\n },\n ];\n }\n\n return parseSesEvent(inner as Record<string, unknown>);\n}\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": "yFAGA,DAAM,EAAoD,CACxD,SAAU,YACV,OAAQ,UACR,UAAW,aACX,KAAM,SACN,MAAO,UACP,cAAe,WACf,KAAM,UACN,OAAQ,UACR,oBAAqB,UACrB,aAAc,SAChB,EAEA,SAAS,CAAmB,CAAC,EAAyC,CACpE,GAAI,OAAO,EAAO,YAAc,SAC9B,OAAO,EAAO,UAEhB,GAAI,OAAO,EAAO,mBAAqB,SACrC,OAAO,EAAO,iBAEhB,MAAO,GAGT,SAAS,CAAmB,CAAC,EAAqD,CAChF,IAAM,EACJ,OAAO,EAAO,WAAa,UAAY,EAAO,WAAa,KACtD,EAAO,SACR,OACN,GACE,GACA,MAAM,QAAQ,EAAS,UAAU,GACjC,OAAO,EAAS,WAAW,KAAO,SAElC,OAAO,EAAS,WAAW,GAG7B,IAAM,EACJ,OAAO,EAAO,SAAW,UAAY,EAAO,SAAW,KAClD,EAAO,OACR,OACN,GAAI,GAAU,MAAM,QAAQ,EAAO,iBAAiB,EAAG,CACrD,IAAM,EAAQ,EAAO,kBAAkB,GACvC,GACE,OAAO,IAAU,UACjB,IAAU,MACV,OAAQ,EAAkC,eAAiB,SAE3D,OAAQ,EAAkC,aAI9C,IAAM,EACJ,OAAO,EAAO,YAAc,UAAY,EAAO,YAAc,KACxD,EAAO,UACR,OACN,GAAI,GAAa,MAAM,QAAQ,EAAU,oBAAoB,EAAG,CAC9D,IAAM,EAAQ,EAAU,qBAAqB,GAC7C,GACE,OAAO,IAAU,UACjB,IAAU,MACV,OAAQ,EAAkC,eAAiB,SAE3D,OAAQ,EAAkC,aAI9C,IAAM,EACJ,OAAO,EAAO,OAAS,UAAY,EAAO,OAAS,KAC9C,EAAO,KACR,OACN,GAAI,MAAM,QAAQ,GAAM,WAAW,GAAK,OAAO,EAAK,YAAY,KAAO,SACrE,OAAO,EAAK,YAAY,GAG1B,OAGF,SAAS,CAAa,CAAC,EAA+C,CACpE,IAAM,EAAY,EAAoB,CAAM,EACtC,EACJ,OAAO,EAAO,OAAS,UAAY,EAAO,OAAS,KAC9C,EAAO,KACR,OAEA,EACJ,OAAO,EAAO,SAAW,UAAY,EAAO,SAAW,KAClD,EAAO,OACR,OACA,EACJ,OAAO,EAAO,YAAc,UAAY,EAAO,YAAc,KACxD,EAAO,UACR,OACA,EACJ,OAAO,EAAO,OAAS,UAAY,EAAO,OAAS,KAC9C,EAAO,KACR,OACA,EACJ,OAAO,EAAO,QAAU,UAAY,EAAO,QAAU,KAChD,EAAO,MACR,OACA,EACJ,OAAO,EAAO,gBAAkB,UAAY,EAAO,gBAAkB,KAChE,EAAO,cACR,OAEA,EAAY,EAAoB,CAAM,EACtC,EAAY,EAChB,EAAO,WACL,GAAQ,WACR,GAAW,WACX,GAAM,WACN,GAAO,WACP,GAAe,WACf,GAAM,SACV,EAEA,MAAO,CACL,EAAW,CACT,SAAU,MACV,KAAM,EAAa,EAAW,CAAa,KACvC,OAAO,GAAM,YAAc,SAAW,CAAE,UAAW,EAAK,SAAU,EAAI,CAAC,KACvE,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,KAC3C,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,EAC/C,IAAK,CACP,CAAC,CACH,EAOK,SAAS,CAAK,CAAC,EAAgC,CACpD,GAAI,OAAO,IAAY,UAAY,IAAY,KAC7C,MAAO,CAAC,EAGV,IAAM,EAAW,EACX,EAAU,OAAO,EAAS,OAAS,SAAW,EAAS,KAAO,GAEpE,GAAI,IAAY,2BACd,MAAO,CAAC,EAGV,GAAI,IAAY,eACd,MAAO,CAAC,EAGV,IAAM,EAAe,EAAS,QAC1B,EAEJ,GAAI,OAAO,IAAiB,SAC1B,GAAI,CACF,EAAQ,KAAK,MAAM,CAAY,EAC/B,KAAM,CACN,MAAO,CACL,CACE,SAAU,MACV,KAAM,UACN,IAAK,CACP,CACF,EAEG,QAAI,OAAO,IAAiB,UAAY,IAAiB,KAC9D,EAAQ,EAER,WAAO,CAAC,EAGV,GAAI,OAAO,IAAU,UAAY,IAAU,KACzC,MAAO,CACL,CACE,SAAU,MACV,KAAM,UACN,IAAK,CACP,CACF,EAGF,OAAO,EAAc,CAAgC",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "F6556F44BF9DBD1B64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/webhooks/sndr.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{a as W,c as _}from"../chunk-pqp843mh.js";import{
|
|
1
|
+
import{a as W,c as _}from"../chunk-pqp843mh.js";import{f as U,g as X,i as j}from"../chunk-sbgt4165.js";import"../chunk-th4cwrpb.js";var x={"email.delivered":"delivered","email.bounced":"bounced","email.unsubscribed":"unknown"};function P(K){if(typeof K!=="object"||K===null)return[];let I=K,z=typeof I.type==="string"?I.type:"",L=typeof I.data==="object"&&I.data!==null?I.data:{},G=L.to??L.addresses,Q=Array.isArray(G)?typeof G[0]==="string"?G[0]:void 0:typeof G==="string"?G:void 0,Z=X(I.created_at);return[j({provider:"sndr",type:U(z,x),...typeof L.email_id==="string"?{messageId:L.email_id}:{},...Q!==void 0?{recipient:Q}:{},...Z!==void 0?{timestamp:Z}:{},raw:K})]}async function R(K,I,z,L){if(!I||!z)return!1;let G,Q;for(let Y of I.split(",")){let $=Y.indexOf("=");if($<0)continue;let J=Y.slice(0,$).trim(),O=Y.slice($+1).trim();if(J==="t")G=O;else if(J==="v1")Q=O}if(!G||!Q)return!1;let Z=L?.toleranceSeconds??300;if(Z>0){let Y=Number(G);if(!Number.isFinite(Y))return!1;let $=Math.floor(Date.now()/1000);if(Math.abs($-Y)>Z)return!1}let F=_(Q);if(F===null)return!1;let C=await crypto.subtle.importKey("raw",new TextEncoder().encode(z),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),D=new TextEncoder().encode(`${G}.${K}`),V=await crypto.subtle.sign("HMAC",C,D);return W(new Uint8Array(V),F)}export{R as verifySignature,P as parse};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=894DC903F2CF2D6664756E2164756E21
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"/**\n * @module\n * SNDR webhook parser and signature verification.\n *\n * Signature scheme (from `@rkiza/sndr` / SNDR dispatcher):\n * `X-Sndr-Signature: t=<unix_seconds>,v1=<hex(HMAC-SHA256(secret, `${t}.${rawBody}`))>`\n */\nimport { decodeHexBytes, timingSafeEqual } from \"./timing-safe-equal.js\";\nimport type { EmailEvent } from \"./types.js\";\nimport { emailEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst SNDR_EVENT_MAP: Record<string, EmailEvent[\"type\"]> = {\n \"email.delivered\": \"delivered\",\n \"email.bounced\": \"bounced\",\n \"email.unsubscribed\": \"unknown\",\n};\n\n/** Parse an SNDR 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 ?? data.addresses;\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);\n\n return [\n emailEvent({\n provider: \"sndr\",\n type: mapEventType(type, SNDR_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 an SNDR webhook signature using HMAC-SHA256 (Web Crypto).\n *\n * Pass the **exact** raw request body string — re-serializing JSON breaks the HMAC.\n *\n * @param rawBody — Raw HTTP body bytes as UTF-8 string (or the string used when signing).\n * @param signatureHeader — Value of the `X-Sndr-Signature` header (`t=…,v1=…`).\n * @param secret — Webhook endpoint signing secret from SNDR.\n * @param opts.toleranceSeconds — Max age of `t` in seconds (default `300`). Pass `0` to skip.\n */\nexport async function verifySignature(\n rawBody: string,\n signatureHeader: string,\n secret: string,\n opts?: { toleranceSeconds?: number },\n): Promise<boolean> {\n if (!signatureHeader || !secret) {\n return false;\n }\n\n let timestamp: string | undefined;\n let v1: string | undefined;\n for (const part of signatureHeader.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n const key = part.slice(0, eq).trim();\n const value = part.slice(eq + 1).trim();\n if (key === \"t\") timestamp = value;\n else if (key === \"v1\") v1 = value;\n }\n\n if (!timestamp || !v1) {\n return false;\n }\n\n const tolerance = opts?.toleranceSeconds ?? 300;\n if (tolerance > 0) {\n const tsNum = Number(timestamp);\n if (!Number.isFinite(tsNum)) {\n return false;\n }\n const nowSec = Math.floor(Date.now() / 1000);\n if (Math.abs(nowSec - tsNum) > tolerance) {\n return false;\n }\n }\n\n const providedBytes = decodeHexBytes(v1);\n if (providedBytes === null) {\n return false;\n }\n\n const key = await crypto.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n\n const signed = new TextEncoder().encode(`${timestamp}.${rawBody}`);\n const expected = await crypto.subtle.sign(\"HMAC\", key, signed);\n return timingSafeEqual(new Uint8Array(expected), providedBytes);\n}\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": "8IAWA,DAAM,HAAqD,CACzD,kBAAmB,YACnB,gBAAiB,UACjB,qBAAsB,SACxB,EAGO,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,IAAM,EAAK,UACrB,EAAY,MAAM,QAAQ,CAAE,EAC9B,OAAO,EAAG,KAAO,SACf,EAAG,GACH,OACF,OAAO,IAAO,SACZ,EACA,OAEA,EAAY,EAAe,EAAO,UAAU,EAElD,MAAO,CACL,EAAW,CACT,SAAU,OACV,KAAM,EAAa,EAAM,CAAc,KACnC,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,EAaF,eAAsB,CAAe,CACnC,EACA,EACA,EACA,EACkB,CAClB,GAAI,CAAC,GAAmB,CAAC,EACvB,MAAO,GAGT,IAAI,EACA,EACJ,QAAW,KAAQ,EAAgB,MAAM,GAAG,EAAG,CAC7C,IAAM,EAAK,EAAK,QAAQ,GAAG,EAC3B,GAAI,EAAK,EAAG,SACZ,IAAM,EAAM,EAAK,MAAM,EAAG,CAAE,EAAE,KAAK,EAC7B,EAAQ,EAAK,MAAM,EAAK,CAAC,EAAE,KAAK,EACtC,GAAI,IAAQ,IAAK,EAAY,EACxB,QAAI,IAAQ,KAAM,EAAK,EAG9B,GAAI,CAAC,GAAa,CAAC,EACjB,MAAO,GAGT,IAAM,EAAY,GAAM,kBAAoB,IAC5C,GAAI,EAAY,EAAG,CACjB,IAAM,EAAQ,OAAO,CAAS,EAC9B,GAAI,CAAC,OAAO,SAAS,CAAK,EACxB,MAAO,GAET,IAAM,EAAS,KAAK,MAAM,KAAK,IAAI,EAAI,IAAI,EAC3C,GAAI,KAAK,IAAI,EAAS,CAAK,EAAI,EAC7B,MAAO,GAIX,IAAM,EAAgB,EAAe,CAAE,EACvC,GAAI,IAAkB,KACpB,MAAO,GAGT,IAAM,EAAM,MAAM,OAAO,OAAO,UAC9B,MACA,IAAI,YAAY,EAAE,OAAO,CAAM,EAC/B,CAAE,KAAM,OAAQ,KAAM,SAAU,EAChC,GACA,CAAC,MAAM,CACT,EAEM,EAAS,IAAI,YAAY,EAAE,OAAO,GAAG,KAAa,GAAS,EAC3D,EAAW,MAAM,OAAO,OAAO,KAAK,OAAQ,EAAK,CAAM,EAC7D,OAAO,EAAgB,IAAI,WAAW,CAAQ,EAAG,CAAa",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "894DC903F2CF2D6664756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{a,b,c}from"../chunk-pqp843mh.js";import"../chunk-
|
|
1
|
+
import{a,b,c}from"../chunk-pqp843mh.js";import"../chunk-th4cwrpb.js";export{a as timingSafeEqual,c as decodeHexBytes,b as decodeBase64Bytes};
|
|
2
2
|
|
|
3
3
|
//# debugId=CA7D50FE7388E14864756E2164756E21
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DeliveryEvent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a Twilio SMS StatusCallback payload into normalized {@link DeliveryEvent}
|
|
4
|
+
* records.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parse(payload: unknown): DeliveryEvent[];
|
|
7
|
+
/**
|
|
8
|
+
* Verify a Twilio request signature (`X-Twilio-Signature`).
|
|
9
|
+
*
|
|
10
|
+
* Builds the signed string as `url + sorted(key+value for each POST param)`,
|
|
11
|
+
* HMAC-SHA1 with the Auth Token, then base64-compares to the header.
|
|
12
|
+
*
|
|
13
|
+
* @param url - Exact public URL Twilio requested (including query string).
|
|
14
|
+
* @param params - Parsed form body (or `URLSearchParams`).
|
|
15
|
+
* @param signature - Value of the `X-Twilio-Signature` header.
|
|
16
|
+
* @param authToken - Twilio Auth Token (or API Key Secret used for validation).
|
|
17
|
+
*
|
|
18
|
+
* @see https://www.twilio.com/docs/usage/security#validating-requests
|
|
19
|
+
*/
|
|
20
|
+
export declare function verifySignature(url: string, params: Record<string, string> | URLSearchParams, signature: string, authToken: string): Promise<boolean>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{a as X,b as $}from"../chunk-pqp843mh.js";import{e as M,f as O,g as V}from"../chunk-sbgt4165.js";import{da as H}from"../chunk-hnzmn4s4.js";import"../chunk-th4cwrpb.js";var E={accepted:"queued",queued:"queued",sending:"sent",sent:"sent",delivered:"delivered",undelivered:"failed",failed:"failed",receiving:"sent",received:"delivered",read:"read"};function U(D){let z=_(D);if(z===void 0)return[];let G=typeof z.MessageStatus==="string"?z.MessageStatus:typeof z.SmsStatus==="string"?z.SmsStatus:typeof z.messageStatus==="string"?z.messageStatus:"",F=typeof z.MessageSid==="string"?z.MessageSid:typeof z.SmsSid==="string"?z.SmsSid:typeof z.messageSid==="string"?z.messageSid:void 0,Q=typeof z.To==="string"?z.To:typeof z.to==="string"?z.to:void 0,Y=V(z.Timestamp??z.timestamp),Z=O(G.toLowerCase(),E);if(Z==="unknown"&&(z.ErrorCode!==void 0||z.errorCode!==void 0))Z="failed";return[M({channel:"sms",provider:"twilio-sms",type:Z,raw:D,...F!==void 0?{messageId:F}:{},...Q!==void 0?{recipient:Q}:{},...Y!==void 0?{timestamp:Y}:{}})]}async function q(D,z,G,F){if(G.length===0||F.length===0||D.length===0)return!1;let Q=z instanceof URLSearchParams?[...z.entries()]:Object.entries(z).map(([J,N])=>[J,String(N)]);Q.sort(([J],[N])=>J<N?-1:J>N?1:0);let Y=D;for(let[J,N]of Q)Y+=J+N;let Z=await crypto.subtle.importKey("raw",W(F),{name:"HMAC",hash:"SHA-1"},!1,["sign"]),j=await crypto.subtle.sign("HMAC",Z,W(Y)),L=H(new Uint8Array(j)).replace(/\r\n/g,""),C=$(L),K=$(G);if(C===null||K===null)return!1;return X(C,K)}function _(D){if(D instanceof URLSearchParams){let z={};for(let[G,F]of D.entries())z[G]=F;return z}if(typeof D==="string")try{return _(new URLSearchParams(D))}catch{return}if(typeof D==="object"&&D!==null&&!Array.isArray(D))return D;return}function W(D){let z=new TextEncoder().encode(D);return z.buffer.slice(z.byteOffset,z.byteOffset+z.byteLength)}export{q as verifySignature,U as parse};
|
|
2
|
+
|
|
3
|
+
//# debugId=016109FCAA9B82FE64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/webhooks/twilio-sms.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Twilio SMS status-callback parser → {@link DeliveryEvent}.\n *\n * Accepts Twilio's form-urlencoded StatusCallback fields (as a parsed object\n * or `URLSearchParams`) or an equivalent JSON object.\n *\n * @see https://www.twilio.com/docs/messaging/guides/track-outbound-message-status\n */\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { decodeBase64Bytes, timingSafeEqual } from \"./timing-safe-equal.js\";\nimport type { DeliveryEvent } from \"./types.js\";\nimport { deliveryEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst TWILIO_STATUS_MAP: Record<string, DeliveryEvent[\"type\"]> = {\n accepted: \"queued\",\n queued: \"queued\",\n sending: \"sent\",\n sent: \"sent\",\n delivered: \"delivered\",\n undelivered: \"failed\",\n failed: \"failed\",\n receiving: \"sent\",\n received: \"delivered\",\n read: \"read\",\n};\n\n/**\n * Parse a Twilio SMS StatusCallback payload into normalized {@link DeliveryEvent}\n * records.\n */\nexport function parse(payload: unknown): DeliveryEvent[] {\n const record = toRecord(payload);\n if (record === undefined) {\n return [];\n }\n\n const statusRaw =\n typeof record.MessageStatus === \"string\"\n ? record.MessageStatus\n : typeof record.SmsStatus === \"string\"\n ? record.SmsStatus\n : typeof record.messageStatus === \"string\"\n ? record.messageStatus\n : \"\";\n\n const messageId =\n typeof record.MessageSid === \"string\"\n ? record.MessageSid\n : typeof record.SmsSid === \"string\"\n ? record.SmsSid\n : typeof record.messageSid === \"string\"\n ? record.messageSid\n : undefined;\n\n const recipient =\n typeof record.To === \"string\"\n ? record.To\n : typeof record.to === \"string\"\n ? record.to\n : undefined;\n\n const timestamp = parseTimestamp(record.Timestamp ?? record.timestamp);\n\n // Permanent failures often include ErrorCode even when status is undelivered.\n let type = mapEventType(statusRaw.toLowerCase(), TWILIO_STATUS_MAP);\n if (type === \"unknown\" && (record.ErrorCode !== undefined || record.errorCode !== undefined)) {\n type = \"failed\";\n }\n\n return [\n deliveryEvent({\n channel: \"sms\",\n provider: \"twilio-sms\",\n type,\n raw: payload,\n ...(messageId !== undefined ? { messageId } : {}),\n ...(recipient !== undefined ? { recipient } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n }),\n ];\n}\n\n/**\n * Verify a Twilio request signature (`X-Twilio-Signature`).\n *\n * Builds the signed string as `url + sorted(key+value for each POST param)`,\n * HMAC-SHA1 with the Auth Token, then base64-compares to the header.\n *\n * @param url - Exact public URL Twilio requested (including query string).\n * @param params - Parsed form body (or `URLSearchParams`).\n * @param signature - Value of the `X-Twilio-Signature` header.\n * @param authToken - Twilio Auth Token (or API Key Secret used for validation).\n *\n * @see https://www.twilio.com/docs/usage/security#validating-requests\n */\nexport async function verifySignature(\n url: string,\n params: Record<string, string> | URLSearchParams,\n signature: string,\n authToken: string,\n): Promise<boolean> {\n if (signature.length === 0 || authToken.length === 0 || url.length === 0) {\n return false;\n }\n\n const entries =\n params instanceof URLSearchParams\n ? [...params.entries()]\n : Object.entries(params).map(([key, value]) => [key, String(value)] as const);\n\n entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n\n let data = url;\n for (const [key, value] of entries) {\n data += key + value;\n }\n\n const key = await crypto.subtle.importKey(\n \"raw\",\n encodeUtf8(authToken),\n { name: \"HMAC\", hash: \"SHA-1\" },\n false,\n [\"sign\"],\n );\n const mac = await crypto.subtle.sign(\"HMAC\", key, encodeUtf8(data));\n const expected = encodeBase64(new Uint8Array(mac)).replace(/\\r\\n/g, \"\");\n const expectedBytes = decodeBase64Bytes(expected);\n const actualBytes = decodeBase64Bytes(signature);\n if (expectedBytes === null || actualBytes === null) {\n return false;\n }\n return timingSafeEqual(expectedBytes, actualBytes);\n}\n\nfunction toRecord(payload: unknown): Record<string, unknown> | undefined {\n if (payload instanceof URLSearchParams) {\n const out: Record<string, unknown> = {};\n for (const [key, value] of payload.entries()) {\n out[key] = value;\n }\n return out;\n }\n\n if (typeof payload === \"string\") {\n try {\n return toRecord(new URLSearchParams(payload));\n } catch {\n return undefined;\n }\n }\n\n if (typeof payload === \"object\" && payload !== null && !Array.isArray(payload)) {\n return payload as Record<string, unknown>;\n }\n\n return undefined;\n}\n\nfunction encodeUtf8(text: string): ArrayBuffer {\n const bytes = new TextEncoder().encode(text);\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "6LAcA,DAAM,HAA2D,JAC/D,SAAU,SACV,OAAQ,SACR,QAAS,OACT,KAAM,OACN,UAAW,YACX,YAAa,SACb,OAAQ,SACR,UAAW,OACX,SAAU,YACV,KAAM,MACR,EAMO,SAAS,CAAK,CAAC,EAAmC,CACvD,IAAM,EAAS,EAAS,CAAO,EAC/B,GAAI,IAAW,OACb,MAAO,CAAC,EAGV,IAAM,EACJ,OAAO,EAAO,gBAAkB,SAC5B,EAAO,cACP,OAAO,EAAO,YAAc,SAC1B,EAAO,UACP,OAAO,EAAO,gBAAkB,SAC9B,EAAO,cACP,GAEJ,EACJ,OAAO,EAAO,aAAe,SACzB,EAAO,WACP,OAAO,EAAO,SAAW,SACvB,EAAO,OACP,OAAO,EAAO,aAAe,SAC3B,EAAO,WACP,OAEJ,EACJ,OAAO,EAAO,KAAO,SACjB,EAAO,GACP,OAAO,EAAO,KAAO,SACnB,EAAO,GACP,OAEF,EAAY,EAAe,EAAO,WAAa,EAAO,SAAS,EAGjE,EAAO,EAAa,EAAU,YAAY,EAAG,CAAiB,EAClE,GAAI,IAAS,YAAc,EAAO,YAAc,QAAa,EAAO,YAAc,QAChF,EAAO,SAGT,MAAO,CACL,EAAc,CACZ,QAAS,MACT,SAAU,aACV,OACA,IAAK,KACD,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,KAC3C,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,KAC3C,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,CACjD,CAAC,CACH,EAgBF,eAAsB,CAAe,CACnC,EACA,EACA,EACA,EACkB,CAClB,GAAI,EAAU,SAAW,GAAK,EAAU,SAAW,GAAK,EAAI,SAAW,EACrE,MAAO,GAGT,IAAM,EACJ,aAAkB,gBACd,CAAC,GAAG,EAAO,QAAQ,CAAC,EACpB,OAAO,QAAQ,CAAM,EAAE,IAAI,EAAE,EAAK,KAAW,CAAC,EAAK,OAAO,CAAK,CAAC,CAAU,EAEhF,EAAQ,KAAK,EAAE,IAAK,KAAQ,EAAI,EAAI,GAAK,EAAI,EAAI,EAAI,CAAE,EAEvD,IAAI,EAAO,EACX,QAAY,EAAK,KAAU,EACzB,GAAQ,EAAM,EAGhB,IAAM,EAAM,MAAM,OAAO,OAAO,UAC9B,MACA,EAAW,CAAS,EACpB,CAAE,KAAM,OAAQ,KAAM,OAAQ,EAC9B,GACA,CAAC,MAAM,CACT,EACM,EAAM,MAAM,OAAO,OAAO,KAAK,OAAQ,EAAK,EAAW,CAAI,CAAC,EAC5D,EAAW,EAAa,IAAI,WAAW,CAAG,CAAC,EAAE,QAAQ,QAAS,EAAE,EAChE,EAAgB,EAAkB,CAAQ,EAC1C,EAAc,EAAkB,CAAS,EAC/C,GAAI,IAAkB,MAAQ,IAAgB,KAC5C,MAAO,GAET,OAAO,EAAgB,EAAe,CAAW,EAGnD,SAAS,CAAQ,CAAC,EAAuD,CACvE,GAAI,aAAmB,gBAAiB,CACtC,IAAM,EAA+B,CAAC,EACtC,QAAY,EAAK,KAAU,EAAQ,QAAQ,EACzC,EAAI,GAAO,EAEb,OAAO,EAGT,GAAI,OAAO,IAAY,SACrB,GAAI,CACF,OAAO,EAAS,IAAI,gBAAgB,CAAO,CAAC,EAC5C,KAAM,CACN,OAIJ,GAAI,OAAO,IAAY,UAAY,IAAY,MAAQ,CAAC,MAAM,QAAQ,CAAO,EAC3E,OAAO,EAGT,OAGF,SAAS,CAAU,CAAC,EAA2B,CAC7C,IAAM,EAAQ,IAAI,YAAY,EAAE,OAAO,CAAI,EAC3C,OAAO,EAAM,OAAO,MAAM,EAAM,WAAY,EAAM,WAAa,EAAM,UAAU",
|
|
8
|
+
"debugId": "016109FCAA9B82FE64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/dist/webhooks/types.d.ts
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/** Messaging channel for normalized delivery events. */
|
|
2
|
+
export type DeliveryChannel = "email" | "sms" | "whatsapp" | "push";
|
|
3
|
+
/**
|
|
4
|
+
* Cross-channel delivery / lifecycle event for ingest pipelines.
|
|
5
|
+
* Email webhooks still return {@link EmailEvent}; map with {@link toDeliveryEvent}.
|
|
6
|
+
*/
|
|
7
|
+
export interface DeliveryEvent {
|
|
8
|
+
/** Channel this event belongs to. */
|
|
9
|
+
channel: DeliveryChannel;
|
|
10
|
+
/** Provider identifier (e.g. `"twilio-sms"`, `"whatsapp-cloud"`). */
|
|
11
|
+
provider: string;
|
|
12
|
+
/** Normalized lifecycle type. */
|
|
13
|
+
type: "queued" | "sent" | "delivered" | "failed" | "read" | "bounced" | "complained" | "opened" | "clicked" | "deferred" | "unknown";
|
|
14
|
+
/** Provider message identifier when available. */
|
|
15
|
+
messageId?: string;
|
|
16
|
+
/** Recipient address or phone when available. */
|
|
17
|
+
recipient?: string;
|
|
18
|
+
/** Event timestamp when available. */
|
|
19
|
+
timestamp?: Date;
|
|
20
|
+
/** Original provider payload for debugging or custom handling. */
|
|
21
|
+
raw: unknown;
|
|
22
|
+
}
|
|
1
23
|
/** Normalized email lifecycle event from any provider webhook. */
|
|
2
24
|
export interface EmailEvent {
|
|
3
25
|
/** Provider identifier (e.g. `"resend"`, `"sendgrid"`). */
|
|
@@ -13,10 +35,22 @@ export interface EmailEvent {
|
|
|
13
35
|
/** Original provider payload for debugging or custom handling. */
|
|
14
36
|
raw: unknown;
|
|
15
37
|
}
|
|
16
|
-
/** Map
|
|
17
|
-
export
|
|
38
|
+
/** Map an {@link EmailEvent} into the shared {@link DeliveryEvent} shape. */
|
|
39
|
+
export declare function toDeliveryEvent(event: EmailEvent): DeliveryEvent;
|
|
40
|
+
/** Build a {@link DeliveryEvent} respecting exact optional property types. */
|
|
41
|
+
export declare function deliveryEvent(input: {
|
|
42
|
+
channel: DeliveryChannel;
|
|
43
|
+
provider: string;
|
|
44
|
+
type: DeliveryEvent["type"];
|
|
45
|
+
raw: unknown;
|
|
46
|
+
messageId?: string | undefined;
|
|
47
|
+
recipient?: string | undefined;
|
|
48
|
+
timestamp?: Date | undefined;
|
|
49
|
+
}): DeliveryEvent;
|
|
50
|
+
/** Map a provider-specific event string to a normalized event type. */
|
|
51
|
+
export type EventTypeMapper = (event: string) => EmailEvent["type"] | DeliveryEvent["type"];
|
|
18
52
|
/** Default mapper — returns `"unknown"` for unrecognized events. */
|
|
19
|
-
export declare function mapEventType(event: string, mapping: Record<string,
|
|
53
|
+
export declare function mapEventType<T extends string>(event: string, mapping: Record<string, T>): T | "unknown";
|
|
20
54
|
/** Parse an ISO or Unix timestamp into a Date, or undefined when invalid. */
|
|
21
55
|
export declare function parseTimestamp(value: unknown): Date | undefined;
|
|
22
56
|
/** Ensure payload is an array (for providers that batch events). */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Unifonic SMS status-callback parser → {@link DeliveryEvent}.
|
|
4
|
+
*
|
|
5
|
+
* Accepts Unifonic DLR / status callback JSON (or form-like objects) with
|
|
6
|
+
* `MessageID` / `Status` fields commonly used alongside el.cloud sends.
|
|
7
|
+
*/
|
|
8
|
+
import type { DeliveryEvent } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Parse a Unifonic SMS status callback payload into normalized
|
|
11
|
+
* {@link DeliveryEvent} records.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parse(payload: unknown): DeliveryEvent[];
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{e as K,f as L,g as Q,h as V}from"../chunk-sbgt4165.js";import"../chunk-th4cwrpb.js";var Y={queued:"queued",sent:"sent",delivered:"delivered",failed:"failed",rejected:"failed",undelivered:"failed",expired:"failed"};function b(q){let W=V(q),B=[];for(let x of W){if(typeof x!=="object"||x===null)continue;let h=Z(x),X=typeof h.Status==="string"?h.Status:typeof h.status==="string"?h.status:"",G=h.MessageID!==void 0?String(h.MessageID):h.messageId!==void 0?String(h.messageId):typeof h.CorrelationID==="string"?h.CorrelationID:void 0,H=typeof h.Recipient==="string"?h.Recipient:typeof h.recipient==="string"?h.recipient:void 0,J=Q(h.TimeCreated??h.timestamp??h.Timestamp);B.push(K({channel:"sms",provider:"unifonic",type:L(X.toLowerCase(),Y),raw:x,...G!==void 0?{messageId:G}:{},...H!==void 0?{recipient:H}:{},...J!==void 0?{timestamp:J}:{}}))}return B}function Z(q){if(typeof q.data==="object"&&q.data!==null&&!Array.isArray(q.data))return q.data;return q}export{b as parse};
|
|
2
|
+
|
|
3
|
+
//# debugId=EBD1E8E48BB9255564756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/webhooks/unifonic.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Unifonic SMS status-callback parser → {@link DeliveryEvent}.\n *\n * Accepts Unifonic DLR / status callback JSON (or form-like objects) with\n * `MessageID` / `Status` fields commonly used alongside el.cloud sends.\n */\nimport type { DeliveryEvent } from \"./types.js\";\nimport { asArray, deliveryEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst UNIFONIC_STATUS_MAP: Record<string, DeliveryEvent[\"type\"]> = {\n queued: \"queued\",\n sent: \"sent\",\n delivered: \"delivered\",\n failed: \"failed\",\n rejected: \"failed\",\n undelivered: \"failed\",\n expired: \"failed\",\n};\n\n/**\n * Parse a Unifonic SMS status callback payload into normalized\n * {@link DeliveryEvent} records.\n */\nexport function parse(payload: unknown): DeliveryEvent[] {\n const items = asArray(payload);\n const events: DeliveryEvent[] = [];\n\n for (const item of items) {\n if (typeof item !== \"object\" || item === null) {\n continue;\n }\n const record = unwrapData(item as Record<string, unknown>);\n const statusRaw =\n typeof record.Status === \"string\"\n ? record.Status\n : typeof record.status === \"string\"\n ? record.status\n : \"\";\n\n const messageId =\n record.MessageID !== undefined\n ? String(record.MessageID)\n : record.messageId !== undefined\n ? String(record.messageId)\n : typeof record.CorrelationID === \"string\"\n ? record.CorrelationID\n : undefined;\n\n const recipient =\n typeof record.Recipient === \"string\"\n ? record.Recipient\n : typeof record.recipient === \"string\"\n ? record.recipient\n : undefined;\n\n const timestamp = parseTimestamp(record.TimeCreated ?? record.timestamp ?? record.Timestamp);\n\n events.push(\n deliveryEvent({\n channel: \"sms\",\n provider: \"unifonic\",\n type: mapEventType(statusRaw.toLowerCase(), UNIFONIC_STATUS_MAP),\n raw: item,\n ...(messageId !== undefined ? { messageId } : {}),\n ...(recipient !== undefined ? { recipient } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n }),\n );\n }\n\n return events;\n}\n\nfunction unwrapData(record: Record<string, unknown>): Record<string, unknown> {\n if (typeof record.data === \"object\" && record.data !== null && !Array.isArray(record.data)) {\n return record.data as Record<string, unknown>;\n }\n return record;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "gGAUA,DAAM,EAA6D,CACjE,OAAQ,SACR,KAAM,OACN,UAAW,YACX,OAAQ,SACR,SAAU,SACV,YAAa,SACb,QAAS,QACX,EAMO,SAAS,CAAK,CAAC,EAAmC,CACvD,IAAM,EAAQ,EAAQ,CAAO,EACvB,EAA0B,CAAC,EAEjC,QAAW,KAAQ,EAAO,CACxB,GAAI,OAAO,IAAS,UAAY,IAAS,KACvC,SAEF,IAAM,EAAS,EAAW,CAA+B,EACnD,EACJ,OAAO,EAAO,SAAW,SACrB,EAAO,OACP,OAAO,EAAO,SAAW,SACvB,EAAO,OACP,GAEF,EACJ,EAAO,YAAc,OACjB,OAAO,EAAO,SAAS,EACvB,EAAO,YAAc,OACnB,OAAO,EAAO,SAAS,EACvB,OAAO,EAAO,gBAAkB,SAC9B,EAAO,cACP,OAEJ,EACJ,OAAO,EAAO,YAAc,SACxB,EAAO,UACP,OAAO,EAAO,YAAc,SAC1B,EAAO,UACP,OAEF,EAAY,EAAe,EAAO,aAAe,EAAO,WAAa,EAAO,SAAS,EAE3F,EAAO,KACL,EAAc,CACZ,QAAS,MACT,SAAU,WACV,KAAM,EAAa,EAAU,YAAY,EAAG,CAAmB,EAC/D,IAAK,KACD,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,KAC3C,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,KAC3C,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,CACjD,CAAC,CACH,EAGF,OAAO,EAGT,SAAS,CAAU,CAAC,EAA0D,CAC5E,GAAI,OAAO,EAAO,OAAS,UAAY,EAAO,OAAS,MAAQ,CAAC,MAAM,QAAQ,EAAO,IAAI,EACvF,OAAO,EAAO,KAEhB,OAAO",
|
|
8
|
+
"debugId": "EBD1E8E48BB9255564756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DeliveryEvent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a WhatsApp Cloud API webhook payload into normalized
|
|
4
|
+
* {@link DeliveryEvent} records (outbound `statuses` only).
|
|
5
|
+
*/
|
|
6
|
+
export declare function parse(payload: unknown): DeliveryEvent[];
|
|
7
|
+
/**
|
|
8
|
+
* Verify a Meta WhatsApp webhook signature (`X-Hub-Signature-256`).
|
|
9
|
+
* Computes HMAC-SHA256 over the unmodified raw body with the app secret.
|
|
10
|
+
* Returns false when the header is missing or the signature does not match.
|
|
11
|
+
*
|
|
12
|
+
* @see https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verification-requests
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifySignature(payload: string, signatureHeader: string, appSecret: string): Promise<boolean>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{a as X,c as j}from"../chunk-pqp843mh.js";import{e as N,f as P,g as V,h as L}from"../chunk-sbgt4165.js";import"../chunk-th4cwrpb.js";var W={sent:"sent",delivered:"delivered",read:"read",failed:"failed",deleted:"failed"};function B(D){if(typeof D!=="object"||D===null)return[];let O=L(D.entry),G=[];for(let I of O){if(typeof I!=="object"||I===null)continue;let Q=L(I.changes);for(let J of Q){if(typeof J!=="object"||J===null)continue;let Y=J.value;if(typeof Y!=="object"||Y===null)continue;let C=Y;for(let K of L(C.statuses)){if(typeof K!=="object"||K===null)continue;let z=K,R=typeof z.status==="string"?z.status.toLowerCase():"",Z=V(typeof z.timestamp==="string"||typeof z.timestamp==="number"?z.timestamp:void 0);G.push(N({channel:"whatsapp",provider:"whatsapp-cloud",type:P(R,W),raw:K,...typeof z.id==="string"?{messageId:z.id}:{},...typeof z.recipient_id==="string"?{recipient:z.recipient_id}:{},...Z!==void 0?{timestamp:Z}:{}}))}}}return G}async function E(D,F,O){let G=/^sha256=([0-9a-fA-F]+)$/u.exec(F.trim());if(G===null||G[1]===void 0)return!1;let I=j(G[1].toLowerCase());if(I===null)return!1;let Q=await crypto.subtle.importKey("raw",$(O),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),J=await crypto.subtle.sign("HMAC",Q,$(D));return X(new Uint8Array(J),I)}function $(D){let F=new TextEncoder().encode(D);return F.buffer.slice(F.byteOffset,F.byteOffset+F.byteLength)}export{E as verifySignature,B as parse};
|
|
2
|
+
|
|
3
|
+
//# debugId=6A204006F33EEB5464756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/webhooks/whatsapp-cloud.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * WhatsApp Cloud API webhook parser → {@link DeliveryEvent}.\n *\n * Parses Meta webhook payloads containing `statuses` (outbound delivery\n * lifecycle). Inbound `messages` are ignored here — use a dedicated inbound\n * handler if you need user replies.\n *\n * @see https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/components\n */\nimport { decodeHexBytes, timingSafeEqual } from \"./timing-safe-equal.js\";\nimport type { DeliveryEvent } from \"./types.js\";\nimport { asArray, deliveryEvent, mapEventType, parseTimestamp } from \"./types.js\";\n\nconst WA_STATUS_MAP: Record<string, DeliveryEvent[\"type\"]> = {\n sent: \"sent\",\n delivered: \"delivered\",\n read: \"read\",\n failed: \"failed\",\n deleted: \"failed\",\n};\n\n/**\n * Parse a WhatsApp Cloud API webhook payload into normalized\n * {@link DeliveryEvent} records (outbound `statuses` only).\n */\nexport function parse(payload: unknown): DeliveryEvent[] {\n if (typeof payload !== \"object\" || payload === null) {\n return [];\n }\n\n const root = payload as Record<string, unknown>;\n const entries = asArray(root.entry);\n const events: DeliveryEvent[] = [];\n\n for (const entry of entries) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const changes = asArray((entry as Record<string, unknown>).changes);\n for (const change of changes) {\n if (typeof change !== \"object\" || change === null) {\n continue;\n }\n const value = (change as Record<string, unknown>).value;\n if (typeof value !== \"object\" || value === null) {\n continue;\n }\n const valueRecord = value as Record<string, unknown>;\n\n for (const status of asArray(valueRecord.statuses)) {\n if (typeof status !== \"object\" || status === null) {\n continue;\n }\n const s = status as Record<string, unknown>;\n const statusName = typeof s.status === \"string\" ? s.status.toLowerCase() : \"\";\n const timestamp = parseTimestamp(\n typeof s.timestamp === \"string\" || typeof s.timestamp === \"number\"\n ? s.timestamp\n : undefined,\n );\n\n events.push(\n deliveryEvent({\n channel: \"whatsapp\",\n provider: \"whatsapp-cloud\",\n type: mapEventType(statusName, WA_STATUS_MAP),\n raw: status,\n ...(typeof s.id === \"string\" ? { messageId: s.id } : {}),\n ...(typeof s.recipient_id === \"string\" ? { recipient: s.recipient_id } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n }),\n );\n }\n }\n }\n\n return events;\n}\n\n/**\n * Verify a Meta WhatsApp webhook signature (`X-Hub-Signature-256`).\n * Computes HMAC-SHA256 over the unmodified raw body with the app secret.\n * Returns false when the header is missing or the signature does not match.\n *\n * @see https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verification-requests\n */\nexport async function verifySignature(\n payload: string,\n signatureHeader: string,\n appSecret: string,\n): Promise<boolean> {\n const match = /^sha256=([0-9a-fA-F]+)$/u.exec(signatureHeader.trim());\n if (match === null || match[1] === undefined) {\n return false;\n }\n\n const expectedBytes = decodeHexBytes(match[1].toLowerCase());\n if (expectedBytes === null) {\n return false;\n }\n\n const key = await crypto.subtle.importKey(\n \"raw\",\n encodeUtf8(appSecret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const mac = await crypto.subtle.sign(\"HMAC\", key, encodeUtf8(payload));\n return timingSafeEqual(new Uint8Array(mac), expectedBytes);\n}\n\nfunction encodeUtf8(text: string): ArrayBuffer {\n const bytes = new TextEncoder().encode(text);\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "qJAcA,DAAM,HAAuD,CAC3D,KAAM,OACN,UAAW,YACX,KAAM,OACN,OAAQ,SACR,QAAS,QACX,EAMO,SAAS,CAAK,CAAC,EAAmC,CACvD,GAAI,OAAO,IAAY,UAAY,IAAY,KAC7C,MAAO,CAAC,EAIV,IAAM,EAAU,EADH,EACgB,KAAK,EAC5B,EAA0B,CAAC,EAEjC,QAAW,KAAS,EAAS,CAC3B,GAAI,OAAO,IAAU,UAAY,IAAU,KACzC,SAEF,IAAM,EAAU,EAAS,EAAkC,OAAO,EAClE,QAAW,KAAU,EAAS,CAC5B,GAAI,OAAO,IAAW,UAAY,IAAW,KAC3C,SAEF,IAAM,EAAS,EAAmC,MAClD,GAAI,OAAO,IAAU,UAAY,IAAU,KACzC,SAEF,IAAM,EAAc,EAEpB,QAAW,KAAU,EAAQ,EAAY,QAAQ,EAAG,CAClD,GAAI,OAAO,IAAW,UAAY,IAAW,KAC3C,SAEF,IAAM,EAAI,EACJ,EAAa,OAAO,EAAE,SAAW,SAAW,EAAE,OAAO,YAAY,EAAI,GACrE,EAAY,EAChB,OAAO,EAAE,YAAc,UAAY,OAAO,EAAE,YAAc,SACtD,EAAE,UACF,MACN,EAEA,EAAO,KACL,EAAc,CACZ,QAAS,WACT,SAAU,iBACV,KAAM,EAAa,EAAY,CAAa,EAC5C,IAAK,KACD,OAAO,EAAE,KAAO,SAAW,CAAE,UAAW,EAAE,EAAG,EAAI,CAAC,KAClD,OAAO,EAAE,eAAiB,SAAW,CAAE,UAAW,EAAE,YAAa,EAAI,CAAC,KACtE,IAAc,OAAY,CAAE,WAAU,EAAI,CAAC,CACjD,CAAC,CACH,IAKN,OAAO,EAUT,eAAsB,CAAe,CACnC,EACA,EACA,EACkB,CAClB,IAAM,EAAQ,2BAA2B,KAAK,EAAgB,KAAK,CAAC,EACpE,GAAI,IAAU,MAAQ,EAAM,KAAO,OACjC,MAAO,GAGT,IAAM,EAAgB,EAAe,EAAM,GAAG,YAAY,CAAC,EAC3D,GAAI,IAAkB,KACpB,MAAO,GAGT,IAAM,EAAM,MAAM,OAAO,OAAO,UAC9B,MACA,EAAW,CAAS,EACpB,CAAE,KAAM,OAAQ,KAAM,SAAU,EAChC,GACA,CAAC,MAAM,CACT,EACM,EAAM,MAAM,OAAO,OAAO,KAAK,OAAQ,EAAK,EAAW,CAAO,CAAC,EACrE,OAAO,EAAgB,IAAI,WAAW,CAAG,EAAG,CAAa,EAG3D,SAAS,CAAU,CAAC,EAA2B,CAC7C,IAAM,EAAQ,IAAI,YAAY,EAAE,OAAO,CAAI,EAC3C,OAAO,EAAM,OAAO,MAAM,EAAM,WAAY,EAAM,WAAa,EAAM,UAAU",
|
|
8
|
+
"debugId": "6A204006F33EEB5464756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/dist/webhooks.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module
|
|
3
|
-
* Provider webhook parsers — normalize delivery events into {@link EmailEvent}
|
|
3
|
+
* Provider webhook parsers — normalize delivery events into {@link EmailEvent}
|
|
4
|
+
* or {@link DeliveryEvent} (SMS / WhatsApp).
|
|
4
5
|
*
|
|
5
6
|
* Pure parsing only; no server framework required. Signature verification
|
|
6
7
|
* helpers are optional and never required to call `parse`.
|
|
7
8
|
*
|
|
8
9
|
* Prefer per-provider subpaths for the smallest runtime graph
|
|
9
|
-
* (`sently/webhooks/resend`, `sently/webhooks/
|
|
10
|
-
* every parser for convenience — bundlers tree-shake unused named
|
|
11
|
-
* Node/Deno without a bundler evaluate every re-export.
|
|
10
|
+
* (`sently/webhooks/resend`, `sently/webhooks/twilio-sms`, …). This barrel
|
|
11
|
+
* re-exports every parser for convenience — bundlers tree-shake unused named
|
|
12
|
+
* imports, but Node/Deno without a bundler evaluate every re-export.
|
|
12
13
|
*
|
|
13
14
|
* @example
|
|
14
15
|
* ```ts
|
|
@@ -30,4 +31,8 @@ export { parse as parseResendWebhook, verifySignature as verifyResendSignature,
|
|
|
30
31
|
export { parse as parseSendGridWebhook } from "./webhooks/sendgrid.js";
|
|
31
32
|
export { parse as parseSesWebhook } from "./webhooks/ses.js";
|
|
32
33
|
export { parse as parseSndrWebhook, verifySignature as verifySndrSignature, } from "./webhooks/sndr.js";
|
|
33
|
-
export
|
|
34
|
+
export { parse as parseTwilioSmsWebhook, verifySignature as verifyTwilioSmsSignature, } from "./webhooks/twilio-sms.js";
|
|
35
|
+
export type { DeliveryChannel, DeliveryEvent, EmailEvent } from "./webhooks/types.js";
|
|
36
|
+
export { toDeliveryEvent } from "./webhooks/types.js";
|
|
37
|
+
export { parse as parseUnifonicWebhook } from "./webhooks/unifonic.js";
|
|
38
|
+
export { parse as parseWhatsAppCloudWebhook, verifySignature as verifyWhatsAppCloudSignature, } from "./webhooks/whatsapp-cloud.js";
|
package/dist/webhooks.js
CHANGED
|
@@ -15,3 +15,14 @@ export {
|
|
|
15
15
|
parse as parseSndrWebhook,
|
|
16
16
|
verifySignature as verifySndrSignature,
|
|
17
17
|
} from "./webhooks/sndr.js";
|
|
18
|
+
export {
|
|
19
|
+
parse as parseTwilioSmsWebhook,
|
|
20
|
+
verifySignature as verifyTwilioSmsSignature,
|
|
21
|
+
} from "./webhooks/twilio-sms.js";
|
|
22
|
+
|
|
23
|
+
export { toDeliveryEvent } from "./webhooks/types.js";
|
|
24
|
+
export { parse as parseUnifonicWebhook } from "./webhooks/unifonic.js";
|
|
25
|
+
export {
|
|
26
|
+
parse as parseWhatsAppCloudWebhook,
|
|
27
|
+
verifySignature as verifyWhatsAppCloudSignature,
|
|
28
|
+
} from "./webhooks/whatsapp-cloud.js";
|
package/dist/whatsapp.d.ts
CHANGED
|
@@ -22,5 +22,6 @@ export interface WhatsAppSender {
|
|
|
22
22
|
* Create a WhatsApp sender that wraps a {@link WhatsAppTransport}.
|
|
23
23
|
*
|
|
24
24
|
* Pipeline: plugins → onSend → transport.send → onSuccess / onError.
|
|
25
|
+
* `onRetry` / `onFallback` wire when the transport is a retry or fallback decorator.
|
|
25
26
|
*/
|
|
26
27
|
export declare function createWhatsAppSender(config: WhatsAppSenderConfig): WhatsAppSender;
|