sently 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [1.2.1] — 2026-08-04
6
+
7
+ ### ✨ Added
8
+
9
+ - **Taqnyat Verify OTP live verified** — production `sendOtp` → handset code →
10
+ `verifyOtp` confirmed; docs OTP tab shows the green `LiveVerified` callout
11
+ alongside SMS, WhatsApp, and Email
12
+
13
+ ### ♻️ Changed
14
+
15
+ - **Hostinger SMTP unified** — SMTP settings now come from the same name via
16
+ function overloading: `HostingerTransport({ user, pass })` returns a ready
17
+ `SMTPConfig` for `createSMTPMailer`, while `new HostingerTransport({ token,
18
+ mailbox })` stays the Mail API transport; IntelliSense narrows options and
19
+ the return type by config shape; `hostingerSmtpConfig()` remains as a 1.x
20
+ compatibility alias
21
+
22
+ ### 🐛 Fixed
23
+
24
+ - **Taqnyat Verify OTP parsing** — live `returnJson: 1` responses put the docs
25
+ result code in `Data.result` and use top-level `status: 1` as a transport
26
+ envelope; `sendOtp` / `verifyOtp` now read `Data.result` (and
27
+ `Data.MessageEn` / `Data.MessageAr`) so a successful send is no longer
28
+ misreported as code `1` (invalid apiKey)
29
+
3
30
  ## [1.2.0] — 2026-08-03
4
31
 
5
32
  ### ✨ Added
@@ -2,7 +2,7 @@ import { SentlyError } from "../core/errors.js";
2
2
  import type { MailOptions, SendResult, SMTPConfig, Transport, VerifyResult } from "../core/types.js";
3
3
  /** Hostinger SMTP hostname. */
4
4
  export declare const HOSTINGER_SMTP_HOST = "smtp.hostinger.com";
5
- /** SSL/TLS-on-connect submission port (default for {@link hostingerSmtpConfig}). */
5
+ /** SSL/TLS-on-connect submission port (default for {@link HostingerTransport} SMTP). */
6
6
  export declare const HOSTINGER_SMTP_PORT_SSL = 465;
7
7
  /** STARTTLS submission port. */
8
8
  export declare const HOSTINGER_SMTP_PORT_STARTTLS = 587;
@@ -19,7 +19,7 @@ export interface HostingerConfig {
19
19
  }
20
20
  /**
21
21
  * Ready SMTP options for Hostinger Email.
22
- * Pass the result of {@link hostingerSmtpConfig} to `createSMTPMailer`.
22
+ * Pass `HostingerTransport({ user, pass })` to `createSMTPMailer`.
23
23
  */
24
24
  export interface HostingerSmtpOptions {
25
25
  /** Full mailbox address — this is the SMTP username. */
@@ -58,21 +58,8 @@ export interface HostingerMessageRef {
58
58
  * Build a ready {@link SMTPConfig} for Hostinger Email.
59
59
  *
60
60
  * Defaults to port `465` with `secure: true`. Hostinger supports `465` and
61
- * `587` only — not `2525`.
62
- *
63
- * @example
64
- * ```ts
65
- * import { createSMTPMailer } from "sently/smtp";
66
- * import { hostingerSmtpConfig } from "sently/transports/hostinger";
67
- *
68
- * const mailer = await createSMTPMailer(
69
- * hostingerSmtpConfig({
70
- * user: "you@yourdomain.com",
71
- * pass: process.env.HOSTINGER_SMTP_PASSWORD!,
72
- * // port: 587, // STARTTLS instead of SSL
73
- * }),
74
- * );
75
- * ```
61
+ * `587` only — not `2525`. Prefer `HostingerTransport({ user, pass })`; this
62
+ * alias stays for 1.x compatibility.
76
63
  */
77
64
  export declare function hostingerSmtpConfig(options: HostingerSmtpOptions): SMTPConfig;
78
65
  /** Error thrown when the Hostinger Mail API returns a non-success response. */
@@ -83,15 +70,10 @@ export declare class HostingerError extends SentlyError {
83
70
  constructor(message: string, statusCode: number, apiError: unknown);
84
71
  }
85
72
  /**
86
- * Hostinger Mail API transport.
87
- *
88
- * Sends through `POST /api/v1/mailboxes/{mailbox}/send`. There is no batch
89
- * endpoint — {@link Mailer.sendBulk} falls back to individual sends.
90
- *
91
- * For SMTP relay, use {@link hostingerSmtpConfig} with `createSMTPMailer`
92
- * instead of this class.
73
+ * Hostinger Mail API transport instance type (`new` / call with API config).
74
+ * Constructed by the {@link HostingerTransport} overload — not exported directly.
93
75
  */
94
- export declare class HostingerTransport implements Transport {
76
+ declare class HostingerTransportImpl implements Transport {
95
77
  readonly provider = "hostinger";
96
78
  /** Hostinger Mail API token for Bearer authentication. */
97
79
  private readonly token;
@@ -126,3 +108,30 @@ export declare class HostingerTransport implements Transport {
126
108
  /** Verifies the API token and that the configured mailbox is in its scope. */
127
109
  verify(): Promise<VerifyResult>;
128
110
  }
111
+ /**
112
+ * Hostinger Mail API transport instance (returned by {@link HostingerTransport}
113
+ * overloads for `{ token, mailbox }` config).
114
+ */
115
+ export type HostingerMailTransport = HostingerTransportImpl;
116
+ /**
117
+ * Function-overloaded constructor for `HostingerTransport`:
118
+ * - `new` / call with {@link HostingerConfig} → {@link HostingerMailTransport}
119
+ * - call with {@link HostingerSmtpOptions} → {@link SMTPConfig}
120
+ */
121
+ export interface HostingerTransportOverloads {
122
+ new (config: HostingerConfig): HostingerMailTransport;
123
+ (config: HostingerConfig): HostingerMailTransport;
124
+ (config: HostingerSmtpOptions): SMTPConfig;
125
+ }
126
+ /**
127
+ * Hostinger email — one name, two shapes (function overloading).
128
+ *
129
+ * - **Mail API** — `new HostingerTransport({ token, mailbox })` (or the same
130
+ * call without `new`) → a `Transport` for `createMailer`.
131
+ * - **SMTP** — `HostingerTransport({ user, pass })` → a ready `SMTPConfig`
132
+ * for `createSMTPMailer`.
133
+ *
134
+ * IntelliSense narrows options and the return type by the config shape.
135
+ */
136
+ export declare const HostingerTransport: HostingerTransportOverloads;
137
+ export {};
@@ -1,3 +1,3 @@
1
- import{K as k}from"../chunk-1ke3nmyg.js";import{S as W,U as J}from"../chunk-8kpgbrba.js";import{da as Y}from"../chunk-hnzmn4s4.js";import{ja as Z,ka as $}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";var U="smtp.hostinger.com",X=465,j=587,V="https://api.mail.hostinger.com";function G(q){let z=q.port??X;return{host:U,port:z,secure:z===X,auth:{user:q.user,pass:q.pass},...q.pool!==void 0?{pool:q.pool}:{},...q.maxConnections!==void 0?{maxConnections:q.maxConnections}:{}}}class Q extends Z{statusCode;apiError;constructor(q,z,F){super(q,$(z),{statusCode:z,provider:"hostinger",cause:F});this.statusCode=z;this.apiError=F;this.name="HostingerError"}}class v{provider="hostinger";token;mailbox;baseUrl;constructor(q){this.token=q.token,this.mailbox=q.mailbox,this.baseUrl=q.baseUrl??V}async listMailboxes(){let q=await fetch(`${this.baseUrl}/api/v1/me`,{headers:{Authorization:`Bearer ${this.token}`}}),z=await q.json().catch(()=>({}));if(!q.ok)throw new Q(z.error??`Hostinger API error (HTTP ${q.status})`,q.status,z);return z.data?.mailboxes??[]}async buildEmailBody(q,z){if(z?.inReplyTo&&z.forwardOf)throw new Q("inReplyTo and forwardOf are mutually exclusive",422,{code:"MUTUALLY_EXCLUSIVE",params:{inReplyTo:["conflicts with forwardOf"]}});let F=await k(q.attachments),K=W(q.from)[0];return{to:J(q.to),...K?.name?{displayName:K.name}:{},...q.cc?{cc:J(q.cc)}:{},...q.bcc?{bcc:J(q.bcc)}:{},subject:q.subject,...q.text?{text:q.text}:{},...q.html?{html:q.html}:{},...F.length>0?{attachments:F.map((D)=>({filename:D.filename,content:D.content instanceof Uint8Array||typeof D.content==="string"?Y(D.content).replace(/\r\n/g,""):"",...D.contentType?{contentType:D.contentType}:{},...D.contentId?{cid:D.contentId.replace(/^<|>$/g,"")}:{}}))}:{},...z?.inReplyTo?{inReplyTo:z.inReplyTo}:{},...z?.forwardOf?{forwardOf:z.forwardOf}:{}}}toSendResult(q){let z=W(q.from)[0];return{messageId:q.messageId??"",accepted:J(q.to),rejected:[],response:"Message sent and saved to the Sent folder",envelope:{from:z?.address??"",to:[...J(q.to),...q.cc?J(q.cc):[],...q.bcc?J(q.bcc):[]]}}}async postSend(q,z){let F=await this.buildEmailBody(q,z),K=await fetch(`${this.baseUrl}/api/v1/mailboxes/${encodeURIComponent(this.mailbox)}/send`,{method:"POST",headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:JSON.stringify(F)});if(K.ok)return this.toSendResult(q);let D=await K.json().catch(()=>({}));throw new Q(D.error??`Hostinger API error (HTTP ${K.status})`,K.status,D)}async send(q){return this.postSend(q)}async sendReply(q,z){return this.postSend(q,{inReplyTo:z})}async sendForward(q,z){return this.postSend(q,{forwardOf:z})}async verify(){try{let q=await this.listMailboxes(),z=q.find((F)=>F.resourceId===this.mailbox);if(!z)return{ok:!1,provider:"hostinger",message:`Mailbox "${this.mailbox}" is not in this token's scope`,raw:q};return{ok:!0,provider:"hostinger",message:`API token is valid — sending as ${z.address}`,raw:q}}catch(q){if(q instanceof Q)return{ok:!1,provider:"hostinger",message:q.message};return{ok:!1,provider:"hostinger",message:q instanceof Error?q.message:String(q)}}}}export{G as hostingerSmtpConfig,v as HostingerTransport,Q as HostingerError,j as HOSTINGER_SMTP_PORT_STARTTLS,X as HOSTINGER_SMTP_PORT_SSL,U as HOSTINGER_SMTP_HOST,V as HOSTINGER_API_BASE_URL};
1
+ import{K as $}from"../chunk-1ke3nmyg.js";import{S as V,U as J}from"../chunk-8kpgbrba.js";import{da as X}from"../chunk-hnzmn4s4.js";import{ja as Y,ka as Z}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";var U="smtp.hostinger.com",W=465,A=587,B="https://api.mail.hostinger.com";function v(q){let z=q.port??W;return{host:U,port:z,secure:z===W,auth:{user:q.user,pass:q.pass},...q.pool!==void 0?{pool:q.pool}:{},...q.maxConnections!==void 0?{maxConnections:q.maxConnections}:{}}}class Q extends Y{statusCode;apiError;constructor(q,z,F){super(q,Z(z),{statusCode:z,provider:"hostinger",cause:F});this.statusCode=z;this.apiError=F;this.name="HostingerError"}}class k{provider="hostinger";token;mailbox;baseUrl;constructor(q){this.token=q.token,this.mailbox=q.mailbox,this.baseUrl=q.baseUrl??B}async listMailboxes(){let q=await fetch(`${this.baseUrl}/api/v1/me`,{headers:{Authorization:`Bearer ${this.token}`}}),z=await q.json().catch(()=>({}));if(!q.ok)throw new Q(z.error??`Hostinger API error (HTTP ${q.status})`,q.status,z);return z.data?.mailboxes??[]}async buildEmailBody(q,z){if(z?.inReplyTo&&z.forwardOf)throw new Q("inReplyTo and forwardOf are mutually exclusive",422,{code:"MUTUALLY_EXCLUSIVE",params:{inReplyTo:["conflicts with forwardOf"]}});let F=await $(q.attachments),K=V(q.from)[0];return{to:J(q.to),...K?.name?{displayName:K.name}:{},...q.cc?{cc:J(q.cc)}:{},...q.bcc?{bcc:J(q.bcc)}:{},subject:q.subject,...q.text?{text:q.text}:{},...q.html?{html:q.html}:{},...F.length>0?{attachments:F.map((D)=>({filename:D.filename,content:D.content instanceof Uint8Array||typeof D.content==="string"?X(D.content).replace(/\r\n/g,""):"",...D.contentType?{contentType:D.contentType}:{},...D.contentId?{cid:D.contentId.replace(/^<|>$/g,"")}:{}}))}:{},...z?.inReplyTo?{inReplyTo:z.inReplyTo}:{},...z?.forwardOf?{forwardOf:z.forwardOf}:{}}}toSendResult(q){let z=V(q.from)[0];return{messageId:q.messageId??"",accepted:J(q.to),rejected:[],response:"Message sent and saved to the Sent folder",envelope:{from:z?.address??"",to:[...J(q.to),...q.cc?J(q.cc):[],...q.bcc?J(q.bcc):[]]}}}async postSend(q,z){let F=await this.buildEmailBody(q,z),K=await fetch(`${this.baseUrl}/api/v1/mailboxes/${encodeURIComponent(this.mailbox)}/send`,{method:"POST",headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:JSON.stringify(F)});if(K.ok)return this.toSendResult(q);let D=await K.json().catch(()=>({}));throw new Q(D.error??`Hostinger API error (HTTP ${K.status})`,K.status,D)}async send(q){return this.postSend(q)}async sendReply(q,z){return this.postSend(q,{inReplyTo:z})}async sendForward(q,z){return this.postSend(q,{forwardOf:z})}async verify(){try{let q=await this.listMailboxes(),z=q.find((F)=>F.resourceId===this.mailbox);if(!z)return{ok:!1,provider:"hostinger",message:`Mailbox "${this.mailbox}" is not in this token's scope`,raw:q};return{ok:!0,provider:"hostinger",message:`API token is valid — sending as ${z.address}`,raw:q}}catch(q){if(q instanceof Q)return{ok:!1,provider:"hostinger",message:q.message};return{ok:!1,provider:"hostinger",message:q instanceof Error?q.message:String(q)}}}}var w=function(q){if("token"in q)return new k(q);return v(q)};export{v as hostingerSmtpConfig,w as HostingerTransport,Q as HostingerError,A as HOSTINGER_SMTP_PORT_STARTTLS,W as HOSTINGER_SMTP_PORT_SSL,U as HOSTINGER_SMTP_HOST,B as HOSTINGER_API_BASE_URL};
2
2
 
3
- //# debugId=5E9B71EE50CFF7E764756E2164756E21
3
+ //# debugId=31B59878CFD615A464756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/transports/hostinger.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @module\n * Hostinger email — Mail API transport (`api.mail.hostinger.com`) plus ready\n * SMTP config for `createSMTPMailer` (`smtp.hostinger.com`, ports 465 / 587).\n *\n * The Mail API sends from the managed mailbox itself, so `MailOptions.from`\n * only contributes the sender display name. Vendor extras (`listMailboxes`,\n * `sendReply`, `sendForward`) stay on this class — never on the channel sender.\n *\n * @example Mail API\n * ```ts\n * import { HostingerTransport } from \"sently/transports/hostinger\";\n * import { createMailer } from \"sently/mailer\";\n *\n * const hostinger = new HostingerTransport({\n * token: process.env.HOSTINGER_API_TOKEN!,\n * mailbox: process.env.HOSTINGER_MAILBOX_ID!, // e.g. \"AC1a2b3c4d5e6f7g\"\n * });\n * const mailer = await createMailer({ transport: hostinger });\n *\n * await mailer.send({\n * from: \"you@yourdomain.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * html: \"<p>Sent via Hostinger</p>\",\n * });\n * ```\n *\n * @example SMTP\n * ```ts\n * import { createSMTPMailer } from \"sently/smtp\";\n * import { hostingerSmtpConfig } from \"sently/transports/hostinger\";\n *\n * const mailer = await createSMTPMailer(\n * hostingerSmtpConfig({\n * user: \"you@yourdomain.com\",\n * pass: process.env.HOSTINGER_SMTP_PASSWORD!,\n * }),\n * );\n * ```\n */\nimport { extractEmails, parseAddresses } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPConfig,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Hostinger SMTP hostname. */\nexport const HOSTINGER_SMTP_HOST = \"smtp.hostinger.com\";\n\n/** SSL/TLS-on-connect submission port (default for {@link hostingerSmtpConfig}). */\nexport const HOSTINGER_SMTP_PORT_SSL = 465;\n\n/** STARTTLS submission port. */\nexport const HOSTINGER_SMTP_PORT_STARTTLS = 587;\n\n/** Default Hostinger Mail API base URL. */\nexport const HOSTINGER_API_BASE_URL = \"https://api.mail.hostinger.com\";\n\n/** Hostinger Mail API configuration. */\nexport interface HostingerConfig {\n /** API token from hPanel → Emails → Agentic Mail → API access (shown once at creation). */\n token: string;\n /** Resource ID of the managed mailbox to send from (e.g. `\"AC1a2b3c4d5e6f7g\"`). */\n mailbox: string;\n /** API base URL. Default: {@link HOSTINGER_API_BASE_URL}. */\n baseUrl?: string;\n}\n\n/**\n * Ready SMTP options for Hostinger Email.\n * Pass the result of {@link hostingerSmtpConfig} to `createSMTPMailer`.\n */\nexport interface HostingerSmtpOptions {\n /** Full mailbox address — this is the SMTP username. */\n user: string;\n /** Mailbox password from hPanel → Emails → Configuration settings. */\n pass: string;\n /**\n * Submission port.\n * - `465` — SSL/TLS on connect (default)\n * - `587` — STARTTLS\n */\n port?: 465 | 587;\n /** Enable the SMTP connection pool. Default: `false`. */\n pool?: boolean;\n /** Max simultaneous SMTP connections when `pool` is true. Default: `5`. */\n maxConnections?: number;\n}\n\n/** A mailbox the API token can manage, as returned by {@link HostingerTransport.listMailboxes}. */\nexport interface HostingerMailbox {\n /** Mailbox resource ID — pass it as {@link HostingerConfig.mailbox}. */\n resourceId: string;\n /** Email address of the mailbox. */\n address: string;\n}\n\n/**\n * Reference to a source message by IMAP UID within a folder.\n * Used by {@link HostingerTransport.sendReply} and {@link HostingerTransport.sendForward}.\n */\nexport interface HostingerMessageRef {\n /** Folder containing the source message (e.g. `\"INBOX\"`). */\n folder: string;\n /** IMAP UID of the source message. */\n uid: number;\n}\n\n/** Error envelope returned by the Hostinger Mail API on non-success responses. */\ninterface HostingerErrorEnvelope {\n error?: string;\n code?: string;\n params?: Record<string, unknown>;\n}\n\n/** Optional reply / forward threading fields for the Mail API send body. */\ninterface HostingerSendExtras {\n inReplyTo?: HostingerMessageRef;\n forwardOf?: HostingerMessageRef;\n}\n\n/**\n * Build a ready {@link SMTPConfig} for Hostinger Email.\n *\n * Defaults to port `465` with `secure: true`. Hostinger supports `465` and\n * `587` only — not `2525`.\n *\n * @example\n * ```ts\n * import { createSMTPMailer } from \"sently/smtp\";\n * import { hostingerSmtpConfig } from \"sently/transports/hostinger\";\n *\n * const mailer = await createSMTPMailer(\n * hostingerSmtpConfig({\n * user: \"you@yourdomain.com\",\n * pass: process.env.HOSTINGER_SMTP_PASSWORD!,\n * // port: 587, // STARTTLS instead of SSL\n * }),\n * );\n * ```\n */\nexport function hostingerSmtpConfig(options: HostingerSmtpOptions): SMTPConfig {\n const port = options.port ?? HOSTINGER_SMTP_PORT_SSL;\n return {\n host: HOSTINGER_SMTP_HOST,\n port,\n secure: port === HOSTINGER_SMTP_PORT_SSL,\n auth: { user: options.user, pass: options.pass },\n ...(options.pool !== undefined ? { pool: options.pool } : {}),\n ...(options.maxConnections !== undefined ? { maxConnections: options.maxConnections } : {}),\n };\n}\n\n/** Error thrown when the Hostinger Mail API returns a non-success response. */\nexport class HostingerError extends SentlyError {\n /** Creates a Hostinger Mail 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: \"hostinger\",\n cause: apiError,\n });\n this.name = \"HostingerError\";\n }\n}\n\n/**\n * Hostinger Mail API transport.\n *\n * Sends through `POST /api/v1/mailboxes/{mailbox}/send`. There is no batch\n * endpoint — {@link Mailer.sendBulk} falls back to individual sends.\n *\n * For SMTP relay, use {@link hostingerSmtpConfig} with `createSMTPMailer`\n * instead of this class.\n */\nexport class HostingerTransport implements Transport {\n readonly provider = \"hostinger\";\n\n /** Hostinger Mail API token for Bearer authentication. */\n private readonly token: string;\n /** Resource ID of the managed mailbox to send from. */\n private readonly mailbox: string;\n /** Hostinger Mail API base URL. */\n private readonly baseUrl: string;\n\n /** Creates a Hostinger transport with the given API token and mailbox. */\n constructor(config: HostingerConfig) {\n this.token = config.token;\n this.mailbox = config.mailbox;\n this.baseUrl = config.baseUrl ?? HOSTINGER_API_BASE_URL;\n }\n\n /** List the mailboxes this token can manage — use it to discover your mailbox resource ID. */\n async listMailboxes(): Promise<HostingerMailbox[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/me`, {\n headers: { Authorization: `Bearer ${this.token}` },\n });\n\n const payload = (await response.json().catch(() => ({}))) as {\n data?: { mailboxes?: HostingerMailbox[] };\n } & HostingerErrorEnvelope;\n\n if (!response.ok) {\n throw new HostingerError(\n payload.error ?? `Hostinger API error (HTTP ${response.status})`,\n response.status,\n payload,\n );\n }\n\n return payload.data?.mailboxes ?? [];\n }\n\n /** Build the JSON body for a single Hostinger email. */\n private async buildEmailBody(\n options: MailOptions,\n extras?: HostingerSendExtras,\n ): Promise<Record<string, unknown>> {\n if (extras?.inReplyTo && extras.forwardOf) {\n throw new HostingerError(\"inReplyTo and forwardOf are mutually exclusive\", 422, {\n code: \"MUTUALLY_EXCLUSIVE\",\n params: { inReplyTo: [\"conflicts with forwardOf\"] },\n });\n }\n\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n return {\n to: extractEmails(options.to),\n ...(from?.name ? { displayName: from.name } : {}),\n ...(options.cc ? { cc: extractEmails(options.cc) } : {}),\n ...(options.bcc ? { bcc: extractEmails(options.bcc) } : {}),\n subject: options.subject,\n ...(options.text ? { text: options.text } : {}),\n ...(options.html ? { html: options.html } : {}),\n ...(attachments.length > 0\n ? {\n attachments: attachments.map((att) => ({\n filename: att.filename,\n content:\n att.content instanceof Uint8Array || typeof att.content === \"string\"\n ? encodeBase64(att.content).replace(/\\r\\n/g, \"\")\n : \"\",\n ...(att.contentType ? { contentType: att.contentType } : {}),\n ...(att.contentId ? { cid: att.contentId.replace(/^<|>$/g, \"\") } : {}),\n })),\n }\n : {}),\n ...(extras?.inReplyTo ? { inReplyTo: extras.inReplyTo } : {}),\n ...(extras?.forwardOf ? { forwardOf: extras.forwardOf } : {}),\n };\n }\n\n /** Map a 204 No Content success to a normalized SendResult. */\n private toSendResult(options: MailOptions): SendResult {\n const from = parseAddresses(options.from)[0];\n return {\n messageId: options.messageId ?? \"\",\n accepted: extractEmails(options.to),\n rejected: [],\n response: \"Message sent and saved to the Sent folder\",\n envelope: {\n from: from?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n },\n };\n }\n\n /** POST the send body and map the response. */\n private async postSend(options: MailOptions, extras?: HostingerSendExtras): Promise<SendResult> {\n const body = await this.buildEmailBody(options, extras);\n\n const response = await fetch(\n `${this.baseUrl}/api/v1/mailboxes/${encodeURIComponent(this.mailbox)}/send`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n },\n );\n\n if (response.ok) {\n return this.toSendResult(options);\n }\n\n const payload = (await response.json().catch(() => ({}))) as HostingerErrorEnvelope;\n throw new HostingerError(\n payload.error ?? `Hostinger API error (HTTP ${response.status})`,\n response.status,\n payload,\n );\n }\n\n /** Sends an email via the Hostinger Mail API. */\n async send(options: MailOptions): Promise<SendResult> {\n return this.postSend(options);\n }\n\n /**\n * Reply to a mailbox message.\n * Copies Message-Id / References into In-Reply-To / References and flags the\n * source `\\Answered`. Mutually exclusive with {@link sendForward}.\n */\n async sendReply(options: MailOptions, inReplyTo: HostingerMessageRef): Promise<SendResult> {\n return this.postSend(options, { inReplyTo });\n }\n\n /**\n * Forward a mailbox message.\n * Copies Message-Id / References into In-Reply-To / References and flags the\n * source `$forwarded`. Mutually exclusive with {@link sendReply}.\n */\n async sendForward(options: MailOptions, forwardOf: HostingerMessageRef): Promise<SendResult> {\n return this.postSend(options, { forwardOf });\n }\n\n /** Verifies the API token and that the configured mailbox is in its scope. */\n async verify(): Promise<VerifyResult> {\n try {\n const mailboxes = await this.listMailboxes();\n const configured = mailboxes.find((mailbox) => mailbox.resourceId === this.mailbox);\n\n if (!configured) {\n return {\n ok: false,\n provider: \"hostinger\",\n message: `Mailbox \"${this.mailbox}\" is not in this token's scope`,\n raw: mailboxes,\n };\n }\n\n return {\n ok: true,\n provider: \"hostinger\",\n message: `API token is valid — sending as ${configured.address}`,\n raw: mailboxes,\n };\n } catch (err) {\n if (err instanceof HostingerError) {\n return { ok: false, provider: \"hostinger\", message: err.message };\n }\n return {\n ok: false,\n provider: \"hostinger\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n"
5
+ "/**\n * @module\n * Hostinger email — Mail API transport (`api.mail.hostinger.com`) plus ready\n * SMTP config for `createSMTPMailer` (`smtp.hostinger.com`, ports 465 / 587).\n *\n * The Mail API sends from the managed mailbox itself, so `MailOptions.from`\n * only contributes the sender display name. Vendor extras (`listMailboxes`,\n * `sendReply`, `sendForward`) stay on this class — never on the channel sender.\n *\n * @example Mail API\n * ```ts\n * import { HostingerTransport } from \"sently/transports/hostinger\";\n * import { createMailer } from \"sently/mailer\";\n *\n * const hostinger = new HostingerTransport({\n * token: process.env.HOSTINGER_API_TOKEN!,\n * mailbox: process.env.HOSTINGER_MAILBOX_ID!, // e.g. \"AC1a2b3c4d5e6f7g\"\n * });\n * const mailer = await createMailer({ transport: hostinger });\n *\n * await mailer.send({\n * from: \"you@yourdomain.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * html: \"<p>Sent via Hostinger</p>\",\n * });\n * ```\n *\n * @example SMTP — same name, SMTP-shaped config (no `new`)\n * ```ts\n * import { createSMTPMailer } from \"sently/smtp\";\n * import { HostingerTransport } from \"sently/transports/hostinger\";\n *\n * const mailer = await createSMTPMailer(\n * HostingerTransport({\n * user: \"you@yourdomain.com\",\n * pass: process.env.HOSTINGER_SMTP_PASSWORD!,\n * }),\n * );\n * ```\n */\nimport { extractEmails, parseAddresses } from \"../core/address.js\";\nimport { encodeBase64 } from \"../core/base64.js\";\nimport { httpStatusToSentlyCode, SentlyError } from \"../core/errors.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPConfig,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/** Hostinger SMTP hostname. */\nexport const HOSTINGER_SMTP_HOST = \"smtp.hostinger.com\";\n\n/** SSL/TLS-on-connect submission port (default for {@link HostingerTransport} SMTP). */\nexport const HOSTINGER_SMTP_PORT_SSL = 465;\n\n/** STARTTLS submission port. */\nexport const HOSTINGER_SMTP_PORT_STARTTLS = 587;\n\n/** Default Hostinger Mail API base URL. */\nexport const HOSTINGER_API_BASE_URL = \"https://api.mail.hostinger.com\";\n\n/** Hostinger Mail API configuration. */\nexport interface HostingerConfig {\n /** API token from hPanel → Emails → Agentic Mail → API access (shown once at creation). */\n token: string;\n /** Resource ID of the managed mailbox to send from (e.g. `\"AC1a2b3c4d5e6f7g\"`). */\n mailbox: string;\n /** API base URL. Default: {@link HOSTINGER_API_BASE_URL}. */\n baseUrl?: string;\n}\n\n/**\n * Ready SMTP options for Hostinger Email.\n * Pass `HostingerTransport({ user, pass })` to `createSMTPMailer`.\n */\nexport interface HostingerSmtpOptions {\n /** Full mailbox address — this is the SMTP username. */\n user: string;\n /** Mailbox password from hPanel → Emails → Configuration settings. */\n pass: string;\n /**\n * Submission port.\n * - `465` — SSL/TLS on connect (default)\n * - `587` — STARTTLS\n */\n port?: 465 | 587;\n /** Enable the SMTP connection pool. Default: `false`. */\n pool?: boolean;\n /** Max simultaneous SMTP connections when `pool` is true. Default: `5`. */\n maxConnections?: number;\n}\n\n/** A mailbox the API token can manage, as returned by {@link HostingerTransport.listMailboxes}. */\nexport interface HostingerMailbox {\n /** Mailbox resource ID — pass it as {@link HostingerConfig.mailbox}. */\n resourceId: string;\n /** Email address of the mailbox. */\n address: string;\n}\n\n/**\n * Reference to a source message by IMAP UID within a folder.\n * Used by {@link HostingerTransport.sendReply} and {@link HostingerTransport.sendForward}.\n */\nexport interface HostingerMessageRef {\n /** Folder containing the source message (e.g. `\"INBOX\"`). */\n folder: string;\n /** IMAP UID of the source message. */\n uid: number;\n}\n\n/** Error envelope returned by the Hostinger Mail API on non-success responses. */\ninterface HostingerErrorEnvelope {\n error?: string;\n code?: string;\n params?: Record<string, unknown>;\n}\n\n/** Optional reply / forward threading fields for the Mail API send body. */\ninterface HostingerSendExtras {\n inReplyTo?: HostingerMessageRef;\n forwardOf?: HostingerMessageRef;\n}\n\n/**\n * Build a ready {@link SMTPConfig} for Hostinger Email.\n *\n * Defaults to port `465` with `secure: true`. Hostinger supports `465` and\n * `587` only — not `2525`. Prefer `HostingerTransport({ user, pass })`; this\n * alias stays for 1.x compatibility.\n */\nexport function hostingerSmtpConfig(options: HostingerSmtpOptions): SMTPConfig {\n const port = options.port ?? HOSTINGER_SMTP_PORT_SSL;\n return {\n host: HOSTINGER_SMTP_HOST,\n port,\n secure: port === HOSTINGER_SMTP_PORT_SSL,\n auth: { user: options.user, pass: options.pass },\n ...(options.pool !== undefined ? { pool: options.pool } : {}),\n ...(options.maxConnections !== undefined ? { maxConnections: options.maxConnections } : {}),\n };\n}\n\n/** Error thrown when the Hostinger Mail API returns a non-success response. */\nexport class HostingerError extends SentlyError {\n /** Creates a Hostinger Mail 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: \"hostinger\",\n cause: apiError,\n });\n this.name = \"HostingerError\";\n }\n}\n\n/**\n * Hostinger Mail API transport instance type (`new` / call with API config).\n * Constructed by the {@link HostingerTransport} overload — not exported directly.\n */\nclass HostingerTransportImpl implements Transport {\n readonly provider = \"hostinger\";\n\n /** Hostinger Mail API token for Bearer authentication. */\n private readonly token: string;\n /** Resource ID of the managed mailbox to send from. */\n private readonly mailbox: string;\n /** Hostinger Mail API base URL. */\n private readonly baseUrl: string;\n\n /** Creates a Hostinger transport with the given API token and mailbox. */\n constructor(config: HostingerConfig) {\n this.token = config.token;\n this.mailbox = config.mailbox;\n this.baseUrl = config.baseUrl ?? HOSTINGER_API_BASE_URL;\n }\n\n /** List the mailboxes this token can manage — use it to discover your mailbox resource ID. */\n async listMailboxes(): Promise<HostingerMailbox[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/me`, {\n headers: { Authorization: `Bearer ${this.token}` },\n });\n\n const payload = (await response.json().catch(() => ({}))) as {\n data?: { mailboxes?: HostingerMailbox[] };\n } & HostingerErrorEnvelope;\n\n if (!response.ok) {\n throw new HostingerError(\n payload.error ?? `Hostinger API error (HTTP ${response.status})`,\n response.status,\n payload,\n );\n }\n\n return payload.data?.mailboxes ?? [];\n }\n\n /** Build the JSON body for a single Hostinger email. */\n private async buildEmailBody(\n options: MailOptions,\n extras?: HostingerSendExtras,\n ): Promise<Record<string, unknown>> {\n if (extras?.inReplyTo && extras.forwardOf) {\n throw new HostingerError(\"inReplyTo and forwardOf are mutually exclusive\", 422, {\n code: \"MUTUALLY_EXCLUSIVE\",\n params: { inReplyTo: [\"conflicts with forwardOf\"] },\n });\n }\n\n const attachments = await resolveAttachments(options.attachments);\n const from = parseAddresses(options.from)[0];\n return {\n to: extractEmails(options.to),\n ...(from?.name ? { displayName: from.name } : {}),\n ...(options.cc ? { cc: extractEmails(options.cc) } : {}),\n ...(options.bcc ? { bcc: extractEmails(options.bcc) } : {}),\n subject: options.subject,\n ...(options.text ? { text: options.text } : {}),\n ...(options.html ? { html: options.html } : {}),\n ...(attachments.length > 0\n ? {\n attachments: attachments.map((att) => ({\n filename: att.filename,\n content:\n att.content instanceof Uint8Array || typeof att.content === \"string\"\n ? encodeBase64(att.content).replace(/\\r\\n/g, \"\")\n : \"\",\n ...(att.contentType ? { contentType: att.contentType } : {}),\n ...(att.contentId ? { cid: att.contentId.replace(/^<|>$/g, \"\") } : {}),\n })),\n }\n : {}),\n ...(extras?.inReplyTo ? { inReplyTo: extras.inReplyTo } : {}),\n ...(extras?.forwardOf ? { forwardOf: extras.forwardOf } : {}),\n };\n }\n\n /** Map a 204 No Content success to a normalized SendResult. */\n private toSendResult(options: MailOptions): SendResult {\n const from = parseAddresses(options.from)[0];\n return {\n messageId: options.messageId ?? \"\",\n accepted: extractEmails(options.to),\n rejected: [],\n response: \"Message sent and saved to the Sent folder\",\n envelope: {\n from: from?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n },\n };\n }\n\n /** POST the send body and map the response. */\n private async postSend(options: MailOptions, extras?: HostingerSendExtras): Promise<SendResult> {\n const body = await this.buildEmailBody(options, extras);\n\n const response = await fetch(\n `${this.baseUrl}/api/v1/mailboxes/${encodeURIComponent(this.mailbox)}/send`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n },\n );\n\n if (response.ok) {\n return this.toSendResult(options);\n }\n\n const payload = (await response.json().catch(() => ({}))) as HostingerErrorEnvelope;\n throw new HostingerError(\n payload.error ?? `Hostinger API error (HTTP ${response.status})`,\n response.status,\n payload,\n );\n }\n\n /** Sends an email via the Hostinger Mail API. */\n async send(options: MailOptions): Promise<SendResult> {\n return this.postSend(options);\n }\n\n /**\n * Reply to a mailbox message.\n * Copies Message-Id / References into In-Reply-To / References and flags the\n * source `\\Answered`. Mutually exclusive with {@link sendForward}.\n */\n async sendReply(options: MailOptions, inReplyTo: HostingerMessageRef): Promise<SendResult> {\n return this.postSend(options, { inReplyTo });\n }\n\n /**\n * Forward a mailbox message.\n * Copies Message-Id / References into In-Reply-To / References and flags the\n * source `$forwarded`. Mutually exclusive with {@link sendReply}.\n */\n async sendForward(options: MailOptions, forwardOf: HostingerMessageRef): Promise<SendResult> {\n return this.postSend(options, { forwardOf });\n }\n\n /** Verifies the API token and that the configured mailbox is in its scope. */\n async verify(): Promise<VerifyResult> {\n try {\n const mailboxes = await this.listMailboxes();\n const configured = mailboxes.find((mailbox) => mailbox.resourceId === this.mailbox);\n\n if (!configured) {\n return {\n ok: false,\n provider: \"hostinger\",\n message: `Mailbox \"${this.mailbox}\" is not in this token's scope`,\n raw: mailboxes,\n };\n }\n\n return {\n ok: true,\n provider: \"hostinger\",\n message: `API token is valid — sending as ${configured.address}`,\n raw: mailboxes,\n };\n } catch (err) {\n if (err instanceof HostingerError) {\n return { ok: false, provider: \"hostinger\", message: err.message };\n }\n return {\n ok: false,\n provider: \"hostinger\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n\n/**\n * Hostinger Mail API transport instance (returned by {@link HostingerTransport}\n * overloads for `{ token, mailbox }` config).\n */\nexport type HostingerMailTransport = HostingerTransportImpl;\n\n/**\n * Function-overloaded constructor for `HostingerTransport`:\n * - `new` / call with {@link HostingerConfig} → {@link HostingerMailTransport}\n * - call with {@link HostingerSmtpOptions} → {@link SMTPConfig}\n */\nexport interface HostingerTransportOverloads {\n new (config: HostingerConfig): HostingerMailTransport;\n (config: HostingerConfig): HostingerMailTransport;\n (config: HostingerSmtpOptions): SMTPConfig;\n}\n\n/**\n * Hostinger email — one name, two shapes (function overloading).\n *\n * - **Mail API** — `new HostingerTransport({ token, mailbox })` (or the same\n * call without `new`) → a `Transport` for `createMailer`.\n * - **SMTP** — `HostingerTransport({ user, pass })` → a ready `SMTPConfig`\n * for `createSMTPMailer`.\n *\n * IntelliSense narrows options and the return type by the config shape.\n */\nexport const HostingerTransport = function (\n this: HostingerTransportImpl | undefined,\n config: HostingerConfig | HostingerSmtpOptions,\n) {\n if (\"token\" in config) {\n return new HostingerTransportImpl(config);\n }\n return hostingerSmtpConfig(config);\n} as unknown as HostingerTransportOverloads;\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "5E9B71EE50CFF7E764756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "31B59878CFD615A464756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,3 +1,3 @@
1
- import{j as Q}from"../chunk-5es0v1j7.js";import{ja as X,ka as Y}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";class J extends X{statusCode;apiError;constructor(A,B,F){super(A,Y(B>=100&&B<600?B:400),{statusCode:B,provider:"taqnyat-sms",cause:F});this.statusCode=B;this.apiError=F;this.name="TaqnyatSmsError"}}function U(A){let B=A.code??A.statusCode??A.status;if(typeof B==="number")return B;if(typeof B==="string"&&/^-?\d+$/.test(B))return Number(B);return}function R(A){if(typeof A.message==="string")return A.message;if(typeof A.MessageEn==="string")return A.MessageEn;return}function W(A){if(Array.isArray(A)){let B=A[0];return B&&typeof B==="object"?B:{}}if(A&&typeof A==="object")return A;return{}}class _{provider="taqnyat-sms";bearerToken;sender;constructor(A){this.bearerToken=A.bearerToken,this.sender=A.sender}async send(A){let F={recipients:[Q(A.to)],body:A.body,sender:A.from??this.sender};if(A.messageId!==void 0)F.smsId=A.messageId;let G=await fetch("https://api.taqnyat.sa/v1/messages",{method:"POST",headers:{Authorization:`Bearer ${this.bearerToken}`,"Content-Type":"application/json"},body:JSON.stringify(F)}),D=await G.json();if(G.status!==201)throw new J(D.message??"Taqnyat SMS API error",D.statusCode??G.status,D);return{messageId:String(D.messageId??A.messageId??""),to:A.to,status:"accepted",response:`cost: ${D.cost??"?"} ${D.currency??"SAR"}`,provider:"taqnyat-sms"}}async sendOtp(A){let B=A.lang??"ar",F=Q(A.to),G=[{apiKey:this.bearerToken,numbers:[F],method:"sms",sender:A.from??this.sender,lang:B,requestId:A.requestId,returnJson:1,...A.note!==void 0?{note:A.note}:{}}],D=await fetch("https://api.taqnyat.sa/verify.php/",{method:"POST",headers:{Authorization:`Bearer ${this.bearerToken}`,"Content-Type":"application/json"},body:JSON.stringify(G)}),N=await D.text(),O;try{O=JSON.parse(N)}catch{throw new J("Taqnyat Verify returned non-JSON response",D.status,N)}let K=W(O),H=U(K);if(!D.ok||H!==5)throw new J(R(K)??`Taqnyat OTP send failed (code ${H??"unknown"})`,H??D.status,K);return{requestId:A.requestId,to:A.to,code:H,response:N,provider:"taqnyat-sms"}}async verifyOtp(A){let B=A.lang??"ar",F=Q(A.to),G=[{apiKey:this.bearerToken,numbers:[F],method:"sms",sender:A.from??this.sender,lang:B,requestId:A.requestId,activeKey:A.code,returnJson:1,...A.note!==void 0?{note:A.note}:{}}],D=await fetch("https://api.taqnyat.sa/verify.php/",{method:"POST",headers:{Authorization:`Bearer ${this.bearerToken}`,"Content-Type":"application/json"},body:JSON.stringify(G)}),N=await D.text(),O;try{O=JSON.parse(N)}catch{throw new J("Taqnyat Verify returned non-JSON response",D.status,N)}let K=W(O),H=U(K),Z=new Set([10,13,19]);if(!D.ok||H===void 0||!Z.has(H))throw new J(R(K)??`Taqnyat OTP verify failed (code ${H??"unknown"})`,H??D.status,K);return{ok:!0,code:H,message:R(K)??"Activation process completed successfully",response:N,provider:"taqnyat-sms"}}async getBalance(){let A=await fetch(`https://api.taqnyat.sa/account/balance?bearerTokens=${encodeURIComponent(this.bearerToken)}`,{headers:{Authorization:`Bearer ${this.bearerToken}`}}),B=await A.json();if(!A.ok||B.statusCode!==200)throw new J(B.message??"Taqnyat balance API error",B.statusCode??A.status,B);return{accountStatus:B.accountStatus??"",balance:B.balance??"0",currency:B.currency??"SAR",...B.accountExpiryDate!==void 0?{accountExpiryDate:B.accountExpiryDate}:{},provider:"taqnyat-sms"}}async listSenders(){let A=await fetch(`https://api.taqnyat.sa/v1/messages/senders?bearerTokens=${encodeURIComponent(this.bearerToken)}`,{headers:{Authorization:`Bearer ${this.bearerToken}`}}),B=await A.json();if(!A.ok||B.statusCode!==200)throw new J(B.message??"Taqnyat senders API error",B.statusCode??A.status,B);return(B.senders??[]).filter((F)=>typeof F.senderName==="string").map((F)=>({senderName:F.senderName,...F.status!==void 0?{status:F.status}:{}}))}async schedule(A){let F={recipients:[Q(A.to)],body:A.body,sender:A.from??this.sender,scheduledDatetime:A.scheduledDatetime};if(A.deleteId!==void 0)F.deleteId=A.deleteId;if(A.messageId!==void 0)F.smsId=A.messageId;let G=await fetch("https://api.taqnyat.sa/v1/messages",{method:"POST",headers:{Authorization:`Bearer ${this.bearerToken}`,"Content-Type":"application/json"},body:JSON.stringify(F)}),D=await G.json();if(G.status!==201)throw new J(D.message??"Taqnyat schedule SMS API error",D.statusCode??G.status,D);return{messageId:String(D.messageId??A.messageId??""),...A.deleteId!==void 0?{deleteId:String(A.deleteId)}:{},to:A.to,response:`cost: ${D.cost??"?"} ${D.currency??"SAR"}`,provider:"taqnyat-sms"}}async deleteScheduled(A){let B=new URL("https://api.taqnyat.sa/v1/messages/delete");B.searchParams.set("bearerTokens",this.bearerToken),B.searchParams.set("deleteId",String(A));let F=await fetch(B,{method:"DELETE",headers:{Authorization:`Bearer ${this.bearerToken}`}}),G=await F.json();if(F.status!==201&&G.statusCode!==201)throw new J(G.message??"Taqnyat delete scheduled SMS API error",G.statusCode??F.status,G);return{ok:!0,message:G.message??"Deleted successfully",provider:"taqnyat-sms"}}async verify(){return{ok:Boolean(this.bearerToken&&this.sender),provider:"taqnyat-sms",message:this.bearerToken&&this.sender?"Credentials present":"Missing credentials"}}}export{_ as TaqnyatSmsTransport,J as TaqnyatSmsError};
1
+ import{j as Q}from"../chunk-5es0v1j7.js";import{ja as Y,ka as Z}from"../chunk-ttbwyxmh.js";import"../chunk-th4cwrpb.js";class J extends Y{statusCode;apiError;constructor(A,B,D){super(A,Z(B>=100&&B<600?B:400),{statusCode:B,provider:"taqnyat-sms",cause:D});this.statusCode=B;this.apiError=D;this.name="TaqnyatSmsError"}}function R(A){if(typeof A==="number"&&Number.isFinite(A))return A;if(typeof A==="string"&&/^-?\d+$/.test(A))return Number(A);return}function W(A){let B=A.Data;if(B&&typeof B==="object"&&!Array.isArray(B)){let G=R(B.result);if(G!==void 0)return G}let D=R(A.code??A.statusCode??A.result);if(D!==void 0)return D;if(A.ResponseStatus===void 0)return R(A.status);return}function U(A){let B=A.Data;if(B&&typeof B==="object"&&!Array.isArray(B)){let D=B;if(typeof D.MessageEn==="string")return D.MessageEn;if(typeof D.MessageAr==="string")return D.MessageAr;if(typeof D.message==="string")return D.message}if(typeof A.message==="string")return A.message;if(typeof A.MessageEn==="string")return A.MessageEn;if(typeof A.Error==="string")return A.Error;return}function X(A){if(Array.isArray(A)){let B=A[0];return B&&typeof B==="object"?B:{}}if(A&&typeof A==="object")return A;return{}}class ${provider="taqnyat-sms";bearerToken;sender;constructor(A){this.bearerToken=A.bearerToken,this.sender=A.sender}async send(A){let D={recipients:[Q(A.to)],body:A.body,sender:A.from??this.sender};if(A.messageId!==void 0)D.smsId=A.messageId;let G=await fetch("https://api.taqnyat.sa/v1/messages",{method:"POST",headers:{Authorization:`Bearer ${this.bearerToken}`,"Content-Type":"application/json"},body:JSON.stringify(D)}),F=await G.json();if(G.status!==201)throw new J(F.message??"Taqnyat SMS API error",F.statusCode??G.status,F);return{messageId:String(F.messageId??A.messageId??""),to:A.to,status:"accepted",response:`cost: ${F.cost??"?"} ${F.currency??"SAR"}`,provider:"taqnyat-sms"}}async sendOtp(A){let B=A.lang??"ar",D=Q(A.to),G=[{apiKey:this.bearerToken,numbers:[D],method:"sms",sender:A.from??this.sender,lang:B,requestId:A.requestId,returnJson:1,...A.note!==void 0?{note:A.note}:{}}],F=await fetch("https://api.taqnyat.sa/verify.php/",{method:"POST",headers:{Authorization:`Bearer ${this.bearerToken}`,"Content-Type":"application/json"},body:JSON.stringify(G)}),N=await F.text(),O;try{O=JSON.parse(N)}catch{throw new J("Taqnyat Verify returned non-JSON response",F.status,N)}let K=X(O),H=W(K);if(!F.ok||H!==5)throw new J(U(K)??`Taqnyat OTP send failed (code ${H??"unknown"})`,H??F.status,K);return{requestId:A.requestId,to:A.to,code:H,response:N,provider:"taqnyat-sms"}}async verifyOtp(A){let B=A.lang??"ar",D=Q(A.to),G=[{apiKey:this.bearerToken,numbers:[D],method:"sms",sender:A.from??this.sender,lang:B,requestId:A.requestId,activeKey:A.code,returnJson:1,...A.note!==void 0?{note:A.note}:{}}],F=await fetch("https://api.taqnyat.sa/verify.php/",{method:"POST",headers:{Authorization:`Bearer ${this.bearerToken}`,"Content-Type":"application/json"},body:JSON.stringify(G)}),N=await F.text(),O;try{O=JSON.parse(N)}catch{throw new J("Taqnyat Verify returned non-JSON response",F.status,N)}let K=X(O),H=W(K),_=new Set([10,13,19]);if(!F.ok||H===void 0||!_.has(H))throw new J(U(K)??`Taqnyat OTP verify failed (code ${H??"unknown"})`,H??F.status,K);return{ok:!0,code:H,message:U(K)??"Activation process completed successfully",response:N,provider:"taqnyat-sms"}}async getBalance(){let A=await fetch(`https://api.taqnyat.sa/account/balance?bearerTokens=${encodeURIComponent(this.bearerToken)}`,{headers:{Authorization:`Bearer ${this.bearerToken}`}}),B=await A.json();if(!A.ok||B.statusCode!==200)throw new J(B.message??"Taqnyat balance API error",B.statusCode??A.status,B);return{accountStatus:B.accountStatus??"",balance:B.balance??"0",currency:B.currency??"SAR",...B.accountExpiryDate!==void 0?{accountExpiryDate:B.accountExpiryDate}:{},provider:"taqnyat-sms"}}async listSenders(){let A=await fetch(`https://api.taqnyat.sa/v1/messages/senders?bearerTokens=${encodeURIComponent(this.bearerToken)}`,{headers:{Authorization:`Bearer ${this.bearerToken}`}}),B=await A.json();if(!A.ok||B.statusCode!==200)throw new J(B.message??"Taqnyat senders API error",B.statusCode??A.status,B);return(B.senders??[]).filter((D)=>typeof D.senderName==="string").map((D)=>({senderName:D.senderName,...D.status!==void 0?{status:D.status}:{}}))}async schedule(A){let D={recipients:[Q(A.to)],body:A.body,sender:A.from??this.sender,scheduledDatetime:A.scheduledDatetime};if(A.deleteId!==void 0)D.deleteId=A.deleteId;if(A.messageId!==void 0)D.smsId=A.messageId;let G=await fetch("https://api.taqnyat.sa/v1/messages",{method:"POST",headers:{Authorization:`Bearer ${this.bearerToken}`,"Content-Type":"application/json"},body:JSON.stringify(D)}),F=await G.json();if(G.status!==201)throw new J(F.message??"Taqnyat schedule SMS API error",F.statusCode??G.status,F);return{messageId:String(F.messageId??A.messageId??""),...A.deleteId!==void 0?{deleteId:String(A.deleteId)}:{},to:A.to,response:`cost: ${F.cost??"?"} ${F.currency??"SAR"}`,provider:"taqnyat-sms"}}async deleteScheduled(A){let B=new URL("https://api.taqnyat.sa/v1/messages/delete");B.searchParams.set("bearerTokens",this.bearerToken),B.searchParams.set("deleteId",String(A));let D=await fetch(B,{method:"DELETE",headers:{Authorization:`Bearer ${this.bearerToken}`}}),G=await D.json();if(D.status!==201&&G.statusCode!==201)throw new J(G.message??"Taqnyat delete scheduled SMS API error",G.statusCode??D.status,G);return{ok:!0,message:G.message??"Deleted successfully",provider:"taqnyat-sms"}}async verify(){return{ok:Boolean(this.bearerToken&&this.sender),provider:"taqnyat-sms",message:this.bearerToken&&this.sender?"Credentials present":"Missing credentials"}}}export{$ as TaqnyatSmsTransport,J as TaqnyatSmsError};
2
2
 
3
- //# debugId=5FB80A8A78A3002064756E2164756E21
3
+ //# debugId=4CF386F61DA773C164756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/transports/taqnyat-sms.ts"],
4
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 * const balance = await taqnyat.getBalance();\n * await taqnyat.schedule({\n * to: \"+9665xxxxxxxx\",\n * body: \"Later\",\n * scheduledDatetime: \"2030-01-01T10:00\",\n * deleteId: \"demo-1\",\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/** Account balance from {@link TaqnyatSmsTransport.getBalance}. */\nexport interface TaqnyatBalance {\n /** Account status string from Taqnyat (e.g. `active`). */\n accountStatus: string;\n /** Balance amount as returned by the API (string). */\n balance: string;\n /** Currency code (usually `SAR`). */\n currency: string;\n /** Account expiry date when present. */\n accountExpiryDate?: string;\n /** Provider identifier. */\n provider: \"taqnyat-sms\";\n}\n\n/** One sender row from {@link TaqnyatSmsTransport.listSenders}. */\nexport interface TaqnyatSender {\n /** Sender name exactly as registered. */\n senderName: string;\n /** Status string when present (e.g. `active`). */\n status?: string;\n}\n\n/** Options for {@link TaqnyatSmsTransport.schedule}. */\nexport interface TaqnyatScheduleOptions {\n /** Recipient phone number (E.164 or international digits). */\n to: string;\n /** Message body. */\n body: string;\n /**\n * Local schedule time in Taqnyat format, e.g. `2020-09-30T14:26`.\n * Required by `POST /v1/messages` schedule docs.\n */\n scheduledDatetime: string;\n /** Sender ID override; defaults to transport `sender`. */\n from?: string;\n /**\n * Client delete key used later with {@link deleteScheduled}.\n * Optional per Taqnyat schedule docs.\n */\n deleteId?: string | number;\n /** Optional client id mapped to `smsId`. */\n messageId?: string;\n}\n\n/** Result of a successful {@link TaqnyatSmsTransport.schedule} call. */\nexport interface TaqnyatScheduleResult {\n /** Provider message id when present. */\n messageId: string;\n /** Echo of {@link TaqnyatScheduleOptions.deleteId} when set. */\n deleteId?: string;\n /** Recipient as passed in. */\n to: string;\n /** Cost summary when present. */\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 /**\n * Vendor extra: account balance via `GET /account/balance`.\n * Not part of {@link SmsTransport}.\n */\n async getBalance(): Promise<TaqnyatBalance> {\n const response = await fetch(\n `https://api.taqnyat.sa/account/balance?bearerTokens=${encodeURIComponent(this.bearerToken)}`,\n {\n headers: { Authorization: `Bearer ${this.bearerToken}` },\n },\n );\n const payload = (await response.json()) as {\n statusCode?: number;\n accountStatus?: string;\n balance?: string;\n currency?: string;\n accountExpiryDate?: string;\n message?: string;\n };\n\n if (!response.ok || payload.statusCode !== 200) {\n throw new TaqnyatSmsError(\n payload.message ?? \"Taqnyat balance API error\",\n payload.statusCode ?? response.status,\n payload,\n );\n }\n\n return {\n accountStatus: payload.accountStatus ?? \"\",\n balance: payload.balance ?? \"0\",\n currency: payload.currency ?? \"SAR\",\n ...(payload.accountExpiryDate !== undefined\n ? { accountExpiryDate: payload.accountExpiryDate }\n : {}),\n provider: \"taqnyat-sms\",\n };\n }\n\n /**\n * Vendor extra: list registered sender names via `GET /v1/messages/senders`.\n * Not part of {@link SmsTransport}.\n */\n async listSenders(): Promise<TaqnyatSender[]> {\n const response = await fetch(\n `https://api.taqnyat.sa/v1/messages/senders?bearerTokens=${encodeURIComponent(this.bearerToken)}`,\n {\n headers: { Authorization: `Bearer ${this.bearerToken}` },\n },\n );\n const payload = (await response.json()) as {\n statusCode?: number;\n senders?: Array<{ senderName?: string; status?: string }>;\n message?: string;\n };\n\n if (!response.ok || payload.statusCode !== 200) {\n throw new TaqnyatSmsError(\n payload.message ?? \"Taqnyat senders API error\",\n payload.statusCode ?? response.status,\n payload,\n );\n }\n\n return (payload.senders ?? [])\n .filter(\n (row): row is { senderName: string; status?: string } => typeof row.senderName === \"string\",\n )\n .map((row) => ({\n senderName: row.senderName,\n ...(row.status !== undefined ? { status: row.status } : {}),\n }));\n }\n\n /**\n * Vendor extra: schedule an SMS (`POST /v1/messages` + `scheduledDatetime`).\n * Not part of {@link SmsTransport}.\n */\n async schedule(options: TaqnyatScheduleOptions): Promise<TaqnyatScheduleResult> {\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 scheduledDatetime: options.scheduledDatetime,\n };\n if (options.deleteId !== undefined) {\n body.deleteId = options.deleteId;\n }\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 };\n\n if (response.status !== 201) {\n throw new TaqnyatSmsError(\n payload.message ?? \"Taqnyat schedule SMS API error\",\n payload.statusCode ?? response.status,\n payload,\n );\n }\n\n return {\n messageId: String(payload.messageId ?? options.messageId ?? \"\"),\n ...(options.deleteId !== undefined ? { deleteId: String(options.deleteId) } : {}),\n to: options.to,\n response: `cost: ${payload.cost ?? \"?\"} ${payload.currency ?? \"SAR\"}`,\n provider: \"taqnyat-sms\",\n };\n }\n\n /**\n * Vendor extra: delete a scheduled SMS via `DELETE /v1/messages/delete`.\n * Pass the same `deleteId` used in {@link schedule}.\n */\n async deleteScheduled(\n deleteId: string | number,\n ): Promise<{ ok: true; message: string; provider: \"taqnyat-sms\" }> {\n const url = new URL(\"https://api.taqnyat.sa/v1/messages/delete\");\n url.searchParams.set(\"bearerTokens\", this.bearerToken);\n url.searchParams.set(\"deleteId\", String(deleteId));\n\n const response = await fetch(url, {\n method: \"DELETE\",\n headers: { Authorization: `Bearer ${this.bearerToken}` },\n });\n\n const payload = (await response.json()) as {\n statusCode?: number;\n message?: string;\n };\n\n if (response.status !== 201 && payload.statusCode !== 201) {\n throw new TaqnyatSmsError(\n payload.message ?? \"Taqnyat delete scheduled SMS API error\",\n payload.statusCode ?? response.status,\n payload,\n );\n }\n\n return {\n ok: true,\n message: payload.message ?? \"Deleted successfully\",\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"
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 * const balance = await taqnyat.getBalance();\n * await taqnyat.schedule({\n * to: \"+9665xxxxxxxx\",\n * body: \"Later\",\n * scheduledDatetime: \"2030-01-01T10:00\",\n * deleteId: \"demo-1\",\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/** Account balance from {@link TaqnyatSmsTransport.getBalance}. */\nexport interface TaqnyatBalance {\n /** Account status string from Taqnyat (e.g. `active`). */\n accountStatus: string;\n /** Balance amount as returned by the API (string). */\n balance: string;\n /** Currency code (usually `SAR`). */\n currency: string;\n /** Account expiry date when present. */\n accountExpiryDate?: string;\n /** Provider identifier. */\n provider: \"taqnyat-sms\";\n}\n\n/** One sender row from {@link TaqnyatSmsTransport.listSenders}. */\nexport interface TaqnyatSender {\n /** Sender name exactly as registered. */\n senderName: string;\n /** Status string when present (e.g. `active`). */\n status?: string;\n}\n\n/** Options for {@link TaqnyatSmsTransport.schedule}. */\nexport interface TaqnyatScheduleOptions {\n /** Recipient phone number (E.164 or international digits). */\n to: string;\n /** Message body. */\n body: string;\n /**\n * Local schedule time in Taqnyat format, e.g. `2020-09-30T14:26`.\n * Required by `POST /v1/messages` schedule docs.\n */\n scheduledDatetime: string;\n /** Sender ID override; defaults to transport `sender`. */\n from?: string;\n /**\n * Client delete key used later with {@link deleteScheduled}.\n * Optional per Taqnyat schedule docs.\n */\n deleteId?: string | number;\n /** Optional client id mapped to `smsId`. */\n messageId?: string;\n}\n\n/** Result of a successful {@link TaqnyatSmsTransport.schedule} call. */\nexport interface TaqnyatScheduleResult {\n /** Provider message id when present. */\n messageId: string;\n /** Echo of {@link TaqnyatScheduleOptions.deleteId} when set. */\n deleteId?: string;\n /** Recipient as passed in. */\n to: string;\n /** Cost summary when present. */\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 asVerifyCode(raw: unknown): number | undefined {\n if (typeof raw === \"number\" && Number.isFinite(raw)) return raw;\n if (typeof raw === \"string\" && /^-?\\d+$/.test(raw)) return Number(raw);\n return undefined;\n}\n\n/**\n * Resolve the Verify result code.\n *\n * Live `returnJson: 1` responses wrap the docs table code in `Data.result`:\n * `{ status: 1, ResponseStatus: \"success\", Data: { result: 5, MessageEn: \"…\" } }`.\n * Top-level `status` here is a transport envelope (not the docs table) — reading\n * it as the OTP code falsely maps success to \"invalid apiKey\" (code 1).\n * Flat shapes (`{ code: 5 }`) from docs samples / older responses still work.\n */\nfunction verifyPayloadCode(payload: Record<string, unknown>): number | undefined {\n const data = payload.Data;\n if (data && typeof data === \"object\" && !Array.isArray(data)) {\n const nested = asVerifyCode((data as Record<string, unknown>).result);\n if (nested !== undefined) return nested;\n }\n const flat = asVerifyCode(payload.code ?? payload.statusCode ?? payload.result);\n if (flat !== undefined) return flat;\n // Only treat top-level `status` as the result code when this is not the\n // envelope form (which always carries `ResponseStatus`).\n if (payload.ResponseStatus === undefined) {\n return asVerifyCode(payload.status);\n }\n return undefined;\n}\n\nfunction verifyPayloadMessage(payload: Record<string, unknown>): string | undefined {\n const data = payload.Data;\n if (data && typeof data === \"object\" && !Array.isArray(data)) {\n const nested = data as Record<string, unknown>;\n if (typeof nested.MessageEn === \"string\") return nested.MessageEn;\n if (typeof nested.MessageAr === \"string\") return nested.MessageAr;\n if (typeof nested.message === \"string\") return nested.message;\n }\n if (typeof payload.message === \"string\") return payload.message;\n if (typeof payload.MessageEn === \"string\") return payload.MessageEn;\n if (typeof payload.Error === \"string\") return payload.Error;\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 /**\n * Vendor extra: account balance via `GET /account/balance`.\n * Not part of {@link SmsTransport}.\n */\n async getBalance(): Promise<TaqnyatBalance> {\n const response = await fetch(\n `https://api.taqnyat.sa/account/balance?bearerTokens=${encodeURIComponent(this.bearerToken)}`,\n {\n headers: { Authorization: `Bearer ${this.bearerToken}` },\n },\n );\n const payload = (await response.json()) as {\n statusCode?: number;\n accountStatus?: string;\n balance?: string;\n currency?: string;\n accountExpiryDate?: string;\n message?: string;\n };\n\n if (!response.ok || payload.statusCode !== 200) {\n throw new TaqnyatSmsError(\n payload.message ?? \"Taqnyat balance API error\",\n payload.statusCode ?? response.status,\n payload,\n );\n }\n\n return {\n accountStatus: payload.accountStatus ?? \"\",\n balance: payload.balance ?? \"0\",\n currency: payload.currency ?? \"SAR\",\n ...(payload.accountExpiryDate !== undefined\n ? { accountExpiryDate: payload.accountExpiryDate }\n : {}),\n provider: \"taqnyat-sms\",\n };\n }\n\n /**\n * Vendor extra: list registered sender names via `GET /v1/messages/senders`.\n * Not part of {@link SmsTransport}.\n */\n async listSenders(): Promise<TaqnyatSender[]> {\n const response = await fetch(\n `https://api.taqnyat.sa/v1/messages/senders?bearerTokens=${encodeURIComponent(this.bearerToken)}`,\n {\n headers: { Authorization: `Bearer ${this.bearerToken}` },\n },\n );\n const payload = (await response.json()) as {\n statusCode?: number;\n senders?: Array<{ senderName?: string; status?: string }>;\n message?: string;\n };\n\n if (!response.ok || payload.statusCode !== 200) {\n throw new TaqnyatSmsError(\n payload.message ?? \"Taqnyat senders API error\",\n payload.statusCode ?? response.status,\n payload,\n );\n }\n\n return (payload.senders ?? [])\n .filter(\n (row): row is { senderName: string; status?: string } => typeof row.senderName === \"string\",\n )\n .map((row) => ({\n senderName: row.senderName,\n ...(row.status !== undefined ? { status: row.status } : {}),\n }));\n }\n\n /**\n * Vendor extra: schedule an SMS (`POST /v1/messages` + `scheduledDatetime`).\n * Not part of {@link SmsTransport}.\n */\n async schedule(options: TaqnyatScheduleOptions): Promise<TaqnyatScheduleResult> {\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 scheduledDatetime: options.scheduledDatetime,\n };\n if (options.deleteId !== undefined) {\n body.deleteId = options.deleteId;\n }\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 };\n\n if (response.status !== 201) {\n throw new TaqnyatSmsError(\n payload.message ?? \"Taqnyat schedule SMS API error\",\n payload.statusCode ?? response.status,\n payload,\n );\n }\n\n return {\n messageId: String(payload.messageId ?? options.messageId ?? \"\"),\n ...(options.deleteId !== undefined ? { deleteId: String(options.deleteId) } : {}),\n to: options.to,\n response: `cost: ${payload.cost ?? \"?\"} ${payload.currency ?? \"SAR\"}`,\n provider: \"taqnyat-sms\",\n };\n }\n\n /**\n * Vendor extra: delete a scheduled SMS via `DELETE /v1/messages/delete`.\n * Pass the same `deleteId` used in {@link schedule}.\n */\n async deleteScheduled(\n deleteId: string | number,\n ): Promise<{ ok: true; message: string; provider: \"taqnyat-sms\" }> {\n const url = new URL(\"https://api.taqnyat.sa/v1/messages/delete\");\n url.searchParams.set(\"bearerTokens\", this.bearerToken);\n url.searchParams.set(\"deleteId\", String(deleteId));\n\n const response = await fetch(url, {\n method: \"DELETE\",\n headers: { Authorization: `Bearer ${this.bearerToken}` },\n });\n\n const payload = (await response.json()) as {\n statusCode?: number;\n message?: string;\n };\n\n if (response.status !== 201 && payload.statusCode !== 201) {\n throw new TaqnyatSmsError(\n payload.message ?? \"Taqnyat delete scheduled SMS API error\",\n payload.statusCode ?? response.status,\n payload,\n );\n }\n\n return {\n ok: true,\n message: payload.message ?? \"Deleted successfully\",\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
6
  ],
7
- "mappings": "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",
8
- "debugId": "5FB80A8A78A3002064756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "4CF386F61DA773C164756E2164756E21",
9
9
  "names": []
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sently",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Runtime-agnostic channel-delivery library for Node.js, Bun, Deno, and Cloudflare Workers. One sender shape, one error model, and one retry path across email, SMS, WhatsApp, and push — with pluggable provider transports.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -13,7 +13,8 @@ const otp = await msegat.sendOtp({ to: "+9665...", lang: "En" });
13
13
  await msegat.verifyOtp({ id: otp.id, code: "1234", lang: "En" });
14
14
  ```
15
15
 
16
- Taqnyat SMS also exposes `sendOtp` and `verifyOtp` on its transport.
16
+ Taqnyat SMS also exposes `sendOtp` and `verifyOtp` on its transport — live verified
17
+ end-to-end against production (`sendOtp` → handset → `verifyOtp`).
17
18
  See [Taqnyat](/docs/transports/taqnyat#sms-vendor-extras) for balance, senders, schedule, WhatsApp templates, opt-in, and failover extras.
18
19
 
19
20
  <Cards><Card title="Msegat" href="/docs/transports/msegat" /><Card title="Taqnyat" href="/docs/transports/taqnyat#sms" /></Cards>
@@ -8,14 +8,14 @@ source: "src/transports/hostinger.ts"
8
8
  Hostinger Email gives you a branded mailbox. Wire it into sently two ways — the Mail API over HTTPS, or the SMTP relay with Hostinger defaults already filled in.
9
9
 
10
10
  <Callout title="The one rule">
11
- Use `createMailer` with `HostingerTransport` for the Mail API, or `createSMTPMailer` with `hostingerSmtpConfig` for SMTP.
12
- Vendor extras (`listMailboxes`, `sendReply`, `sendForward`) stay on the transport instance — never on the channel sender.
11
+ One name, two shapes: `new HostingerTransport({ token, mailbox })` `createMailer` (Mail API); `HostingerTransport({ user, pass })` → `createSMTPMailer` (SMTP, no `new`).
12
+ IntelliSense narrows options and the return type by config shape. Vendor extras (`listMailboxes`, `sendReply`, `sendForward`) stay on the Mail API instance — never on the channel sender.
13
13
  </Callout>
14
14
 
15
- | Path | Import | When to use |
16
- | --- | --- | --- |
17
- | Mail API | `sently/transports/hostinger` → `HostingerTransport` | Tokens, Agentic Mail, reply/forward threading |
18
- | SMTP | `hostingerSmtpConfig` + `sently/smtp` | Classic relay, clients, apps that already speak SMTP |
15
+ | Path | Call | Returns | Wire into |
16
+ | --- | --- | --- | --- |
17
+ | Mail API | `new HostingerTransport({ token, mailbox })` | Transport | `createMailer` |
18
+ | SMTP | `HostingerTransport({ user, pass })` | `SMTPConfig` | `createSMTPMailer` |
19
19
 
20
20
  Official references: [Hostinger API](https://developers.hostinger.com/), [Mail API](https://api.mail.hostinger.com/), [SMTP ports](https://www.hostinger.com/tutorials/smtp-port/).
21
21
 
@@ -243,7 +243,7 @@ const viaMailer = await mailer.verify(); // same check through the channel sende
243
243
  SMTP send against Hostinger’s production relay (`smtp.hostinger.com`) succeeded previously with a real mailbox — SSL port 465 and STARTTLS port 587.
244
244
  </LiveVerified>
245
245
 
246
- Ready Hostinger relay settings — no host/port guesswork. Pass `hostingerSmtpConfig(...)` straight into `createSMTPMailer`.
246
+ Ready Hostinger relay settings — no host/port guesswork. Pass `HostingerTransport({ user, pass })` straight into `createSMTPMailer`.
247
247
 
248
248
  | Setting | Value |
249
249
  | --- | --- |
@@ -267,10 +267,10 @@ Hostinger supports ports **465** and **587** only — not `2525` ([SMTP ports gu
267
267
 
268
268
  ```ts
269
269
  import { createSMTPMailer } from "sently/smtp";
270
- import { hostingerSmtpConfig } from "sently/transports/hostinger";
270
+ import { HostingerTransport } from "sently/transports/hostinger";
271
271
 
272
272
  const mailer = await createSMTPMailer(
273
- hostingerSmtpConfig({
273
+ HostingerTransport({
274
274
  user: "you@yourdomain.com",
275
275
  pass: process.env.HOSTINGER_SMTP_PASSWORD!,
276
276
  }),
@@ -301,7 +301,7 @@ Default — implicit TLS on connect.
301
301
 
302
302
  ```ts
303
303
  const mailer = await createSMTPMailer(
304
- hostingerSmtpConfig({
304
+ HostingerTransport({
305
305
  user: "you@yourdomain.com",
306
306
  pass: process.env.HOSTINGER_SMTP_PASSWORD!,
307
307
  // port: 465, // default
@@ -318,7 +318,7 @@ Plain connect, then upgrade with STARTTLS.
318
318
 
319
319
  ```ts
320
320
  const mailer = await createSMTPMailer(
321
- hostingerSmtpConfig({
321
+ HostingerTransport({
322
322
  user: "you@yourdomain.com",
323
323
  pass: process.env.HOSTINGER_SMTP_PASSWORD!,
324
324
  port: 587,
@@ -335,7 +335,7 @@ Reuse SMTP connections under load.
335
335
 
336
336
  ```ts
337
337
  const mailer = await createSMTPMailer(
338
- hostingerSmtpConfig({
338
+ HostingerTransport({
339
339
  user: "you@yourdomain.com",
340
340
  pass: process.env.HOSTINGER_SMTP_PASSWORD!,
341
341
  pool: true,
@@ -366,7 +366,9 @@ See [SMTP](./smtp) for pooling, DKIM, and adapter details.
366
366
  </Tab>
367
367
  </Tabs>
368
368
 
369
- ### Config helper
369
+ ### SMTP options
370
+
371
+ Options for `HostingerTransport({ user, pass })` (the SMTP overload):
370
372
 
371
373
  | Option | Type | Default |
372
374
  | --- | --- | --- |
@@ -376,6 +378,8 @@ See [SMTP](./smtp) for pooling, DKIM, and adapter details.
376
378
  | `pool` | `boolean` | unset |
377
379
  | `maxConnections` | `number` | unset (SMTP default `5` when pooled) |
378
380
 
381
+ `hostingerSmtpConfig(...)` still works as a 1.x compatibility alias for the same options.
382
+
379
383
  ## Mail API vs SMTP
380
384
 
381
385
  | Need | Prefer |
@@ -68,6 +68,10 @@ Optional `from` overrides the transport `sender` for that send.
68
68
 
69
69
  Taqnyat Verify API on the transport (`sendOtp` → user enters code → `verifyOtp`).
70
70
 
71
+ <LiveVerified>
72
+ Verify OTP end-to-end against Taqnyat’s production API: `sendOtp` returned `Data.result: 5`, the handset received the code, and `verifyOtp` completed successfully (live suite + manual confirm).
73
+ </LiveVerified>
74
+
71
75
  ```ts
72
76
  await taqnyat.sendOtp({
73
77
  to: "+9665xxxxxxxx",
@@ -84,7 +88,7 @@ await taqnyat.verifyOtp({
84
88
  });
85
89
  ```
86
90
 
87
- Success send code is `5`. Verify accepts `10`, `13`, or `19`.
91
+ Success send code is `5` (`Data.result` on live `returnJson` responses). Verify accepts `10`, `13`, or `19`.
88
92
 
89
93
  </Tab>
90
94
  <Tab value="Balance">
@@ -313,11 +317,36 @@ Body is `html` when set, otherwise `text` (`msg` on Taqnyat’s API).
313
317
  </Tab>
314
318
  </Tabs>
315
319
 
320
+ ## Account verification & sender names
321
+
322
+ Portal onboarding from Taqnyat — required before a custom SMS `sender` is active. Not part of the sently API; complete these in the Taqnyat portal.
323
+
324
+ ### Account verification
325
+
326
+ 1. Fill out the contract, sign it, and stamp it.
327
+ 2. If the organization is subject to tax, attach the tax certificate.
328
+
329
+ ### New sender name
330
+
331
+ | Rule | Detail |
332
+ | --- | --- |
333
+ | Authorization | Fill out the sender-name authorization form, sign it, and stamp it |
334
+ | Name relation | Sender name must relate to the organization; otherwise attach a supporting document that explains the link |
335
+ | Fee | SAR 230, renewed annually |
336
+ | Service name | Max **11** characters |
337
+ | Promotional name | Max **8** characters, with `-AD` appended at the end |
338
+ | Dual registration | Names ≤ 8 characters can be registered as both service and promotional for one SAR 230 fee (counts as one name) |
339
+
340
+ <Callout title="Tip">
341
+ Use the **Senders** tab above (`listSenders`) after registration to confirm the name is active before hard-coding `sender`.
342
+ </Callout>
343
+
316
344
  ## Troubleshooting
317
345
 
318
346
  <Accordions>
319
347
  <Accordion title="SMS — Sender Name not active / not accepted">
320
348
  Open the **Senders** branch and call `listSenders()`, or use an active portal sender exactly as shown (trial accounts often use `Taqnyat.sa`).
349
+ Custom names need portal registration first — see [Account verification & sender names](#account-verification--sender-names).
321
350
  </Accordion>
322
351
  <Accordion title="WhatsApp — sandbox rejects the number">
323
352
  Add the destination under Manage WhatsApp → Sandbox, then use the **Opt-in** branch before business-initiated templates.