sently 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +34 -0
- package/CHANGELOG.md +49 -1
- package/README.md +89 -776
- package/dist/adapters/bun.js +1 -1
- package/dist/adapters/cf.js +2 -2
- package/dist/adapters/cf.js.map +2 -2
- package/dist/adapters/deno.js +1 -1
- package/dist/adapters/node.js +1 -1
- package/dist/auth/oauth2.js +2 -2
- package/dist/auth/oauth2.js.map +1 -1
- package/dist/chunk-0gqy32pe.js +4 -0
- package/dist/chunk-0gqy32pe.js.map +10 -0
- package/dist/{chunk-kebhy657.js → chunk-0qxws3kj.js} +2 -2
- package/dist/{chunk-kebhy657.js.map → chunk-0qxws3kj.js.map} +1 -1
- package/dist/chunk-29z7fkzy.js +4 -0
- package/dist/{chunk-74rha3ac.js.map → chunk-29z7fkzy.js.map} +2 -2
- package/dist/chunk-5915vbcj.js +4 -0
- package/dist/chunk-5915vbcj.js.map +10 -0
- package/dist/{chunk-keksa6fx.js → chunk-6npp3x3c.js} +2 -2
- package/dist/{chunk-keksa6fx.js.map → chunk-6npp3x3c.js.map} +1 -1
- package/dist/chunk-7gy2q9sh.js +4 -0
- package/dist/chunk-7gy2q9sh.js.map +10 -0
- package/dist/{chunk-gkkv7yqm.js → chunk-8dty8c4p.js} +3 -3
- package/dist/{chunk-gkkv7yqm.js.map → chunk-8dty8c4p.js.map} +2 -2
- package/dist/{chunk-qp4y0kp2.js → chunk-8njyga62.js} +2 -2
- package/dist/{chunk-qp4y0kp2.js.map → chunk-8njyga62.js.map} +1 -1
- package/dist/{chunk-gh5n759p.js → chunk-93vqxxj2.js} +3 -3
- package/dist/{chunk-gh5n759p.js.map → chunk-93vqxxj2.js.map} +1 -1
- package/dist/chunk-b602dhck.js +4 -0
- package/dist/{chunk-rm78s1ek.js.map → chunk-b602dhck.js.map} +2 -2
- package/dist/chunk-gp4fwrs8.js +4 -0
- package/dist/chunk-gp4fwrs8.js.map +10 -0
- package/dist/chunk-n0qeyzqm.js +13 -0
- package/dist/{chunk-cvrbfke9.js.map → chunk-n0qeyzqm.js.map} +2 -2
- package/dist/chunk-qtkd5bak.js +4 -0
- package/dist/chunk-qtkd5bak.js.map +11 -0
- package/dist/chunk-skqhj0rm.js +4 -0
- package/dist/{chunk-r7ee9tk7.js.map → chunk-skqhj0rm.js.map} +2 -2
- package/dist/{chunk-a1c87hn1.js → chunk-tamww15j.js} +2 -2
- package/dist/{chunk-a1c87hn1.js.map → chunk-tamww15j.js.map} +1 -1
- package/dist/chunk-vjpds2pf.js +3 -0
- package/dist/chunk-vjpds2pf.js.map +10 -0
- package/dist/chunk-yp311efr.js +4 -0
- package/dist/chunk-yp311efr.js.map +10 -0
- package/dist/chunk-ywq6s10g.js +5 -0
- package/dist/{chunk-d5rwm3br.js.map → chunk-ywq6s10g.js.map} +3 -3
- package/dist/core/base64.d.ts +10 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/core/hooks.d.ts +14 -0
- package/dist/core/plugin.d.ts +7 -7
- package/dist/core/provider-label.d.ts +6 -0
- package/dist/core/push-endpoint.d.ts +29 -0
- package/dist/core/push-types.d.ts +98 -0
- package/dist/core/sms-types.d.ts +76 -0
- package/dist/core/smtp.js +2 -2
- package/dist/core/smtp.js.map +1 -1
- package/dist/core/types.d.ts +27 -4
- package/dist/core/whatsapp-types.d.ts +116 -0
- package/dist/detect.js +2 -2
- package/dist/detect.js.map +1 -1
- package/dist/dkim.js +2 -2
- package/dist/dkim.js.map +2 -2
- package/dist/idempotency.d.ts +1 -0
- package/dist/idempotency.js +2 -2
- package/dist/idempotency.js.map +3 -3
- package/dist/index.d.ts +24 -2
- package/dist/index.js +23 -0
- package/dist/mailer.js +2 -2
- package/dist/mailer.js.map +1 -1
- package/dist/observability/console.d.ts +12 -0
- package/dist/observability/console.js +3 -0
- package/dist/observability/console.js.map +10 -0
- package/dist/plugins/react.js +2 -2
- package/dist/plugins/react.js.map +2 -2
- package/dist/plugins/template.js +1 -1
- package/dist/pool/pool.js +2 -2
- package/dist/pool/pool.js.map +1 -1
- package/dist/push.d.ts +26 -0
- package/dist/push.js +3 -0
- package/dist/push.js.map +10 -0
- package/dist/sms.d.ts +26 -0
- package/dist/sms.js +3 -0
- package/dist/sms.js.map +10 -0
- package/dist/smtp-mailer.js +2 -2
- package/dist/smtp-mailer.js.map +2 -2
- package/dist/transports/brevo.d.ts +1 -0
- package/dist/transports/brevo.js +2 -2
- package/dist/transports/brevo.js.map +3 -3
- package/dist/transports/cloudflare-email.d.ts +58 -0
- package/dist/transports/cloudflare-email.js +3 -0
- package/dist/transports/cloudflare-email.js.map +10 -0
- package/dist/transports/fallback.d.ts +100 -0
- package/dist/transports/fallback.js +3 -0
- package/dist/transports/fallback.js.map +9 -0
- package/dist/transports/loops.d.ts +32 -0
- package/dist/transports/loops.js +3 -0
- package/dist/transports/loops.js.map +10 -0
- package/dist/transports/mailersend.d.ts +27 -0
- package/dist/transports/mailersend.js +3 -0
- package/dist/transports/mailersend.js.map +10 -0
- package/dist/transports/mailgun.d.ts +1 -0
- package/dist/transports/mailgun.js +2 -2
- package/dist/transports/mailgun.js.map +3 -3
- package/dist/transports/mailtrap.d.ts +36 -0
- package/dist/transports/mailtrap.js +3 -0
- package/dist/transports/mailtrap.js.map +10 -0
- package/dist/transports/msegat.d.ts +132 -0
- package/dist/transports/msegat.js +3 -0
- package/dist/transports/msegat.js.map +10 -0
- package/dist/transports/plunk.d.ts +29 -0
- package/dist/transports/plunk.js +3 -0
- package/dist/transports/plunk.js.map +10 -0
- package/dist/transports/postmark.d.ts +1 -0
- package/dist/transports/postmark.js +2 -2
- package/dist/transports/postmark.js.map +3 -3
- package/dist/transports/preview.d.ts +1 -0
- package/dist/transports/preview.js +2 -2
- package/dist/transports/preview.js.map +3 -3
- package/dist/transports/resend.d.ts +1 -0
- package/dist/transports/resend.js +2 -2
- package/dist/transports/resend.js.map +3 -3
- package/dist/transports/retry.d.ts +1 -0
- package/dist/transports/retry.js +2 -2
- package/dist/transports/retry.js.map +3 -3
- package/dist/transports/sendgrid.d.ts +1 -0
- package/dist/transports/sendgrid.js +2 -2
- package/dist/transports/sendgrid.js.map +3 -3
- package/dist/transports/ses.d.ts +1 -0
- package/dist/transports/ses.js +3 -3
- package/dist/transports/ses.js.map +3 -3
- package/dist/transports/smtp.d.ts +1 -0
- package/dist/transports/smtp.js +2 -2
- package/dist/transports/smtp.js.map +1 -1
- package/dist/transports/sndr.d.ts +41 -0
- package/dist/transports/sndr.js +3 -0
- package/dist/transports/sndr.js.map +10 -0
- package/dist/transports/sparkpost.d.ts +30 -0
- package/dist/transports/sparkpost.js +3 -0
- package/dist/transports/sparkpost.js.map +10 -0
- package/dist/transports/taqnyat-mail.d.ts +33 -0
- package/dist/transports/taqnyat-mail.js +3 -0
- package/dist/transports/taqnyat-mail.js.map +10 -0
- package/dist/transports/taqnyat-phone.d.ts +4 -0
- package/dist/transports/taqnyat-sms.d.ts +131 -0
- package/dist/transports/taqnyat-sms.js +3 -0
- package/dist/transports/taqnyat-sms.js.map +10 -0
- package/dist/transports/taqnyat-whatsapp.d.ts +51 -0
- package/dist/transports/taqnyat-whatsapp.js +3 -0
- package/dist/transports/taqnyat-whatsapp.js.map +10 -0
- package/dist/transports/twilio-sms.d.ts +66 -0
- package/dist/transports/twilio-sms.js +3 -0
- package/dist/transports/twilio-sms.js.map +10 -0
- package/dist/transports/webpush.d.ts +46 -0
- package/dist/transports/webpush.js +3 -0
- package/dist/transports/webpush.js.map +10 -0
- package/dist/transports/weighted-fallback.d.ts +65 -0
- package/dist/transports/weighted-fallback.js +3 -0
- package/dist/transports/weighted-fallback.js.map +10 -0
- package/dist/transports/whatsapp-cloud.d.ts +56 -0
- package/dist/transports/whatsapp-cloud.js +3 -0
- package/dist/transports/whatsapp-cloud.js.map +10 -0
- package/dist/webhooks/brevo.js +1 -1
- package/dist/webhooks/mailgun.js +1 -1
- package/dist/webhooks/postmark.js +1 -1
- package/dist/webhooks/resend.js +1 -1
- package/dist/webhooks/sendgrid.js +1 -1
- package/dist/webhooks/ses.js +1 -1
- package/dist/webhooks/sndr.d.ts +16 -0
- package/dist/webhooks/sndr.js +3 -0
- package/dist/webhooks/sndr.js.map +10 -0
- package/dist/webhooks/timing-safe-equal.js +1 -1
- package/dist/webhooks.d.ts +9 -2
- package/dist/webhooks.js +4 -0
- package/dist/whatsapp.d.ts +26 -0
- package/dist/whatsapp.js +3 -0
- package/dist/whatsapp.js.map +10 -0
- package/package.json +121 -4
- package/site/README.md +26 -0
- package/site/content/docs/ai/index.mdx +13 -0
- package/site/content/docs/ai/llms-txt.mdx +12 -0
- package/site/content/docs/ai/mcp.mdx +16 -0
- package/site/content/docs/ai/meta.json +9 -0
- package/site/content/docs/channels/email.mdx +100 -0
- package/site/content/docs/channels/hooks.mdx +87 -0
- package/site/content/docs/channels/index.mdx +81 -0
- package/site/content/docs/channels/meta.json +12 -0
- package/site/content/docs/channels/push.mdx +92 -0
- package/site/content/docs/channels/sms.mdx +88 -0
- package/site/content/docs/channels/whatsapp.mdx +95 -0
- package/site/content/docs/decorators/fallback.mdx +34 -0
- package/site/content/docs/decorators/idempotency.mdx +34 -0
- package/site/content/docs/decorators/index.mdx +50 -0
- package/site/content/docs/decorators/meta.json +12 -0
- package/site/content/docs/decorators/preview.mdx +34 -0
- package/site/content/docs/decorators/retry.mdx +34 -0
- package/site/content/docs/decorators/weighted-fallback.mdx +34 -0
- package/site/content/docs/get-started/entrypoints.mdx +114 -0
- package/site/content/docs/get-started/index.mdx +14 -0
- package/site/content/docs/get-started/installation.mdx +78 -0
- package/site/content/docs/get-started/introduction.mdx +77 -0
- package/site/content/docs/get-started/meta.json +12 -0
- package/site/content/docs/get-started/migrate-nodemailer.mdx +23 -0
- package/site/content/docs/get-started/runtimes.mdx +17 -0
- package/site/content/docs/guides/adapters.mdx +26 -0
- package/site/content/docs/guides/attachments.mdx +19 -0
- package/site/content/docs/guides/bundle-size.mdx +78 -0
- package/site/content/docs/guides/dkim.mdx +26 -0
- package/site/content/docs/guides/index.mdx +15 -0
- package/site/content/docs/guides/meta.json +21 -0
- package/site/content/docs/guides/oauth2.mdx +23 -0
- package/site/content/docs/guides/observability.mdx +16 -0
- package/site/content/docs/guides/plugins-template.mdx +22 -0
- package/site/content/docs/guides/pool.mdx +24 -0
- package/site/content/docs/guides/react-email.mdx +20 -0
- package/site/content/docs/guides/security.mdx +17 -0
- package/site/content/docs/guides/send-bulk.mdx +18 -0
- package/site/content/docs/guides/vendor-extras-otp.mdx +18 -0
- package/site/content/docs/guides/webhooks.mdx +120 -0
- package/site/content/docs/guides/webpush-interop.mdx +18 -0
- package/site/content/docs/index.mdx +15 -0
- package/site/content/docs/meta.json +15 -0
- package/site/content/docs/quick-start/email.mdx +40 -0
- package/site/content/docs/quick-start/index.mdx +15 -0
- package/site/content/docs/quick-start/meta.json +5 -0
- package/site/content/docs/quick-start/push.mdx +36 -0
- package/site/content/docs/quick-start/sms.mdx +36 -0
- package/site/content/docs/quick-start/whatsapp.mdx +38 -0
- package/site/content/docs/reference/errors.mdx +22 -0
- package/site/content/docs/reference/exports.mdx +83 -0
- package/site/content/docs/reference/index.mdx +15 -0
- package/site/content/docs/reference/mail-options.mdx +22 -0
- package/site/content/docs/reference/meta.json +15 -0
- package/site/content/docs/reference/push-options.mdx +20 -0
- package/site/content/docs/reference/sms-options.mdx +17 -0
- package/site/content/docs/reference/transport-contracts.mdx +17 -0
- package/site/content/docs/reference/webhook-events.mdx +70 -0
- package/site/content/docs/reference/whatsapp-options.mdx +15 -0
- package/site/content/docs/transports/brevo.mdx +40 -0
- package/site/content/docs/transports/cloudflare-email.mdx +40 -0
- package/site/content/docs/transports/index.mdx +105 -0
- package/site/content/docs/transports/loops.mdx +41 -0
- package/site/content/docs/transports/mailersend.mdx +40 -0
- package/site/content/docs/transports/mailgun.mdx +42 -0
- package/site/content/docs/transports/mailtrap.mdx +42 -0
- package/site/content/docs/transports/meta.json +32 -0
- package/site/content/docs/transports/msegat.mdx +42 -0
- package/site/content/docs/transports/plunk.mdx +40 -0
- package/site/content/docs/transports/postmark.mdx +40 -0
- package/site/content/docs/transports/resend.mdx +43 -0
- package/site/content/docs/transports/sendgrid.mdx +40 -0
- package/site/content/docs/transports/ses.mdx +44 -0
- package/site/content/docs/transports/smtp.mdx +47 -0
- package/site/content/docs/transports/sndr.mdx +144 -0
- package/site/content/docs/transports/sparkpost.mdx +41 -0
- package/site/content/docs/transports/taqnyat-mail.mdx +41 -0
- package/site/content/docs/transports/taqnyat-sms.mdx +41 -0
- package/site/content/docs/transports/taqnyat-whatsapp.mdx +40 -0
- package/site/content/docs/transports/twilio-sms.mdx +128 -0
- package/site/content/docs/transports/webpush.mdx +43 -0
- package/site/content/docs/transports/whatsapp-cloud.mdx +42 -0
- package/dist/chunk-4mvp3b85.js +0 -3
- package/dist/chunk-4mvp3b85.js.map +0 -10
- package/dist/chunk-74rha3ac.js +0 -4
- package/dist/chunk-cvrbfke9.js +0 -13
- package/dist/chunk-d5rwm3br.js +0 -5
- package/dist/chunk-ka6t8g77.js +0 -4
- package/dist/chunk-ka6t8g77.js.map +0 -11
- package/dist/chunk-r7ee9tk7.js +0 -4
- package/dist/chunk-rm78s1ek.js +0 -4
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/taqnyat-sms.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Taqnyat SMS API transport (Saudi Arabia).\n *\n * Request/response shapes follow\n * [Taqnyat SMS](https://dev.taqnyat.sa/en/doc/sms/) and\n * [Verify](https://dev.taqnyat.sa/en/doc/verify/):\n * - SMS: `POST /v1/messages` with Bearer + JSON (`recipients`, `body`, `sender`)\n * - OTP: `POST /verify.php/` with JSON array (`apiKey`, `numbers`, `method`, …)\n *\n * Sently-first: wire SMS into {@link createSmsSender}; OTP helpers stay on this class.\n *\n * @example\n * ```ts\n * import { createSmsSender } from \"sently/sms\";\n * import { TaqnyatSmsTransport } from \"sently/transports/taqnyat-sms\";\n *\n * const taqnyat = new TaqnyatSmsTransport({\n * bearerToken: process.env.TAQNYAT_TOKEN!,\n * sender: \"MyBrand\",\n * });\n * const sms = createSmsSender({ transport: taqnyat });\n *\n * await sms.send({ to: \"+9665xxxxxxxx\", body: \"Hello\" });\n * await taqnyat.sendOtp({ to: \"+9665xxxxxxxx\", requestId: \"login-1\", lang: \"en\" });\n * await taqnyat.verifyOtp({\n * to: \"+9665xxxxxxxx\",\n * requestId: \"login-1\",\n * code: \"6240\",\n * lang: \"en\",\n * });\n * ```\n */\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type { SmsOptions, SmsSendResult, SmsTransport } from \"../core/sms-types.js\";\nimport type { VerifyResult } from \"../core/types.js\";\nimport { normalizeTaqnyatPhone } from \"./taqnyat-phone.js\";\n\n/** Taqnyat SMS API configuration. */\nexport interface TaqnyatSmsConfig {\n /** Bearer token from the Taqnyat dashboard. */\n bearerToken: string;\n /** Pre-approved sender name. */\n sender: string;\n}\n\n/** Options for {@link TaqnyatSmsTransport.sendOtp}. */\nexport interface TaqnyatSendOtpOptions {\n /** Recipient phone number (E.164 or international digits). */\n to: string;\n /** Unique id for this verification flow (required again on {@link verifyOtp}). */\n requestId: string;\n /** Message language (`en` or `ar`). Defaults to `\"ar\"` per Taqnyat docs. */\n lang?: \"en\" | \"ar\";\n /** Optional note appended to the OTP SMS. */\n note?: string;\n /** Sender ID override; defaults to transport `sender`. */\n from?: string;\n}\n\n/** Result of a successful {@link TaqnyatSmsTransport.sendOtp} call. */\nexport interface TaqnyatOtpSendResult {\n /** Echo of {@link TaqnyatSendOtpOptions.requestId}. */\n requestId: string;\n /** Recipient as passed in. */\n to: string;\n /** Provider status code (`5` = code sent). */\n code: number;\n /** Raw response body text. */\n response: string;\n /** Provider identifier. */\n provider: \"taqnyat-sms\";\n}\n\n/** Options for {@link TaqnyatSmsTransport.verifyOtp}. */\nexport interface TaqnyatVerifyOtpOptions {\n /** Recipient phone number (same as send). */\n to: string;\n /** Same {@link TaqnyatSendOtpOptions.requestId} used when sending. */\n requestId: string;\n /** OTP code the user entered (`activeKey` in Taqnyat docs). */\n code: string;\n /** Message language (`en` or `ar`). Defaults to `\"ar\"`. */\n lang?: \"en\" | \"ar\";\n /** Sender ID override; defaults to transport `sender`. */\n from?: string;\n /** Optional note (documented on check OTP). */\n note?: string;\n}\n\n/** Result of a successful {@link TaqnyatSmsTransport.verifyOtp} call. */\nexport interface TaqnyatOtpVerifyResult {\n /** Always `true` when the call resolves (failures throw). */\n ok: true;\n /** Provider status code (`10` = completed, `13`/`19` = already verified). */\n code: number;\n /** Provider message when present. */\n message: string;\n /** Raw response body text. */\n response: string;\n /** Provider identifier. */\n provider: \"taqnyat-sms\";\n}\n\n/** Error thrown when the Taqnyat SMS / Verify API returns a non-success response. */\nexport class TaqnyatSmsError extends SentlyError {\n /** Creates a Taqnyat SMS API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(\n message,\n httpStatusToSentlyCode(statusCode >= 100 && statusCode < 600 ? statusCode : 400),\n {\n statusCode,\n provider: \"taqnyat-sms\",\n cause: apiError,\n },\n );\n this.name = \"TaqnyatSmsError\";\n }\n}\n\nfunction verifyPayloadCode(payload: Record<string, unknown>): number | undefined {\n const raw = payload.code ?? payload.statusCode ?? payload.status;\n if (typeof raw === \"number\") return raw;\n if (typeof raw === \"string\" && /^-?\\d+$/.test(raw)) return Number(raw);\n return undefined;\n}\n\nfunction verifyPayloadMessage(payload: Record<string, unknown>): string | undefined {\n if (typeof payload.message === \"string\") return payload.message;\n if (typeof payload.MessageEn === \"string\") return payload.MessageEn;\n return undefined;\n}\n\nfunction unwrapVerifyPayload(payload: unknown): Record<string, unknown> {\n if (Array.isArray(payload)) {\n const first = payload[0];\n return first && typeof first === \"object\" ? (first as Record<string, unknown>) : {};\n }\n if (payload && typeof payload === \"object\") {\n return payload as Record<string, unknown>;\n }\n return {};\n}\n\n/**\n * Taqnyat SMS transport via JSON Bearer API, plus Verify OTP helpers.\n */\nexport class TaqnyatSmsTransport implements SmsTransport {\n readonly provider = \"taqnyat-sms\";\n\n private readonly bearerToken: string;\n private readonly sender: string;\n\n /** Creates a Taqnyat SMS transport. */\n constructor(config: TaqnyatSmsConfig) {\n this.bearerToken = config.bearerToken;\n this.sender = config.sender;\n }\n\n /**\n * Sends an SMS via `POST https://api.taqnyat.sa/v1/messages`\n * (Bearer + JSON: `recipients`, `body`, `sender`; optional `smsId`).\n */\n async send(options: SmsOptions): Promise<SmsSendResult> {\n const recipients = [normalizeTaqnyatPhone(options.to)];\n const body: Record<string, unknown> = {\n recipients,\n body: options.body,\n sender: options.from ?? this.sender,\n };\n // Official PHP SDK maps a client id to `smsId`.\n if (options.messageId !== undefined) {\n body.smsId = options.messageId;\n }\n\n const response = await fetch(\"https://api.taqnyat.sa/v1/messages\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.bearerToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const payload = (await response.json()) as {\n statusCode?: number;\n messageId?: number;\n cost?: number;\n currency?: string;\n message?: string;\n accepted?: string;\n rejected?: string;\n };\n\n // Docs: successful send returns HTTP 201 with statusCode 201.\n if (response.status !== 201) {\n throw new TaqnyatSmsError(\n payload.message ?? \"Taqnyat SMS API error\",\n payload.statusCode ?? response.status,\n payload,\n );\n }\n\n return {\n messageId: String(payload.messageId ?? options.messageId ?? \"\"),\n to: options.to,\n status: \"accepted\",\n response: `cost: ${payload.cost ?? \"?\"} ${payload.currency ?? \"SAR\"}`,\n provider: \"taqnyat-sms\",\n };\n }\n\n /**\n * Vendor extra: generate/send OTP via `POST /verify.php/` (success code `5`).\n * Not part of {@link SmsTransport} — call on {@link TaqnyatSmsTransport} directly.\n */\n async sendOtp(options: TaqnyatSendOtpOptions): Promise<TaqnyatOtpSendResult> {\n const lang = options.lang ?? \"ar\";\n const number = normalizeTaqnyatPhone(options.to);\n const body = [\n {\n apiKey: this.bearerToken,\n numbers: [number],\n method: \"sms\",\n sender: options.from ?? this.sender,\n lang,\n requestId: options.requestId,\n returnJson: 1,\n ...(options.note !== undefined ? { note: options.note } : {}),\n },\n ];\n\n const response = await fetch(\"https://api.taqnyat.sa/verify.php/\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.bearerToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const text = await response.text();\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new TaqnyatSmsError(\"Taqnyat Verify returned non-JSON response\", response.status, text);\n }\n\n const result = unwrapVerifyPayload(parsed);\n const code = verifyPayloadCode(result);\n\n // Docs: 5 = Activation code sent successfully.\n if (!response.ok || code !== 5) {\n throw new TaqnyatSmsError(\n verifyPayloadMessage(result) ?? `Taqnyat OTP send failed (code ${code ?? \"unknown\"})`,\n code ?? response.status,\n result,\n );\n }\n\n return {\n requestId: options.requestId,\n to: options.to,\n code,\n response: text,\n provider: \"taqnyat-sms\",\n };\n }\n\n /**\n * Vendor extra: check OTP via `POST /verify.php/` with `activeKey`\n * (`10` = completed; `13` / `19` = already verified).\n */\n async verifyOtp(options: TaqnyatVerifyOtpOptions): Promise<TaqnyatOtpVerifyResult> {\n const lang = options.lang ?? \"ar\";\n const number = normalizeTaqnyatPhone(options.to);\n const body = [\n {\n apiKey: this.bearerToken,\n numbers: [number],\n method: \"sms\",\n sender: options.from ?? this.sender,\n lang,\n requestId: options.requestId,\n activeKey: options.code,\n returnJson: 1,\n ...(options.note !== undefined ? { note: options.note } : {}),\n },\n ];\n\n const response = await fetch(\"https://api.taqnyat.sa/verify.php/\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.bearerToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const text = await response.text();\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new TaqnyatSmsError(\"Taqnyat Verify returned non-JSON response\", response.status, text);\n }\n\n const result = unwrapVerifyPayload(parsed);\n const code = verifyPayloadCode(result);\n const okCodes = new Set([10, 13, 19]);\n\n if (!response.ok || code === undefined || !okCodes.has(code)) {\n throw new TaqnyatSmsError(\n verifyPayloadMessage(result) ?? `Taqnyat OTP verify failed (code ${code ?? \"unknown\"})`,\n code ?? response.status,\n result,\n );\n }\n\n return {\n ok: true,\n code,\n message: verifyPayloadMessage(result) ?? \"Activation process completed successfully\",\n response: text,\n provider: \"taqnyat-sms\",\n };\n }\n\n /** Lightweight credential check. */\n async verify(): Promise<VerifyResult> {\n return {\n ok: Boolean(this.bearerToken && this.sender),\n provider: \"taqnyat-sms\",\n message: this.bearerToken && this.sender ? \"Credentials present\" : \"Missing credentials\",\n };\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "18DB31EDCA321AE964756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Taqnyat WhatsApp Business API transport.
|
|
4
|
+
*
|
|
5
|
+
* Follows [Taqnyat WhatsApp](https://dev.taqnyat.sa/en/doc/whatsapp/):
|
|
6
|
+
* `POST https://api.taqnyat.sa/wa/v2/messages/` with Bearer + JSON.
|
|
7
|
+
* Template: `{ to, type: "template", template: { name, language: { code } } }`.
|
|
8
|
+
* Session text: `{ to, type: "text", text: { body } }`.
|
|
9
|
+
*
|
|
10
|
+
* Sently-first: wire into {@link createWhatsAppSender}.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { createWhatsAppSender } from "sently/whatsapp";
|
|
15
|
+
* import { TaqnyatWhatsAppTransport } from "sently/transports/taqnyat-whatsapp";
|
|
16
|
+
*
|
|
17
|
+
* const wa = createWhatsAppSender({
|
|
18
|
+
* transport: new TaqnyatWhatsAppTransport({
|
|
19
|
+
* bearerToken: process.env.TAQNYAT_WHATSAPP_TOKEN!,
|
|
20
|
+
* }),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import { SentlyError } from "../core/errors.js";
|
|
25
|
+
import type { VerifyResult } from "../core/types.js";
|
|
26
|
+
import type { WhatsAppOptions, WhatsAppSendResult, WhatsAppTransport } from "../core/whatsapp-types.js";
|
|
27
|
+
/** Taqnyat WhatsApp API configuration. */
|
|
28
|
+
export interface TaqnyatWhatsAppConfig {
|
|
29
|
+
/** Bearer token from a Taqnyat application enabled for WhatsApp. */
|
|
30
|
+
bearerToken: string;
|
|
31
|
+
}
|
|
32
|
+
/** Error thrown when the Taqnyat WhatsApp API reports a failure. */
|
|
33
|
+
export declare class TaqnyatWhatsAppError extends SentlyError {
|
|
34
|
+
readonly statusCode: number;
|
|
35
|
+
readonly apiError: unknown;
|
|
36
|
+
/** Creates a Taqnyat WhatsApp API error with status code and response payload. */
|
|
37
|
+
constructor(message: string, statusCode: number, apiError: unknown);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Taqnyat WhatsApp transport via JSON Bearer `wa/v2/messages/`.
|
|
41
|
+
*/
|
|
42
|
+
export declare class TaqnyatWhatsAppTransport implements WhatsAppTransport {
|
|
43
|
+
readonly provider = "taqnyat-whatsapp";
|
|
44
|
+
private readonly bearerToken;
|
|
45
|
+
/** Creates a Taqnyat WhatsApp transport. */
|
|
46
|
+
constructor(config: TaqnyatWhatsAppConfig);
|
|
47
|
+
/** Sends a WhatsApp template or session text message. */
|
|
48
|
+
send(options: WhatsAppOptions): Promise<WhatsAppSendResult>;
|
|
49
|
+
/** Lightweight credential check. */
|
|
50
|
+
verify(): Promise<VerifyResult>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{h as O}from"../chunk-7gy2q9sh.js";import{ca as K,da as N}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class Q extends K{statusCode;apiError;constructor(B,D,G){super(B,N(D>=100&&D<600?D:400),{statusCode:D,provider:"taqnyat-whatsapp",cause:G});this.statusCode=D;this.apiError=G;this.name="TaqnyatWhatsAppError"}}function V(B){return"template"in B}function X(B){let D=B.statuses;if(Array.isArray(D)){let G=D[0];if(typeof G?.message_id==="string")return G.message_id}else if(D&&typeof D==="object"){for(let G of Object.values(D))if(G&&typeof G==="object"){let H=G.message_id;if(typeof H==="string")return H}}return}class Y{provider="taqnyat-whatsapp";bearerToken;constructor(B){this.bearerToken=B.bearerToken}async send(B){let D=O(B.to),G=V(B)?{to:D,type:"template",template:{name:B.template.name,language:{code:B.template.language},...B.template.components?{components:B.template.components}:{}}}:{to:D,type:"text",text:{body:B.text}},H=await fetch("https://api.taqnyat.sa/wa/v2/messages/",{method:"POST",headers:{Authorization:`Bearer ${this.bearerToken}`,"Content-Type":"application/json"},body:JSON.stringify(G)}),F=await H.json(),J=X(F);if(!H.ok||F.message!==void 0&&J===void 0){let R=typeof F.reason==="string"?F.reason:typeof F.message==="string"?`Taqnyat WhatsApp error ${F.message}`:"Taqnyat WhatsApp API error",U=typeof F.message==="string"&&/^\d+$/.test(F.message)?Number(F.message):H.status;throw new Q(R,U||H.status,F)}return{messageId:J??B.messageId??"",to:B.to,status:"accepted",response:typeof F.type==="string"?F.type:"Message sent",provider:"taqnyat-whatsapp"}}async verify(){return{ok:Boolean(this.bearerToken),provider:"taqnyat-whatsapp",message:this.bearerToken?"Credentials present":"Missing credentials"}}}export{Y as TaqnyatWhatsAppTransport,Q as TaqnyatWhatsAppError};
|
|
2
|
+
|
|
3
|
+
//# debugId=5657D7720E9F16CB64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/taqnyat-whatsapp.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Taqnyat WhatsApp Business API transport.\n *\n * Follows [Taqnyat WhatsApp](https://dev.taqnyat.sa/en/doc/whatsapp/):\n * `POST https://api.taqnyat.sa/wa/v2/messages/` with Bearer + JSON.\n * Template: `{ to, type: \"template\", template: { name, language: { code } } }`.\n * Session text: `{ to, type: \"text\", text: { body } }`.\n *\n * Sently-first: wire into {@link createWhatsAppSender}.\n *\n * @example\n * ```ts\n * import { createWhatsAppSender } from \"sently/whatsapp\";\n * import { TaqnyatWhatsAppTransport } from \"sently/transports/taqnyat-whatsapp\";\n *\n * const wa = createWhatsAppSender({\n * transport: new TaqnyatWhatsAppTransport({\n * bearerToken: process.env.TAQNYAT_WHATSAPP_TOKEN!,\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\";\nimport { normalizeTaqnyatPhone } from \"./taqnyat-phone.js\";\n\n/** Taqnyat WhatsApp API configuration. */\nexport interface TaqnyatWhatsAppConfig {\n /** Bearer token from a Taqnyat application enabled for WhatsApp. */\n bearerToken: string;\n}\n\n/** Error thrown when the Taqnyat WhatsApp API reports a failure. */\nexport class TaqnyatWhatsAppError extends SentlyError {\n /** Creates a Taqnyat WhatsApp API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(\n message,\n httpStatusToSentlyCode(statusCode >= 100 && statusCode < 600 ? statusCode : 400),\n {\n statusCode,\n provider: \"taqnyat-whatsapp\",\n cause: apiError,\n },\n );\n this.name = \"TaqnyatWhatsAppError\";\n }\n}\n\nfunction isTemplateMessage(\n options: WhatsAppOptions,\n): options is Extract<WhatsAppOptions, { template: unknown }> {\n return \"template\" in options;\n}\n\nfunction extractMessageId(payload: Record<string, unknown>): string | undefined {\n const statuses = payload.statuses;\n if (Array.isArray(statuses)) {\n const first = statuses[0] as Record<string, unknown> | undefined;\n if (typeof first?.message_id === \"string\") return first.message_id;\n } else if (statuses && typeof statuses === \"object\") {\n for (const value of Object.values(statuses as Record<string, unknown>)) {\n if (value && typeof value === \"object\") {\n const id = (value as Record<string, unknown>).message_id;\n if (typeof id === \"string\") return id;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Taqnyat WhatsApp transport via JSON Bearer `wa/v2/messages/`.\n */\nexport class TaqnyatWhatsAppTransport implements WhatsAppTransport {\n readonly provider = \"taqnyat-whatsapp\";\n\n private readonly bearerToken: string;\n\n /** Creates a Taqnyat WhatsApp transport. */\n constructor(config: TaqnyatWhatsAppConfig) {\n this.bearerToken = config.bearerToken;\n }\n\n /** Sends a WhatsApp template or session text message. */\n async send(options: WhatsAppOptions): Promise<WhatsAppSendResult> {\n const to = normalizeTaqnyatPhone(options.to);\n const body = isTemplateMessage(options)\n ? {\n to,\n type: \"template\" as const,\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 to,\n type: \"text\" as const,\n text: { body: options.text },\n };\n\n // Docs: base URL must end with `/`.\n const response = await fetch(\"https://api.taqnyat.sa/wa/v2/messages/\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.bearerToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n const payload = (await response.json()) as Record<string, unknown>;\n const messageId = extractMessageId(payload);\n\n // Docs return `{ message, reason }` on failure (often still HTTP 200).\n if (!response.ok || (payload.message !== undefined && messageId === undefined)) {\n const reason =\n typeof payload.reason === \"string\"\n ? payload.reason\n : typeof payload.message === \"string\"\n ? `Taqnyat WhatsApp error ${payload.message}`\n : \"Taqnyat WhatsApp API error\";\n const status =\n typeof payload.message === \"string\" && /^\\d+$/.test(payload.message)\n ? Number(payload.message)\n : response.status;\n throw new TaqnyatWhatsAppError(reason, status || response.status, payload);\n }\n\n return {\n messageId: messageId ?? options.messageId ?? \"\",\n to: options.to,\n status: \"accepted\",\n response: typeof payload.type === \"string\" ? payload.type : \"Message sent\",\n provider: \"taqnyat-whatsapp\",\n };\n }\n\n /** Lightweight credential check. */\n async verify(): Promise<VerifyResult> {\n return {\n ok: Boolean(this.bearerToken),\n provider: \"taqnyat-whatsapp\",\n message: this.bearerToken ? \"Credentials present\" : \"Missing credentials\",\n };\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "kIAuCO,CAAM,KAA6B,CAAY,CAIlC,WACA,SAHlB,WAAW,CACT,EACgB,EACA,EAChB,CACA,MACE,EACA,EAAuB,GAAc,KAAO,EAAa,IAAM,EAAa,GAAG,EAC/E,CACE,aACA,SAAU,mBACV,MAAO,CACT,CACF,EAXgB,kBACA,gBAWhB,KAAK,KAAO,uBAEhB,CAEA,SAAS,CAAiB,CACxB,EAC4D,CAC5D,MAAO,aAAc,EAGvB,SAAS,CAAgB,CAAC,EAAsD,CAC9E,IAAM,EAAW,EAAQ,SACzB,GAAI,MAAM,QAAQ,CAAQ,EAAG,CAC3B,IAAM,EAAQ,EAAS,GACvB,GAAI,OAAO,GAAO,aAAe,SAAU,OAAO,EAAM,WACnD,QAAI,GAAY,OAAO,IAAa,UACzC,QAAW,KAAS,OAAO,OAAO,CAAmC,EACnE,GAAI,GAAS,OAAO,IAAU,SAAU,CACtC,IAAM,EAAM,EAAkC,WAC9C,GAAI,OAAO,IAAO,SAAU,OAAO,GAIzC,OAMK,MAAM,CAAsD,CACxD,SAAW,mBAEH,YAGjB,WAAW,CAAC,EAA+B,CACzC,KAAK,YAAc,EAAO,iBAItB,KAAI,CAAC,EAAuD,CAChE,IAAM,EAAK,EAAsB,EAAQ,EAAE,EACrC,EAAO,EAAkB,CAAO,EAClC,CACE,KACA,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,KACA,KAAM,OACN,KAAM,CAAE,KAAM,EAAQ,IAAK,CAC7B,EAGE,EAAW,MAAM,MAAM,yCAA0C,CACrE,OAAQ,OACR,QAAS,CACP,cAAe,UAAU,KAAK,cAC9B,eAAgB,kBAClB,EACA,KAAM,KAAK,UAAU,CAAI,CAC3B,CAAC,EAEK,EAAW,MAAM,EAAS,KAAK,EAC/B,EAAY,EAAiB,CAAO,EAG1C,GAAI,CAAC,EAAS,IAAO,EAAQ,UAAY,QAAa,IAAc,OAAY,CAC9E,IAAM,EACJ,OAAO,EAAQ,SAAW,SACtB,EAAQ,OACR,OAAO,EAAQ,UAAY,SACzB,0BAA0B,EAAQ,UAClC,6BACF,EACJ,OAAO,EAAQ,UAAY,UAAY,QAAQ,KAAK,EAAQ,OAAO,EAC/D,OAAO,EAAQ,OAAO,EACtB,EAAS,OACf,MAAM,IAAI,EAAqB,EAAQ,GAAU,EAAS,OAAQ,CAAO,EAG3E,MAAO,CACL,UAAW,GAAa,EAAQ,WAAa,GAC7C,GAAI,EAAQ,GACZ,OAAQ,WACR,SAAU,OAAO,EAAQ,OAAS,SAAW,EAAQ,KAAO,eAC5D,SAAU,kBACZ,OAII,OAAM,EAA0B,CACpC,MAAO,CACL,GAAI,QAAQ,KAAK,WAAW,EAC5B,SAAU,mBACV,QAAS,KAAK,YAAc,sBAAwB,qBACtD,EAEJ",
|
|
8
|
+
"debugId": "5657D7720E9F16CB64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SentlyError } from "../core/errors.js";
|
|
2
|
+
import type { SmsOptions, SmsSendResult, SmsTransport } from "../core/sms-types.js";
|
|
3
|
+
import type { VerifyResult } from "../core/types.js";
|
|
4
|
+
/** Twilio Messages API configuration. */
|
|
5
|
+
export interface TwilioSmsConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Twilio Account SID — always used in the Messages API URL path.
|
|
8
|
+
* Store in environment variables — never hardcode.
|
|
9
|
+
*/
|
|
10
|
+
accountSid: string;
|
|
11
|
+
/**
|
|
12
|
+
* Basic-auth password: API Key Secret when {@link apiKey} is set, otherwise Auth Token.
|
|
13
|
+
* Store in environment variables — never hardcode.
|
|
14
|
+
*/
|
|
15
|
+
authToken: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional Twilio API Key SID used as Basic-auth username (recommended in production).
|
|
18
|
+
* When omitted, {@link accountSid} is used as the Basic-auth username.
|
|
19
|
+
*/
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Default From phone number, alphanumeric sender ID, short code, or channel address
|
|
23
|
+
* (E.164 recommended, e.g. `+15557654321`). Either this / `options.from` or
|
|
24
|
+
* {@link messagingServiceSid} is required.
|
|
25
|
+
*/
|
|
26
|
+
from?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Messaging Service SID (`MG…`). When set and `From` is omitted, Twilio selects
|
|
29
|
+
* the optimal sender from the service's Sender Pool.
|
|
30
|
+
*/
|
|
31
|
+
messagingServiceSid?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Optional status callback URL (`StatusCallback`). Twilio POSTs delivery updates here.
|
|
34
|
+
*/
|
|
35
|
+
statusCallback?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Error thrown when the Twilio SMS API returns a non-success response. */
|
|
38
|
+
export declare class TwilioSmsError extends SentlyError {
|
|
39
|
+
readonly statusCode: number;
|
|
40
|
+
readonly apiError: unknown;
|
|
41
|
+
/** Creates a Twilio SMS API error with status code and response payload. */
|
|
42
|
+
constructor(message: string, statusCode: number, apiError: unknown);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Twilio SMS transport via the Messages REST API (form-urlencoded).
|
|
46
|
+
*/
|
|
47
|
+
export declare class TwilioSmsTransport implements SmsTransport {
|
|
48
|
+
readonly provider = "twilio-sms";
|
|
49
|
+
private readonly accountSid;
|
|
50
|
+
private readonly authToken;
|
|
51
|
+
private readonly apiKey?;
|
|
52
|
+
private readonly from?;
|
|
53
|
+
private readonly messagingServiceSid?;
|
|
54
|
+
private readonly statusCallback?;
|
|
55
|
+
/** Creates a Twilio SMS transport. */
|
|
56
|
+
constructor(config: TwilioSmsConfig);
|
|
57
|
+
/** Sends an SMS via Twilio Messages API. */
|
|
58
|
+
send(options: SmsOptions): Promise<SmsSendResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Validates credentials via Twilio Account fetch
|
|
61
|
+
* (`GET /2010-04-01/Accounts/{AccountSid}.json`).
|
|
62
|
+
*/
|
|
63
|
+
verify(): Promise<VerifyResult>;
|
|
64
|
+
private authHeaders;
|
|
65
|
+
private readJson;
|
|
66
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{Y as J}from"../chunk-ywq6s10g.js";import{ca as K,da as L}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class H extends K{statusCode;apiError;constructor(k,q,x){super(k,L(q),{statusCode:q,provider:"twilio-sms",cause:x});this.statusCode=q;this.apiError=x;this.name="TwilioSmsError"}}class N{provider="twilio-sms";accountSid;authToken;apiKey;from;messagingServiceSid;statusCallback;constructor(k){if(this.accountSid=k.accountSid,this.authToken=k.authToken,k.apiKey!==void 0)this.apiKey=k.apiKey;if(k.from!==void 0)this.from=k.from;if(k.messagingServiceSid!==void 0)this.messagingServiceSid=k.messagingServiceSid;if(k.statusCallback!==void 0)this.statusCallback=k.statusCallback}async send(k){let q=k.from??this.from;if(!q&&!this.messagingServiceSid)throw new H("Twilio SMS requires From or MessagingServiceSid",400,{message:"Missing from / messagingServiceSid"});let x=new URLSearchParams({To:k.to,Body:k.body});if(q)x.set("From",q);if(this.messagingServiceSid)x.set("MessagingServiceSid",this.messagingServiceSid);if(this.statusCallback)x.set("StatusCallback",this.statusCallback);let I=`https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`,z=await fetch(I,{method:"POST",headers:{...this.authHeaders(),"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:x}),D=await this.readJson(z);if(!z.ok){let M=typeof D==="object"&&D!==null&&"message"in D&&typeof D.message==="string"?D.message:"Twilio SMS API error";throw new H(M,z.status,D)}let F=D;return{messageId:F.sid??k.messageId??"",to:k.to,status:F.status??"queued",response:F.status??"queued",provider:"twilio-sms"}}async verify(){if(!this.accountSid||!this.authToken)return{ok:!1,provider:"twilio-sms",message:"Missing credentials"};try{let k=`https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}.json`,q=await fetch(k,{method:"GET",headers:{...this.authHeaders(),Accept:"application/json"}});if(!q.ok){let z=await this.readJson(q);return{ok:!1,provider:"twilio-sms",message:typeof z==="object"&&z!==null&&"message"in z&&typeof z.message==="string"?z.message:`HTTP ${q.status}`}}let x=await this.readJson(q);return{ok:!0,provider:"twilio-sms",message:`Account ${x.friendly_name??x.status??"ok"}`}}catch(k){return{ok:!1,provider:"twilio-sms",message:k instanceof Error?k.message:String(k)}}}authHeaders(){let k=this.apiKey??this.accountSid;return{Authorization:`Basic ${J(`${k}:${this.authToken}`).replace(/\r\n/g,"")}`}}async readJson(k){return k.json().catch(()=>({}))}}export{N as TwilioSmsTransport,H as TwilioSmsError};
|
|
2
|
+
|
|
3
|
+
//# debugId=54563D7C95D0712D64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/twilio-sms.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Twilio Messages API transport for SMS.\n *\n * Aligned with Twilio Programmable Messaging\n * ([Message resource](https://www.twilio.com/docs/messaging/api/message-resource)):\n * `POST /2010-04-01/Accounts/{AccountSid}/Messages.json` with\n * `application/x-www-form-urlencoded` and HTTP Basic auth.\n *\n * Sender must be {@link TwilioSmsConfig.from} / `options.from` **or**\n * {@link TwilioSmsConfig.messagingServiceSid} (Twilio selects from the Sender Pool\n * when `From` is omitted).\n *\n * Sently-first: wire into {@link createSmsSender}. Twilio-only products (Verify,\n * Voice, Conversations, etc.) stay off this transport unless added as concrete\n * helpers later.\n *\n * Production auth: set `apiKey` + `authToken` (API Key Secret) with `accountSid`\n * still required in the URL path. Account SID + Auth Token alone is fine for\n * local testing — never hardcode credentials in source.\n *\n * @example\n * ```ts\n * import { createSmsSender } from \"sently/sms\";\n * import { TwilioSmsTransport } from \"sently/transports/twilio-sms\";\n *\n * const sms = createSmsSender({\n * transport: new TwilioSmsTransport({\n * accountSid: process.env.TWILIO_ACCOUNT_SID!,\n * apiKey: process.env.TWILIO_API_KEY!,\n * authToken: process.env.TWILIO_API_KEY_SECRET!,\n * from: \"+15557654321\",\n * }),\n * });\n * ```\n */\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type { SmsOptions, SmsSendResult, SmsTransport } from \"../core/sms-types.js\";\nimport type { VerifyResult } from \"../core/types.js\";\n\n/** Twilio Messages API configuration. */\nexport interface TwilioSmsConfig {\n /**\n * Twilio Account SID — always used in the Messages API URL path.\n * Store in environment variables — never hardcode.\n */\n accountSid: string;\n /**\n * Basic-auth password: API Key Secret when {@link apiKey} is set, otherwise Auth Token.\n * Store in environment variables — never hardcode.\n */\n authToken: string;\n /**\n * Optional Twilio API Key SID used as Basic-auth username (recommended in production).\n * When omitted, {@link accountSid} is used as the Basic-auth username.\n */\n apiKey?: string;\n /**\n * Default From phone number, alphanumeric sender ID, short code, or channel address\n * (E.164 recommended, e.g. `+15557654321`). Either this / `options.from` or\n * {@link messagingServiceSid} is required.\n */\n from?: string;\n /**\n * Messaging Service SID (`MG…`). When set and `From` is omitted, Twilio selects\n * the optimal sender from the service's Sender Pool.\n */\n messagingServiceSid?: string;\n /**\n * Optional status callback URL (`StatusCallback`). Twilio POSTs delivery updates here.\n */\n statusCallback?: string;\n}\n\n/** Error thrown when the Twilio SMS API returns a non-success response. */\nexport class TwilioSmsError extends SentlyError {\n /** Creates a Twilio SMS API error with status code and response payload. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"twilio-sms\",\n cause: apiError,\n });\n this.name = \"TwilioSmsError\";\n }\n}\n\n/**\n * Twilio SMS transport via the Messages REST API (form-urlencoded).\n */\nexport class TwilioSmsTransport implements SmsTransport {\n readonly provider = \"twilio-sms\";\n\n private readonly accountSid: string;\n private readonly authToken: string;\n private readonly apiKey?: string;\n private readonly from?: string;\n private readonly messagingServiceSid?: string;\n private readonly statusCallback?: string;\n\n /** Creates a Twilio SMS transport. */\n constructor(config: TwilioSmsConfig) {\n this.accountSid = config.accountSid;\n this.authToken = config.authToken;\n if (config.apiKey !== undefined) {\n this.apiKey = config.apiKey;\n }\n if (config.from !== undefined) {\n this.from = config.from;\n }\n if (config.messagingServiceSid !== undefined) {\n this.messagingServiceSid = config.messagingServiceSid;\n }\n if (config.statusCallback !== undefined) {\n this.statusCallback = config.statusCallback;\n }\n }\n\n /** Sends an SMS via Twilio Messages API. */\n async send(options: SmsOptions): Promise<SmsSendResult> {\n const from = options.from ?? this.from;\n if (!from && !this.messagingServiceSid) {\n throw new TwilioSmsError(\"Twilio SMS requires From or MessagingServiceSid\", 400, {\n message: \"Missing from / messagingServiceSid\",\n });\n }\n\n // Keep E.164 `+` — Twilio expects it (unlike Taqnyat/Msegat).\n const body = new URLSearchParams({\n To: options.to,\n Body: options.body,\n });\n if (from) {\n body.set(\"From\", from);\n }\n if (this.messagingServiceSid) {\n body.set(\"MessagingServiceSid\", this.messagingServiceSid);\n }\n if (this.statusCallback) {\n body.set(\"StatusCallback\", this.statusCallback);\n }\n\n const url = `https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n ...this.authHeaders(),\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body,\n });\n\n const payload = await this.readJson(response);\n\n if (!response.ok) {\n const message =\n typeof payload === \"object\" &&\n payload !== null &&\n \"message\" in payload &&\n typeof (payload as { message: unknown }).message === \"string\"\n ? (payload as { message: string }).message\n : \"Twilio SMS API error\";\n throw new TwilioSmsError(message, response.status, payload);\n }\n\n const record = payload as {\n sid?: string;\n status?: string;\n };\n\n return {\n messageId: record.sid ?? options.messageId ?? \"\",\n to: options.to,\n status: record.status ?? \"queued\",\n response: record.status ?? \"queued\",\n provider: \"twilio-sms\",\n };\n }\n\n /**\n * Validates credentials via Twilio Account fetch\n * (`GET /2010-04-01/Accounts/{AccountSid}.json`).\n */\n async verify(): Promise<VerifyResult> {\n if (!this.accountSid || !this.authToken) {\n return { ok: false, provider: \"twilio-sms\", message: \"Missing credentials\" };\n }\n\n try {\n const url = `https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}.json`;\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n ...this.authHeaders(),\n Accept: \"application/json\",\n },\n });\n\n if (!response.ok) {\n const payload = await this.readJson(response);\n const message =\n typeof payload === \"object\" &&\n payload !== null &&\n \"message\" in payload &&\n typeof (payload as { message: unknown }).message === \"string\"\n ? (payload as { message: string }).message\n : `HTTP ${response.status}`;\n return { ok: false, provider: \"twilio-sms\", message };\n }\n\n const payload = (await this.readJson(response)) as {\n friendly_name?: string;\n status?: string;\n };\n const label = payload.friendly_name ?? payload.status ?? \"ok\";\n return { ok: true, provider: \"twilio-sms\", message: `Account ${label}` };\n } catch (error) {\n return {\n ok: false,\n provider: \"twilio-sms\",\n message: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n private authHeaders(): { Authorization: string } {\n const authUser = this.apiKey ?? this.accountSid;\n const credentials = encodeBase64(`${authUser}:${this.authToken}`).replace(/\\r\\n/g, \"\");\n return { Authorization: `Basic ${credentials}` };\n }\n\n private async readJson(response: Response): Promise<unknown> {\n return response.json().catch(() => ({}));\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "54563D7C95D0712D64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { SentlyError } from "../core/errors.js";
|
|
2
|
+
import type { PushOptions, PushSendResult, PushTransport } from "../core/push-types.js";
|
|
3
|
+
import type { VerifyResult } from "../core/types.js";
|
|
4
|
+
/** Web Push / VAPID configuration. */
|
|
5
|
+
export interface WebPushConfig {
|
|
6
|
+
/** Base64url-encoded uncompressed P-256 public key (65 bytes). */
|
|
7
|
+
vapidPublicKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* Base64url-encoded raw P-256 private key (32 bytes).
|
|
10
|
+
* Treat as a tier-1 secret — inject from env / secrets manager only.
|
|
11
|
+
*/
|
|
12
|
+
vapidPrivateKey: string;
|
|
13
|
+
/** Contact URI for the VAPID `sub` claim (`mailto:` or `https:`). */
|
|
14
|
+
subject: string;
|
|
15
|
+
/**
|
|
16
|
+
* Extra exact hostnames allowed for `subscription.endpoint` beyond the
|
|
17
|
+
* built-in FCM / Mozilla / Apple / WNS allowlist. Use for private push relays.
|
|
18
|
+
*/
|
|
19
|
+
allowedEndpointHosts?: string[];
|
|
20
|
+
}
|
|
21
|
+
/** Error thrown when a push service rejects the request. */
|
|
22
|
+
export declare class WebPushError extends SentlyError {
|
|
23
|
+
readonly statusCode: number;
|
|
24
|
+
readonly apiError: unknown;
|
|
25
|
+
/** Creates a Web Push error with HTTP status and response body. */
|
|
26
|
+
constructor(message: string, statusCode: number, apiError: unknown);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Web Push transport — VAPID auth + RFC 8291 payload encryption.
|
|
30
|
+
*
|
|
31
|
+
* Endpoint URLs are validated against an allowlist before fetch to mitigate SSRF.
|
|
32
|
+
* Redirects are not followed.
|
|
33
|
+
*/
|
|
34
|
+
export declare class WebPushTransport implements PushTransport {
|
|
35
|
+
readonly provider = "webpush";
|
|
36
|
+
private readonly vapidPublicKey;
|
|
37
|
+
private readonly vapidPrivateKey;
|
|
38
|
+
private readonly subject;
|
|
39
|
+
private readonly allowedEndpointHosts;
|
|
40
|
+
/** Creates a Web Push transport with VAPID credentials. */
|
|
41
|
+
constructor(config: WebPushConfig);
|
|
42
|
+
/** Encrypts and POSTs a notification to the subscription endpoint. */
|
|
43
|
+
send(options: PushOptions): Promise<PushSendResult>;
|
|
44
|
+
/** Lightweight VAPID credential shape check. */
|
|
45
|
+
verify(): Promise<VerifyResult>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{U as T}from"../chunk-yp311efr.js";import{$ as N,aa as H,ba as J}from"../chunk-ywq6s10g.js";import{ca as m,da as v}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class V extends m{statusCode;apiError;constructor(q,z,Q){super(q,v(z),{statusCode:z,provider:"webpush",cause:Q});this.statusCode=z;this.apiError=Q;this.name="WebPushError"}}var k=2419200,U=4096,C=4096;function X(q){return q.buffer.slice(q.byteOffset,q.byteOffset+q.byteLength)}function j(...q){let z=q.reduce((G,Z)=>G+Z.length,0),Q=new Uint8Array(z),$=0;for(let G of q)Q.set(G,$),$+=G.length;return Q}function g(q){let z=new Uint8Array(4);return new DataView(z.buffer).setUint32(0,q,!1),z}function K(q){if(q.length!==65||q[0]!==4)throw new V("Invalid P-256 uncompressed public key",400,{length:q.length});return{kty:"EC",crv:"P-256",x:N(q.subarray(1,33)),y:N(q.subarray(33,65))}}function E(q,z){return{...K(q),d:N(z)}}async function _(q,z){let Q=await crypto.subtle.importKey("raw",X(q),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),$=await crypto.subtle.sign("HMAC",Q,X(z));return new Uint8Array($)}async function w(q,z,Q,$){let G=await _(q,z);return(await _(G,j(Q,new Uint8Array([1])))).subarray(0,$)}async function B(q,z){let Q=H(q.keys.p256dh),$=H(q.keys.auth);if(Q.length!==65||Q[0]!==4)throw new V("Invalid subscription p256dh key",400,{length:Q.length});if($.length<16)throw new V("Invalid subscription auth key",400,{length:$.length});let G=await crypto.subtle.generateKey({name:"ECDH",namedCurve:"P-256"},!0,["deriveBits"]),Z=new Uint8Array(await crypto.subtle.exportKey("raw",G.publicKey)),F=await crypto.subtle.importKey("raw",X(Q),{name:"ECDH",namedCurve:"P-256"},!1,[]),M=new Uint8Array(await crypto.subtle.deriveBits({name:"ECDH",public:F},G.privateKey,256)),L=j(J("WebPush: info\x00"),Q,Z),x=await w($,M,L,32),O=crypto.getRandomValues(new Uint8Array(16)),I=J("Content-Encoding: aes128gcm\x00"),R=J("Content-Encoding: nonce\x00"),Y=await w(O,x,I,16),D=await w(O,x,R,12),A=j(z,new Uint8Array([2])),W=await crypto.subtle.importKey("raw",X(Y),"AES-GCM",!1,["encrypt"]),S=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:X(D)},W,X(A)));return j(O,g(U),new Uint8Array([Z.length]),Z,S)}async function P(q,z,Q,$){let G=H(Q),Z=H($),F=E(G,Z),M=await crypto.subtle.importKey("jwk",F,{name:"ECDSA",namedCurve:"P-256"},!1,["sign"]),L=new URL(q).origin,x={typ:"JWT",alg:"ES256"},O={aud:L,exp:Math.floor(Date.now()/1000)+43200,sub:z},I=N(J(JSON.stringify(x))),R=N(J(JSON.stringify(O))),Y=`${I}.${R}`,D=new Uint8Array(await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},M,X(J(Y))));return`${Y}.${N(D)}`}class h{provider="webpush";vapidPublicKey;vapidPrivateKey;subject;allowedEndpointHosts;constructor(q){this.vapidPublicKey=q.vapidPublicKey,this.vapidPrivateKey=q.vapidPrivateKey,this.subject=q.subject,this.allowedEndpointHosts=q.allowedEndpointHosts??[]}async send(q){try{T(q.subscription.endpoint,this.allowedEndpointHosts)}catch(M){let L;try{L=new URL(q.subscription.endpoint).hostname}catch{L=void 0}throw new V(M instanceof Error?M.message:"Unsafe push subscription endpoint",400,{endpointHost:L})}let z={title:q.title,body:q.body,...q.data!==void 0?{data:q.data}:{},...q.icon!==void 0?{icon:q.icon}:{}},Q=J(JSON.stringify(z)),$=await B(q.subscription,Q);if($.length>C)throw new V(`Encrypted push payload exceeds ${C} bytes (RFC 8291)`,413,{size:$.length});let G=await P(q.subscription.endpoint,this.subject,this.vapidPublicKey,this.vapidPrivateKey),Z=q.ttl??k,F=await fetch(q.subscription.endpoint,{method:"POST",redirect:"manual",headers:{Authorization:`vapid t=${G}, k=${this.vapidPublicKey}`,"Content-Encoding":"aes128gcm",TTL:String(Z),"Content-Type":"application/octet-stream"},body:X($)});if(F.status>=300&&F.status<400)throw new V(`Push service returned redirect ${F.status}; redirects are not followed`,F.status,{location:F.headers.get("location")});if(!F.ok){let M=await F.text().catch(()=>"");throw new V(M||`Web Push request failed with status ${F.status}`,F.status,M||{status:F.status})}return{messageId:q.messageId??crypto.randomUUID(),status:"accepted",response:String(F.status),provider:"webpush"}}async verify(){try{let q=H(this.vapidPublicKey),z=H(this.vapidPrivateKey),Q=q.length===65&&z.length===32&&this.subject.length>0;return{ok:Q,provider:"webpush",message:Q?"VAPID credentials present":"Invalid VAPID key lengths"}}catch(q){return{ok:!1,provider:"webpush",message:q instanceof Error?q.message:"Invalid VAPID credentials"}}}}export{h as WebPushTransport,V as WebPushError};
|
|
2
|
+
|
|
3
|
+
//# debugId=43FC53C9208BBE5564756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/webpush.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Web Push transport (RFC 8292 VAPID + RFC 8291 aes128gcm).\n *\n * VAPID keys are the common web-push raw format: base64url-encoded\n * uncompressed P-256 public key (65 bytes) and raw private key (32 bytes).\n *\n * Store `vapidPrivateKey` in a secrets manager / environment variable — never\n * hardcode it or commit it to source control.\n *\n * @example\n * ```ts\n * import { createPushSender } from \"sently/push\";\n * import { WebPushTransport } from \"sently/transports/webpush\";\n *\n * const push = createPushSender({\n * transport: new WebPushTransport({\n * vapidPublicKey: process.env.VAPID_PUBLIC_KEY!,\n * vapidPrivateKey: process.env.VAPID_PRIVATE_KEY!,\n * subject: \"mailto:you@example.com\",\n * }),\n * });\n * ```\n */\nimport { decodeBase64Url, encodeBase64Url, encodeUtf8 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport { assertSafePushEndpoint } from \"../core/push-endpoint.js\";\nimport type {\n PushOptions,\n PushSendResult,\n PushSubscription,\n PushTransport,\n} from \"../core/push-types.js\";\nimport type { VerifyResult } from \"../core/types.js\";\n\n/** Web Push / VAPID configuration. */\nexport interface WebPushConfig {\n /** Base64url-encoded uncompressed P-256 public key (65 bytes). */\n vapidPublicKey: string;\n /**\n * Base64url-encoded raw P-256 private key (32 bytes).\n * Treat as a tier-1 secret — inject from env / secrets manager only.\n */\n vapidPrivateKey: string;\n /** Contact URI for the VAPID `sub` claim (`mailto:` or `https:`). */\n subject: string;\n /**\n * Extra exact hostnames allowed for `subscription.endpoint` beyond the\n * built-in FCM / Mozilla / Apple / WNS allowlist. Use for private push relays.\n */\n allowedEndpointHosts?: string[];\n}\n\n/** Error thrown when a push service rejects the request. */\nexport class WebPushError extends SentlyError {\n /** Creates a Web Push error with HTTP status and response body. */\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly apiError: unknown,\n ) {\n super(message, httpStatusToSentlyCode(statusCode), {\n statusCode,\n provider: \"webpush\",\n cause: apiError,\n });\n this.name = \"WebPushError\";\n }\n}\n\n/** Default TTL (28 days) in seconds. */\nconst DEFAULT_TTL_SECONDS = 2419200;\n/** RFC 8188 / Web Push record size. */\nconst RECORD_SIZE = 4096;\n/**\n * Maximum encrypted body size push services are required to accept (RFC 8291).\n * Includes salt, rs, keyid, and ciphertext+tag.\n */\nconst MAX_ENCRYPTED_BODY_BYTES = 4096;\n\nfunction toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n\nfunction concatBytes(...parts: Uint8Array[]): Uint8Array {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nfunction uint32Be(value: number): Uint8Array {\n const out = new Uint8Array(4);\n new DataView(out.buffer).setUint32(0, value, false);\n return out;\n}\n\nfunction ecPublicToJwk(publicKey: Uint8Array): JsonWebKey {\n if (publicKey.length !== 65 || publicKey[0] !== 0x04) {\n throw new WebPushError(\"Invalid P-256 uncompressed public key\", 400, {\n length: publicKey.length,\n });\n }\n return {\n kty: \"EC\",\n crv: \"P-256\",\n x: encodeBase64Url(publicKey.subarray(1, 33)),\n y: encodeBase64Url(publicKey.subarray(33, 65)),\n };\n}\n\nfunction ecKeyPairToJwk(publicKey: Uint8Array, privateKey: Uint8Array): JsonWebKey {\n return {\n ...ecPublicToJwk(publicKey),\n d: encodeBase64Url(privateKey),\n };\n}\n\nasync function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n toArrayBuffer(key),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await crypto.subtle.sign(\"HMAC\", cryptoKey, toArrayBuffer(data));\n return new Uint8Array(sig);\n}\n\n/** HKDF-Extract + Expand (HMAC-SHA-256) for a single block (L ≤ 32). */\nasync function hkdf(\n salt: Uint8Array,\n ikm: Uint8Array,\n info: Uint8Array,\n length: number,\n): Promise<Uint8Array> {\n const prk = await hmacSha256(salt, ikm);\n const block = await hmacSha256(prk, concatBytes(info, new Uint8Array([0x01])));\n return block.subarray(0, length);\n}\n\n/**\n * Encrypt a payload per RFC 8291 (aes128gcm content coding).\n *\n * Confidence note: Implemented from RFC 8291 / RFC 8188. Tests assert header\n * shapes, framing, and size limits; full known-vector fixtures were not\n * available in-repo — treat as high-scrutiny crypto.\n */\nasync function encryptAes128Gcm(\n subscription: PushSubscription,\n plaintext: Uint8Array,\n): Promise<Uint8Array> {\n const uaPublic = decodeBase64Url(subscription.keys.p256dh);\n const authSecret = decodeBase64Url(subscription.keys.auth);\n\n if (uaPublic.length !== 65 || uaPublic[0] !== 0x04) {\n throw new WebPushError(\"Invalid subscription p256dh key\", 400, {\n length: uaPublic.length,\n });\n }\n if (authSecret.length < 16) {\n throw new WebPushError(\"Invalid subscription auth key\", 400, {\n length: authSecret.length,\n });\n }\n\n const asKeyPair = await crypto.subtle.generateKey({ name: \"ECDH\", namedCurve: \"P-256\" }, true, [\n \"deriveBits\",\n ]);\n const asPublicRaw = new Uint8Array(await crypto.subtle.exportKey(\"raw\", asKeyPair.publicKey));\n\n const uaKey = await crypto.subtle.importKey(\n \"raw\",\n toArrayBuffer(uaPublic),\n { name: \"ECDH\", namedCurve: \"P-256\" },\n false,\n [],\n );\n\n const ecdhSecret = new Uint8Array(\n await crypto.subtle.deriveBits({ name: \"ECDH\", public: uaKey }, asKeyPair.privateKey, 256),\n );\n\n const keyInfo = concatBytes(encodeUtf8(\"WebPush: info\\0\"), uaPublic, asPublicRaw);\n const ikm = await hkdf(authSecret, ecdhSecret, keyInfo, 32);\n\n const salt = crypto.getRandomValues(new Uint8Array(16));\n const cekInfo = encodeUtf8(\"Content-Encoding: aes128gcm\\0\");\n const nonceInfo = encodeUtf8(\"Content-Encoding: nonce\\0\");\n const cek = await hkdf(salt, ikm, cekInfo, 16);\n const nonce = await hkdf(salt, ikm, nonceInfo, 12);\n\n // RFC 8188 final-record padding delimiter\n const padded = concatBytes(plaintext, new Uint8Array([0x02]));\n\n const aesKey = await crypto.subtle.importKey(\"raw\", toArrayBuffer(cek), \"AES-GCM\", false, [\n \"encrypt\",\n ]);\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv: toArrayBuffer(nonce) },\n aesKey,\n toArrayBuffer(padded),\n ),\n );\n\n // salt || rs || idlen || keyid || ciphertext\n return concatBytes(\n salt,\n uint32Be(RECORD_SIZE),\n new Uint8Array([asPublicRaw.length]),\n asPublicRaw,\n ciphertext,\n );\n}\n\nasync function signVapidJwt(\n endpoint: string,\n subject: string,\n vapidPublicKey: string,\n vapidPrivateKey: string,\n): Promise<string> {\n const publicKey = decodeBase64Url(vapidPublicKey);\n const privateKey = decodeBase64Url(vapidPrivateKey);\n const jwk = ecKeyPairToJwk(publicKey, privateKey);\n\n const key = await crypto.subtle.importKey(\n \"jwk\",\n jwk,\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n false,\n [\"sign\"],\n );\n\n const origin = new URL(endpoint).origin;\n const header = { typ: \"JWT\", alg: \"ES256\" };\n const payload = {\n aud: origin,\n exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60,\n sub: subject,\n };\n\n const encodedHeader = encodeBase64Url(encodeUtf8(JSON.stringify(header)));\n const encodedPayload = encodeBase64Url(encodeUtf8(JSON.stringify(payload)));\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n\n const signature = new Uint8Array(\n await crypto.subtle.sign(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n key,\n toArrayBuffer(encodeUtf8(signingInput)),\n ),\n );\n\n return `${signingInput}.${encodeBase64Url(signature)}`;\n}\n\n/**\n * Web Push transport — VAPID auth + RFC 8291 payload encryption.\n *\n * Endpoint URLs are validated against an allowlist before fetch to mitigate SSRF.\n * Redirects are not followed.\n */\nexport class WebPushTransport implements PushTransport {\n readonly provider = \"webpush\";\n\n private readonly vapidPublicKey: string;\n private readonly vapidPrivateKey: string;\n private readonly subject: string;\n private readonly allowedEndpointHosts: readonly string[];\n\n /** Creates a Web Push transport with VAPID credentials. */\n constructor(config: WebPushConfig) {\n this.vapidPublicKey = config.vapidPublicKey;\n this.vapidPrivateKey = config.vapidPrivateKey;\n this.subject = config.subject;\n this.allowedEndpointHosts = config.allowedEndpointHosts ?? [];\n }\n\n /** Encrypts and POSTs a notification to the subscription endpoint. */\n async send(options: PushOptions): Promise<PushSendResult> {\n try {\n assertSafePushEndpoint(options.subscription.endpoint, this.allowedEndpointHosts);\n } catch (error) {\n let endpointHost: string | undefined;\n try {\n endpointHost = new URL(options.subscription.endpoint).hostname;\n } catch {\n endpointHost = undefined;\n }\n throw new WebPushError(\n error instanceof Error ? error.message : \"Unsafe push subscription endpoint\",\n 400,\n { endpointHost },\n );\n }\n\n const notification = {\n title: options.title,\n body: options.body,\n ...(options.data !== undefined ? { data: options.data } : {}),\n ...(options.icon !== undefined ? { icon: options.icon } : {}),\n };\n const plaintext = encodeUtf8(JSON.stringify(notification));\n const encrypted = await encryptAes128Gcm(options.subscription, plaintext);\n\n if (encrypted.length > MAX_ENCRYPTED_BODY_BYTES) {\n throw new WebPushError(\n `Encrypted push payload exceeds ${MAX_ENCRYPTED_BODY_BYTES} bytes (RFC 8291)`,\n 413,\n { size: encrypted.length },\n );\n }\n\n const jwt = await signVapidJwt(\n options.subscription.endpoint,\n this.subject,\n this.vapidPublicKey,\n this.vapidPrivateKey,\n );\n\n const ttl = options.ttl ?? DEFAULT_TTL_SECONDS;\n const response = await fetch(options.subscription.endpoint, {\n method: \"POST\",\n redirect: \"manual\",\n headers: {\n Authorization: `vapid t=${jwt}, k=${this.vapidPublicKey}`,\n \"Content-Encoding\": \"aes128gcm\",\n TTL: String(ttl),\n \"Content-Type\": \"application/octet-stream\",\n },\n body: toArrayBuffer(encrypted),\n });\n\n // Opaque redirect responses (3xx) must not be followed — SSRF mitigation.\n if (response.status >= 300 && response.status < 400) {\n throw new WebPushError(\n `Push service returned redirect ${response.status}; redirects are not followed`,\n response.status,\n { location: response.headers.get(\"location\") },\n );\n }\n\n if (!response.ok) {\n const bodyText = await response.text().catch(() => \"\");\n throw new WebPushError(\n bodyText || `Web Push request failed with status ${response.status}`,\n response.status,\n bodyText || { status: response.status },\n );\n }\n\n return {\n messageId: options.messageId ?? crypto.randomUUID(),\n status: \"accepted\",\n response: String(response.status),\n provider: \"webpush\",\n };\n }\n\n /** Lightweight VAPID credential shape check. */\n async verify(): Promise<VerifyResult> {\n try {\n const pub = decodeBase64Url(this.vapidPublicKey);\n const priv = decodeBase64Url(this.vapidPrivateKey);\n const ok = pub.length === 65 && priv.length === 32 && this.subject.length > 0;\n return {\n ok,\n provider: \"webpush\",\n message: ok ? \"VAPID credentials present\" : \"Invalid VAPID key lengths\",\n };\n } catch (error) {\n return {\n ok: false,\n provider: \"webpush\",\n message: error instanceof Error ? error.message : \"Invalid VAPID credentials\",\n };\n }\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "43FC53C9208BBE5564756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* WeightedFallbackTransport — routes each send through a weighted-random
|
|
4
|
+
* primary provider, then fails over through the remaining providers on error.
|
|
5
|
+
* Useful for gradual traffic shifting, migrations, and A/B testing providers.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { WeightedFallbackTransport } from "sently/transports/weighted-fallback";
|
|
10
|
+
* import { ResendTransport } from "sently/transports/resend";
|
|
11
|
+
* import { SESTransport } from "sently/transports/ses";
|
|
12
|
+
*
|
|
13
|
+
* const transport = new WeightedFallbackTransport(
|
|
14
|
+
* [
|
|
15
|
+
* { transport: new ResendTransport({ apiKey }), weight: 80 },
|
|
16
|
+
* { transport: new SESTransport({ accessKeyId, secretAccessKey }), weight: 20 },
|
|
17
|
+
* ],
|
|
18
|
+
* { cooldownMs: 300_000 },
|
|
19
|
+
* );
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import type { MailOptions, SendResult, Transport, VerifyResult } from "../core/types.js";
|
|
23
|
+
import { type FallbackOptions, FallbackTransport, type FallbackVerifyAllResult } from "./fallback.js";
|
|
24
|
+
/** A transport entry with a relative routing weight. */
|
|
25
|
+
export interface WeightedTransportEntry {
|
|
26
|
+
/** Transport instance. */
|
|
27
|
+
transport: Transport;
|
|
28
|
+
/** Relative weight (e.g. 80 vs 20 → ~80% of sends try this provider first). */
|
|
29
|
+
weight: number;
|
|
30
|
+
}
|
|
31
|
+
type MailerOnFallback = Parameters<FallbackTransport["setMailerOnFallback"]>[0];
|
|
32
|
+
/**
|
|
33
|
+
* Weighted provider routing with failover on error.
|
|
34
|
+
*/
|
|
35
|
+
export declare class WeightedFallbackTransport implements Transport {
|
|
36
|
+
readonly provider = "weighted-fallback";
|
|
37
|
+
private readonly entries;
|
|
38
|
+
private readonly fallbackOptions;
|
|
39
|
+
private readonly random;
|
|
40
|
+
private readonly inner;
|
|
41
|
+
private mailerOnFallback;
|
|
42
|
+
/**
|
|
43
|
+
* Creates a weighted fallback transport.
|
|
44
|
+
* @throws {Error} When entries is empty or total weight is zero.
|
|
45
|
+
*/
|
|
46
|
+
constructor(entries: WeightedTransportEntry[], options?: FallbackOptions & {
|
|
47
|
+
random?: () => number;
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Register a per-send onFallback callback from mailer lifecycle hooks.
|
|
51
|
+
* @internal Used by {@link MailerImpl}; cleared after each send.
|
|
52
|
+
*/
|
|
53
|
+
setMailerOnFallback(callback: MailerOnFallback | undefined): void;
|
|
54
|
+
private buildOrder;
|
|
55
|
+
private pickWeightedIndex;
|
|
56
|
+
/** Sends via a weighted-random primary provider, failing over on error. */
|
|
57
|
+
send(message: MailOptions): Promise<SendResult>;
|
|
58
|
+
/** Returns the first healthy provider in the chain. */
|
|
59
|
+
verify(): Promise<VerifyResult>;
|
|
60
|
+
/** Verifies every weighted provider in definition order. */
|
|
61
|
+
verifyAll(): Promise<FallbackVerifyAllResult>;
|
|
62
|
+
/** Closes every underlying transport. */
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{k as E}from"../chunk-0gqy32pe.js";import"../chunk-gp4fwrs8.js";import"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";class J{provider="weighted-fallback";entries;fallbackOptions;random;inner;mailerOnFallback;constructor(h,z){if(h.length===0)throw Error("WeightedFallbackTransport requires at least one entry");if(h.reduce((D,H)=>D+H.weight,0)<=0)throw Error("WeightedFallbackTransport requires positive total weight");this.entries=h,this.random=z?.random??Math.random;let q=new Map,{random:B,...G}=z??{};this.fallbackOptions={...G,cooldownState:q},this.inner=new E(h.map((D)=>D.transport),this.fallbackOptions)}setMailerOnFallback(h){this.mailerOnFallback=h}buildOrder(){let h=this.pickWeightedIndex(),z=[this.entries[h]?.transport],A=this.entries.map((q,B)=>({entry:q,index:B})).filter(({index:q})=>q!==h).sort((q,B)=>B.entry.weight-q.entry.weight);for(let{entry:q}of A)z.push(q.transport);return z}pickWeightedIndex(){let h=this.entries.reduce((A,q)=>A+q.weight,0),z=this.random()*h;for(let A=0;A<this.entries.length;A++){let q=this.entries[A];if(z-=q.weight,z<=0)return A}return this.entries.length-1}async send(h){let z=new E(this.buildOrder(),this.fallbackOptions);if(this.mailerOnFallback!==void 0)z.setMailerOnFallback(this.mailerOnFallback);return z.send(h)}async verify(){return this.inner.verify()}async verifyAll(){return this.inner.verifyAll()}async close(){await this.inner.close()}}export{J as WeightedFallbackTransport};
|
|
2
|
+
|
|
3
|
+
//# debugId=1AE5329B4BB01DFC64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/weighted-fallback.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * WeightedFallbackTransport — routes each send through a weighted-random\n * primary provider, then fails over through the remaining providers on error.\n * Useful for gradual traffic shifting, migrations, and A/B testing providers.\n *\n * @example\n * ```ts\n * import { WeightedFallbackTransport } from \"sently/transports/weighted-fallback\";\n * import { ResendTransport } from \"sently/transports/resend\";\n * import { SESTransport } from \"sently/transports/ses\";\n *\n * const transport = new WeightedFallbackTransport(\n * [\n * { transport: new ResendTransport({ apiKey }), weight: 80 },\n * { transport: new SESTransport({ accessKeyId, secretAccessKey }), weight: 20 },\n * ],\n * { cooldownMs: 300_000 },\n * );\n * ```\n */\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\nimport {\n type FallbackOptions,\n FallbackTransport,\n type FallbackVerifyAllResult,\n} from \"./fallback.js\";\n\n/** A transport entry with a relative routing weight. */\nexport interface WeightedTransportEntry {\n /** Transport instance. */\n transport: Transport;\n /** Relative weight (e.g. 80 vs 20 → ~80% of sends try this provider first). */\n weight: number;\n}\n\ntype MailerOnFallback = Parameters<FallbackTransport[\"setMailerOnFallback\"]>[0];\n\n/**\n * Weighted provider routing with failover on error.\n */\nexport class WeightedFallbackTransport implements Transport {\n readonly provider = \"weighted-fallback\";\n\n private readonly entries: WeightedTransportEntry[];\n private readonly fallbackOptions: FallbackOptions;\n private readonly random: () => number;\n private readonly inner: FallbackTransport;\n private mailerOnFallback: MailerOnFallback | undefined;\n\n /**\n * Creates a weighted fallback transport.\n * @throws {Error} When entries is empty or total weight is zero.\n */\n constructor(\n entries: WeightedTransportEntry[],\n options?: FallbackOptions & { random?: () => number },\n ) {\n if (entries.length === 0) {\n throw new Error(\"WeightedFallbackTransport requires at least one entry\");\n }\n\n const totalWeight = entries.reduce((sum, entry) => sum + entry.weight, 0);\n if (totalWeight <= 0) {\n throw new Error(\"WeightedFallbackTransport requires positive total weight\");\n }\n\n this.entries = entries;\n this.random = options?.random ?? Math.random;\n\n const cooldownState = new Map<string, number>();\n const { random: _random, ...rest } = options ?? {};\n this.fallbackOptions = { ...rest, cooldownState };\n\n this.inner = new FallbackTransport(\n entries.map((entry) => entry.transport),\n this.fallbackOptions,\n );\n }\n\n /**\n * Register a per-send onFallback callback from mailer lifecycle hooks.\n * @internal Used by {@link MailerImpl}; cleared after each send.\n */\n setMailerOnFallback(callback: MailerOnFallback | undefined): void {\n this.mailerOnFallback = callback;\n }\n\n private buildOrder(): Transport[] {\n const primaryIndex = this.pickWeightedIndex();\n const order: Transport[] = [this.entries[primaryIndex]?.transport as Transport];\n\n const remaining = this.entries\n .map((entry, index) => ({ entry, index }))\n .filter(({ index }) => index !== primaryIndex)\n .sort((a, b) => b.entry.weight - a.entry.weight);\n\n for (const { entry } of remaining) {\n order.push(entry.transport);\n }\n\n return order;\n }\n\n private pickWeightedIndex(): number {\n const total = this.entries.reduce((sum, entry) => sum + entry.weight, 0);\n let roll = this.random() * total;\n\n for (let i = 0; i < this.entries.length; i++) {\n const entry = this.entries[i] as WeightedTransportEntry;\n roll -= entry.weight;\n if (roll <= 0) {\n return i;\n }\n }\n\n return this.entries.length - 1;\n }\n\n /** Sends via a weighted-random primary provider, failing over on error. */\n async send(message: MailOptions): Promise<SendResult> {\n const transport = new FallbackTransport(this.buildOrder(), this.fallbackOptions);\n if (this.mailerOnFallback !== undefined) {\n transport.setMailerOnFallback(this.mailerOnFallback);\n }\n return transport.send(message);\n }\n\n /** Returns the first healthy provider in the chain. */\n async verify(): Promise<VerifyResult> {\n return this.inner.verify();\n }\n\n /** Verifies every weighted provider in definition order. */\n async verifyAll(): Promise<FallbackVerifyAllResult> {\n return this.inner.verifyAll();\n }\n\n /** Closes every underlying transport. */\n async close(): Promise<void> {\n await this.inner.close();\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "+IAyCO,CAAM,JAA+C,JACjD,SAAW,oBAEH,QACA,gBACA,OACA,MACT,iBAMR,WAAW,CACT,EACA,EACA,CACA,GAAI,EAAQ,SAAW,EACrB,MAAU,MAAM,uDAAuD,EAIzE,GADoB,EAAQ,OAAO,CAAC,EAAK,IAAU,EAAM,EAAM,OAAQ,CAAC,GACrD,EACjB,MAAU,MAAM,0DAA0D,EAG5E,KAAK,QAAU,EACf,KAAK,OAAS,GAAS,QAAU,KAAK,OAEtC,IAAM,EAAgB,IAAI,KAClB,OAAQ,KAAY,GAAS,GAAW,CAAC,EACjD,KAAK,gBAAkB,IAAK,EAAM,eAAc,EAEhD,KAAK,MAAQ,IAAI,EACf,EAAQ,IAAI,CAAC,IAAU,EAAM,SAAS,EACtC,KAAK,eACP,EAOF,mBAAmB,CAAC,EAA8C,CAChE,KAAK,iBAAmB,EAGlB,UAAU,EAAgB,CAChC,IAAM,EAAe,KAAK,kBAAkB,EACtC,EAAqB,CAAC,KAAK,QAAQ,IAAe,SAAsB,EAExE,EAAY,KAAK,QACpB,IAAI,CAAC,EAAO,KAAW,CAAE,QAAO,OAAM,EAAE,EACxC,OAAO,EAAG,WAAY,IAAU,CAAY,EAC5C,KAAK,CAAC,EAAG,IAAM,EAAE,MAAM,OAAS,EAAE,MAAM,MAAM,EAEjD,QAAa,WAAW,EACtB,EAAM,KAAK,EAAM,SAAS,EAG5B,OAAO,EAGD,iBAAiB,EAAW,CAClC,IAAM,EAAQ,KAAK,QAAQ,OAAO,CAAC,EAAK,IAAU,EAAM,EAAM,OAAQ,CAAC,EACnE,EAAO,KAAK,OAAO,EAAI,EAE3B,QAAS,EAAI,EAAG,EAAI,KAAK,QAAQ,OAAQ,IAAK,CAC5C,IAAM,EAAQ,KAAK,QAAQ,GAE3B,GADA,GAAQ,EAAM,OACV,GAAQ,EACV,OAAO,EAIX,OAAO,KAAK,QAAQ,OAAS,OAIzB,KAAI,CAAC,EAA2C,CACpD,IAAM,EAAY,IAAI,EAAkB,KAAK,WAAW,EAAG,KAAK,eAAe,EAC/E,GAAI,KAAK,mBAAqB,OAC5B,EAAU,oBAAoB,KAAK,gBAAgB,EAErD,OAAO,EAAU,KAAK,CAAO,OAIzB,OAAM,EAA0B,CACpC,OAAO,KAAK,MAAM,OAAO,OAIrB,UAAS,EAAqC,CAClD,OAAO,KAAK,MAAM,UAAU,OAIxB,MAAK,EAAkB,CAC3B,MAAM,KAAK,MAAM,MAAM,EAE3B",
|
|
8
|
+
"debugId": "1AE5329B4BB01DFC64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Meta WhatsApp Cloud API transport.
|
|
4
|
+
*
|
|
5
|
+
* Sently-first: wire into {@link createWhatsAppSender}. Other WhatsApp providers
|
|
6
|
+
* (e.g. `TaqnyatWhatsAppTransport`) are separate transports with the same contract.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createWhatsAppSender } from "sently/whatsapp";
|
|
11
|
+
* import { WhatsAppCloudTransport } from "sently/transports/whatsapp-cloud";
|
|
12
|
+
*
|
|
13
|
+
* const wa = createWhatsAppSender({
|
|
14
|
+
* transport: new WhatsAppCloudTransport({
|
|
15
|
+
* accessToken: process.env.WHATSAPP_TOKEN!,
|
|
16
|
+
* phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
|
|
17
|
+
* }),
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { SentlyError } from "../core/errors.js";
|
|
22
|
+
import type { VerifyResult } from "../core/types.js";
|
|
23
|
+
import type { WhatsAppOptions, WhatsAppSendResult, WhatsAppTransport } from "../core/whatsapp-types.js";
|
|
24
|
+
/** Default Graph API version (current as of 2026-08). */
|
|
25
|
+
export declare const WHATSAPP_CLOUD_DEFAULT_API_VERSION = "v26.0";
|
|
26
|
+
/** WhatsApp Cloud API configuration. */
|
|
27
|
+
export interface WhatsAppCloudConfig {
|
|
28
|
+
/** Meta Graph API access token. */
|
|
29
|
+
accessToken: string;
|
|
30
|
+
/** WhatsApp Business phone number ID. */
|
|
31
|
+
phoneNumberId: string;
|
|
32
|
+
/** Graph API version (e.g. `"v26.0"`). Default: {@link WHATSAPP_CLOUD_DEFAULT_API_VERSION}. */
|
|
33
|
+
apiVersion?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Error thrown when the WhatsApp Cloud API returns a non-success response. */
|
|
36
|
+
export declare class WhatsAppCloudError extends SentlyError {
|
|
37
|
+
readonly statusCode: number;
|
|
38
|
+
readonly apiError: unknown;
|
|
39
|
+
/** Creates a WhatsApp Cloud API error with status code and response payload. */
|
|
40
|
+
constructor(message: string, statusCode: number, apiError: unknown);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* WhatsApp Cloud API transport (JSON Bearer).
|
|
44
|
+
*/
|
|
45
|
+
export declare class WhatsAppCloudTransport implements WhatsAppTransport {
|
|
46
|
+
readonly provider = "whatsapp-cloud";
|
|
47
|
+
private readonly accessToken;
|
|
48
|
+
private readonly phoneNumberId;
|
|
49
|
+
private readonly apiVersion;
|
|
50
|
+
/** Creates a WhatsApp Cloud API transport. */
|
|
51
|
+
constructor(config: WhatsAppCloudConfig);
|
|
52
|
+
/** Sends a WhatsApp template or text message. */
|
|
53
|
+
send(options: WhatsAppOptions): Promise<WhatsAppSendResult>;
|
|
54
|
+
/** Lightweight credential check. */
|
|
55
|
+
verify(): Promise<VerifyResult>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{ca as x,da as z}from"../chunk-tamww15j.js";import"../chunk-6npp3x3c.js";var G="v26.0";class B extends x{statusCode;apiError;constructor(j,k,q){super(j,z(k),{statusCode:k,provider:"whatsapp-cloud",cause:q});this.statusCode=k;this.apiError=q;this.name="WhatsAppCloudError"}}function J(j){return"template"in j}class K{provider="whatsapp-cloud";accessToken;phoneNumberId;apiVersion;constructor(j){this.accessToken=j.accessToken,this.phoneNumberId=j.phoneNumberId,this.apiVersion=j.apiVersion??G}async send(j){let k=`https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`,q=J(j)?{messaging_product:"whatsapp",to:j.to,type:"template",template:{name:j.template.name,language:{code:j.template.language},...j.template.components?{components:j.template.components}:{}}}:{messaging_product:"whatsapp",to:j.to,type:"text",text:{body:j.text}},v=await fetch(k,{method:"POST",headers:{Authorization:`Bearer ${this.accessToken}`,"Content-Type":"application/json"},body:JSON.stringify(q)}),w=await v.json();if(!v.ok)throw new B(w.error?.message??"WhatsApp Cloud API error",v.status,w);return{messageId:w.messages?.[0]?.id??j.messageId??"",to:j.to,status:"accepted",response:"Message sent",provider:"whatsapp-cloud"}}async verify(){return{ok:Boolean(this.accessToken&&this.phoneNumberId),provider:"whatsapp-cloud",message:this.accessToken&&this.phoneNumberId?"Credentials present":"Missing credentials"}}}export{K as WhatsAppCloudTransport,B as WhatsAppCloudError,G as WHATSAPP_CLOUD_DEFAULT_API_VERSION};
|
|
2
|
+
|
|
3
|
+
//# debugId=D51060C2D4F4831264756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/whatsapp-cloud.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
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
|
+
],
|
|
7
|
+
"mappings": "oFA6BO,DAAM,EAAqC,QAa3C,MAAM,UAA2B,CAAY,CAIhC,WACA,SAHlB,WAAW,CACT,EACgB,EACA,EAChB,CACA,MAAM,EAAS,EAAuB,CAAU,EAAG,CACjD,aACA,SAAU,iBACV,MAAO,CACT,CAAC,EAPe,kBACA,gBAOhB,KAAK,KAAO,qBAEhB,CAEA,SAAS,CAAiB,CACxB,EAC4D,CAC5D,MAAO,aAAc,EAMhB,MAAM,CAAoD,CACtD,SAAW,iBAEH,YACA,cACA,WAGjB,WAAW,CAAC,EAA6B,CACvC,KAAK,YAAc,EAAO,YAC1B,KAAK,cAAgB,EAAO,cAC5B,KAAK,WAAa,EAAO,YAAc,OAInC,KAAI,CAAC,EAAuD,CAChE,IAAM,EAAM,8BAA8B,KAAK,cAAc,KAAK,yBAE5D,EAAO,EAAkB,CAAO,EAClC,CACE,kBAAmB,WACnB,GAAI,EAAQ,GACZ,KAAM,WACN,SAAU,CACR,KAAM,EAAQ,SAAS,KACvB,SAAU,CAAE,KAAM,EAAQ,SAAS,QAAS,KACxC,EAAQ,SAAS,WAAa,CAAE,WAAY,EAAQ,SAAS,UAAW,EAAI,CAAC,CACnF,CACF,EACA,CACE,kBAAmB,WACnB,GAAI,EAAQ,GACZ,KAAM,OACN,KAAM,CAAE,KAAM,EAAQ,IAAK,CAC7B,EAEE,EAAW,MAAM,MAAM,EAAK,CAChC,OAAQ,OACR,QAAS,CACP,cAAe,UAAU,KAAK,cAC9B,eAAgB,kBAClB,EACA,KAAM,KAAK,UAAU,CAAI,CAC3B,CAAC,EAEK,EAAW,MAAM,EAAS,KAAK,EAUrC,GAAI,CAAC,EAAS,GACZ,MAAM,IAAI,EACR,EAAQ,OAAO,SAAW,2BAC1B,EAAS,OACT,CACF,EAKF,MAAO,CACL,UAHS,EAAQ,WAAW,IAAI,IAAM,EAAQ,WAAa,GAI3D,GAAI,EAAQ,GACZ,OAAQ,WACR,SAAU,eACV,SAAU,gBACZ,OAII,OAAM,EAA0B,CACpC,MAAO,CACL,GAAI,QAAQ,KAAK,aAAe,KAAK,aAAa,EAClD,SAAU,iBACV,QACE,KAAK,aAAe,KAAK,cAAgB,sBAAwB,qBACrE,EAEJ",
|
|
8
|
+
"debugId": "D51060C2D4F4831264756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/dist/webhooks/brevo.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{d as F,e as u,f as G,g as H}from"../chunk-q8k3m6qe.js";import"../chunk-
|
|
1
|
+
import{d as F,e as u,f as G,g as H}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var K={delivered:"delivered",hard_bounce:"bounced",hardBounce:"bounced",soft_bounce:"bounced",soft_bounced:"bounced",softBounce:"bounced",blocked:"bounced",invalid:"bounced",invalid_email:"bounced",deferred:"deferred",spam:"complained",opened:"opened",unique_opened:"opened",uniqueOpened:"opened",click:"clicked",clicked:"clicked",unique_click:"clicked",uniqueClicked:"clicked",sent:"unknown",request:"unknown",unsubscribe:"complained",unsubscribed:"complained"};function L(f){return u(f.ts_event)??u(f.ts)??u(f.date_event)??u(f.date)??u(f.ts_epoch!==void 0?Number(f.ts_epoch):void 0)}function Q(f){if(typeof f["message-id"]==="string")return f["message-id"];if(typeof f.messageId==="string")return f.messageId;return}function U(f){return G(f).flatMap((x)=>{if(typeof x!=="object"||x===null)return[];let q=x,J=typeof q.event==="string"?q.event:"",C=L(q),D=Q(q);return[H({provider:"brevo",type:F(J,K),...D!==void 0?{messageId:D}:{},...typeof q.email==="string"?{recipient:q.email}:{},...C!==void 0?{timestamp:C}:{},raw:x})]})}export{U as parse};
|
|
2
2
|
|
|
3
3
|
//# debugId=916BC5D0AADEB5C764756E2164756E21
|
package/dist/webhooks/mailgun.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{a as G,c as H}from"../chunk-pqp843mh.js";import{d as Y,e as Z,g as $}from"../chunk-q8k3m6qe.js";import"../chunk-
|
|
1
|
+
import{a as G,c as H}from"../chunk-pqp843mh.js";import{d as Y,e as Z,g as $}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var R={accepted:"unknown",delivered:"delivered",rejected:"bounced",complained:"complained",opened:"opened",clicked:"clicked",stored:"unknown",unsubscribed:"unknown"};function U(z){let J=typeof z.event==="string"?z.event:"";if(J==="failed")return z.severity==="temporary"?"deferred":"bounced";return Y(J,R)}function b(z){if(!z)return;let J=z.headers;if(typeof J==="object"&&J!==null){let C=J["message-id"];if(typeof C==="string")return C}return typeof z["message-id"]==="string"?z["message-id"]:void 0}function N(z){if(typeof z!=="object"||z===null)return[];let J=z,C=typeof J["event-data"]==="object"&&J["event-data"]!==null?J["event-data"]:J,O=typeof C.message==="object"&&C.message!==null?C.message:void 0,Q=b(O),W=Z(C.timestamp);return[$({provider:"mailgun",type:U(C),...Q!==void 0?{messageId:Q}:{},...typeof C.recipient==="string"?{recipient:C.recipient}:{},...W!==void 0?{timestamp:W}:{},raw:z})]}async function q(z,J,C,O){let Q=await crypto.subtle.importKey("raw",new TextEncoder().encode(O),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),W=new TextEncoder().encode(z+J),L=await crypto.subtle.sign("HMAC",Q,W),P=new Uint8Array(L),X=H(C);if(X===null)return!1;return G(P,X)}async function S(z,J){if(typeof z!=="object"||z===null)return!1;let C=z.signature;if(typeof C!=="object"||C===null)return!1;let O=C;if(typeof O.timestamp!=="string"||typeof O.token!=="string"||typeof O.signature!=="string")return!1;return q(O.timestamp,O.token,O.signature,J)}export{q as verifySignature,S as verifyPayload,N as parse};
|
|
2
2
|
|
|
3
3
|
//# debugId=7FB21CD9E29B4FE264756E2164756E21
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{d as u,e as q,g as x}from"../chunk-q8k3m6qe.js";import"../chunk-
|
|
1
|
+
import{d as u,e as q,g as x}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var C={Delivery:"delivered",Bounce:"bounced",SpamComplaint:"complained",Open:"opened",Click:"clicked",SubscriptionChange:"unknown",Inbound:"unknown"};function F(g){if(typeof g!=="object"||g===null)return[];let f=g,B=typeof f.RecordType==="string"?f.RecordType:"";return[x({provider:"postmark",type:u(B,C),...typeof f.MessageID==="string"?{messageId:f.MessageID}:{},...typeof f.Recipient==="string"?{recipient:f.Recipient}:typeof f.Email==="string"?{recipient:f.Email}:{},...q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)!==void 0?{timestamp:q(f.DeliveredAt??f.BouncedAt??f.ReceivedAt??f.ChangedAt)}:{},raw:g})]}export{F as parse};
|
|
2
2
|
|
|
3
3
|
//# debugId=7CD05BB54D484C3864756E2164756E21
|
package/dist/webhooks/resend.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{a as $,b as A}from"../chunk-pqp843mh.js";import{d as X,e as Y,g as Z}from"../chunk-q8k3m6qe.js";import"../chunk-
|
|
1
|
+
import{a as $,b as A}from"../chunk-pqp843mh.js";import{d as X,e as Y,g as Z}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var j={"email.sent":"unknown","email.scheduled":"unknown","email.delivered":"delivered","email.delivery_delayed":"deferred","email.bounced":"bounced","email.failed":"bounced","email.complained":"complained","email.opened":"opened","email.clicked":"clicked","email.received":"unknown","email.suppressed":"unknown"};function k(G){let F=G.startsWith("whsec_")?G.slice(6):G,K=atob(F),J=new Uint8Array(K.length);for(let z=0;z<K.length;z++)J[z]=K.charCodeAt(z);return J.buffer}function H(G){if(typeof G!=="object"||G===null)return[];let F=G,K=typeof F.type==="string"?F.type:"",J=typeof F.data==="object"&&F.data!==null?F.data:{},z=J.to,L=Array.isArray(z)?typeof z[0]==="string"?z[0]:void 0:typeof z==="string"?z:void 0,O=Y(F.created_at??J.created_at);return[Z({provider:"resend",type:X(K,j),...typeof J.email_id==="string"?{messageId:J.email_id}:{},...L!==void 0?{recipient:L}:{},...O!==void 0?{timestamp:O}:{},raw:G})]}async function I(G,F,K){let J=F["svix-id"],z=F["svix-timestamp"],L=F["svix-signature"];if(!J||!z||!L)return!1;let O=`${J}.${z}.${G}`,D=await crypto.subtle.importKey("raw",k(K),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),M=await crypto.subtle.sign("HMAC",D,new TextEncoder().encode(O)),P=new Uint8Array(M);for(let W of L.split(" ")){let Q=W.split(",",2)[1];if(Q===void 0)continue;let U=A(Q);if(U===null)continue;if($(P,U))return!0}return!1}export{I as verifySignature,H as parse};
|
|
2
2
|
|
|
3
3
|
//# debugId=2EA62D07463E258664756E2164756E21
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{d as w,e as x,f as B,g as C}from"../chunk-q8k3m6qe.js";import"../chunk-
|
|
1
|
+
import{d as w,e as x,f as B,g as C}from"../chunk-q8k3m6qe.js";import"../chunk-6npp3x3c.js";var F={processed:"unknown",delivered:"delivered",deferred:"deferred",bounce:"bounced",dropped:"bounced",spamreport:"complained",unsubscribe:"complained",group_unsubscribe:"complained",group_resubscribe:"unknown",open:"opened",click:"clicked"};function H(q){let g=typeof q.event==="string"?q.event:"";if(g==="bounce"&&q.type==="blocked")return"deferred";return w(g,F)}function K(q){return B(q).flatMap((g)=>{if(typeof g!=="object"||g===null)return[];let f=g,u=x(f.timestamp);return[C({provider:"sendgrid",type:H(f),...typeof f.sg_message_id==="string"?{messageId:f.sg_message_id}:typeof f["smtp-id"]==="string"?{messageId:f["smtp-id"]}:{},...typeof f.email==="string"?{recipient:f.email}:{},...u!==void 0?{timestamp:u}:{},raw:g})]})}export{K as parse};
|
|
2
2
|
|
|
3
3
|
//# debugId=D4C7DDEE719D449764756E2164756E21
|