@zeroxsolutions/sms 0.1.1 → 0.1.2

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,3 +1,18 @@
1
+ ## 0.1.2 (2026-10-08)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **sms:** send through twilio for numbers outside vietnam ([1d94662](https://github.com/zeroxsolutions/zeroxsolutions/commit/1d94662))
6
+
7
+ ### 💅 Refactors
8
+
9
+ - **sms:** name the twilio option for the origin it holds ([8c69c31](https://github.com/zeroxsolutions/zeroxsolutions/commit/8c69c31))
10
+
11
+ ### ❤️ Thank You
12
+
13
+ - Claude Opus 5.5
14
+ - Lương Văn Tú
15
+
1
16
  ## 0.1.1 (2026-09-25)
2
17
 
3
18
  ### 🩹 Fixes
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @zeroxsolutions/sms
2
2
 
3
- The SMS seam: a composed message, the port that sends it, and its typed refusals.
3
+ The SMS seam: a composed message, the port that sends it, its typed refusals, and the eSMS and
4
+ Twilio adapters behind it.
4
5
 
5
6
  What it does not own is the reason an SMS exists at all - templates, message keys, locales, delivery
6
7
  records, queues and retries are product decisions and stay in the product. This package takes a
@@ -45,7 +46,8 @@ takes a name and an address pair. `to` is in E.164 and is not validated here: wh
45
46
  accepts, and how it normalises one, is the product's own decision.
46
47
 
47
48
  `reference` is optional: the caller's own id for the message. An adapter whose provider refuses a
48
- repeated id sends it, so a send retried after a lost answer cannot deliver and charge twice.
49
+ repeated id sends it, so a send retried after a lost answer cannot deliver and charge twice. Twilio
50
+ takes no such id, so its adapter ignores `reference`.
49
51
 
50
52
  ## eSMS
51
53
 
@@ -117,6 +119,58 @@ eSMS signs nothing. The route that receives the callback checks the secret in it
117
119
  caller's address against `ESMS_CALLBACK_SOURCE_ADDRESSES`, redacts the query in its access log (it
118
120
  carries the number), and answers quickly. A query it cannot read throws `SmsDeliveryReportInvalid`.
119
121
 
122
+ ## Twilio
123
+
124
+ `@zeroxsolutions/sms/twilio` sends through [Twilio](https://www.twilio.com)'s Messages API, for
125
+ numbers outside the countries a local provider covers. It posts a form body with Basic auth over the
126
+ runtime's own `fetch`, so it installs nothing and runs on workerd as on Node.
127
+
128
+ ```ts
129
+ import { TwilioSmsSender } from '@zeroxsolutions/sms/twilio';
130
+
131
+ const sender = new TwilioSmsSender({
132
+ accountSid: env.TWILIO_ACCOUNT_SID,
133
+ authToken: env.TWILIO_AUTH_TOKEN,
134
+ });
135
+
136
+ const { messageId } = await sender.send({
137
+ from: 'MG0123456789abcdef0123456789abcdef',
138
+ to: '+821012345678',
139
+ text: 'Your code is 123456.',
140
+ });
141
+ ```
142
+
143
+ `from` is a Messaging Service id (`MG` and 32 hex digits), sent as `MessagingServiceSid`, or a Twilio
144
+ number or alphanumeric sender id, sent as `From`. A Messaging Service picks the sender per destination
145
+ from a pool the account configures. A `to` that is not E.164 is refused before any request is made.
146
+
147
+ `send` resolving means Twilio queued the message, not that the handset received it.
148
+
149
+ | Twilio error `code` | Refusal |
150
+ | ----------------------------------- | ------------------------- |
151
+ | `21211`, `21614` | `SmsRecipientInvalid` |
152
+ | `21612`, `21610` | `SmsRecipientUnreachable` |
153
+ | `21212`, `21606`, `21408` | `SmsSenderNotRegistered` |
154
+ | `21617` | `SmsMessageRejected` |
155
+ | `20429`, `21611`, or HTTP 429 | `SmsRateLimited` |
156
+ | 5xx, not JSON, or no answer in time | `SmsProviderUnavailable` |
157
+ | any other | `SmsSendFailed` |
158
+
159
+ `21610` is a recipient who replied STOP. `21408` is the account's geographic permissions refusing the
160
+ destination country. Twilio has no code for an empty balance: a suspended account answers `20005`,
161
+ which is `SmsSendFailed`.
162
+
163
+ Twilio takes no idempotency key. `SmsProviderUnavailable` may follow a send Twilio already queued, and
164
+ retrying it can deliver and charge twice.
165
+
166
+ The account has to meet three conditions no code can check:
167
+
168
+ 1. Each destination country is enabled in its geographic permissions, or Twilio answers `21408`.
169
+ 2. The sender suits each destination. The United States accepts no alphanumeric sender id.
170
+ 3. A trial account sends only to numbers verified in it.
171
+
172
+ Test credentials, with `+15005550006` as `from`, send nothing and charge nothing.
173
+
120
174
  ## Refusals
121
175
 
122
176
  A refusal is one plain `Error` subclass per meaning a caller acts on differently - no wire status, no
@@ -126,7 +180,7 @@ code, no envelope. A caller maps them by class.
126
180
  | -------------------------- | --------------------------------------------------------- |
127
181
  | `SmsRecipientInvalid` | the number is not routable |
128
182
  | `SmsRecipientUnreachable` | the carrier would not deliver to the subscriber |
129
- | `SmsSenderNotRegistered` | the sender is not a brandname this account may send under |
183
+ | `SmsSenderNotRegistered` | the account may not send from this sender or to this destination |
130
184
  | `SmsMessageRejected` | the text breaks a provider or carrier rule |
131
185
  | `SmsRateLimited` | the provider is throttling this account |
132
186
  | `SmsBalanceExhausted` | the account has no credit left |
@@ -18,7 +18,10 @@ export declare class SmsRecipientInvalid extends Error {
18
18
  export declare class SmsRecipientUnreachable extends Error {
19
19
  constructor(options?: ErrorOptions);
20
20
  }
21
- /** The sender is not a brandname this account may send under, so every message from it fails alike. */
21
+ /**
22
+ * The account may not send from this sender, or to this destination. An operator fixes it in the
23
+ * account, and every message it covers fails alike until then.
24
+ */
22
25
  export declare class SmsSenderNotRegistered extends Error {
23
26
  constructor(options?: ErrorOptions);
24
27
  }
@@ -37,7 +40,8 @@ export declare class SmsBalanceExhausted extends Error {
37
40
  /**
38
41
  * The provider failed on its own side; the message itself may be fine. This refusal can follow a send
39
42
  * the provider already accepted - a timeout or a lost answer - so a retry must reuse the same
40
- * `reference`.
43
+ * `reference`. An adapter whose provider takes no idempotency key, Twilio's, cannot make that retry
44
+ * safe, so a retry may deliver twice.
41
45
  */
42
46
  export declare class SmsProviderUnavailable extends Error {
43
47
  constructor(options?: ErrorOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/lib/sender/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,gHAAgH;AAChH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,oGAAoG;AACpG,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,uGAAuG;AACvG,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,sIAAsI;AACtI,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,mGAAmG;AACnG,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,yFAAyF;AACzF,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,sGAAsG;AACtG,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,2FAA2F;AAC3F,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,OAAO,CAAC,EAAE,YAAY;CAInC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/lib/sender/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,gHAAgH;AAChH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,oGAAoG;AACpG,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,sIAAsI;AACtI,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,mGAAmG;AACnG,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,yFAAyF;AACzF,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED;;;;;GAKG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,sGAAsG;AACtG,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,2FAA2F;AAC3F,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,OAAO,CAAC,EAAE,YAAY;CAInC"}
@@ -24,7 +24,10 @@ export class SmsRecipientUnreachable extends Error {
24
24
  this.name = 'SmsRecipientUnreachable';
25
25
  }
26
26
  }
27
- /** The sender is not a brandname this account may send under, so every message from it fails alike. */
27
+ /**
28
+ * The account may not send from this sender, or to this destination. An operator fixes it in the
29
+ * account, and every message it covers fails alike until then.
30
+ */
28
31
  export class SmsSenderNotRegistered extends Error {
29
32
  constructor(options) {
30
33
  super('The sender is not registered with the provider', options);
@@ -55,7 +58,8 @@ export class SmsBalanceExhausted extends Error {
55
58
  /**
56
59
  * The provider failed on its own side; the message itself may be fine. This refusal can follow a send
57
60
  * the provider already accepted - a timeout or a lost answer - so a retry must reuse the same
58
- * `reference`.
61
+ * `reference`. An adapter whose provider takes no idempotency key, Twilio's, cannot make that retry
62
+ * safe, so a retry may deliver twice.
59
63
  */
60
64
  export class SmsProviderUnavailable extends Error {
61
65
  constructor(options) {
@@ -1,7 +1,9 @@
1
1
  /** The port a product sends one composed message through; a runtime supplies the implementation. */
2
2
  /**
3
3
  * One message, already composed. `from` is a single string where a mailbox takes a name and an
4
- * address pair, because an SMS sender is a brandname or a short code registered with the carriers.
4
+ * address pair. Its form depends on the provider: a brandname or short code registered with the
5
+ * carriers, or through Twilio an E.164 number, an alphanumeric sender id or an `MG...` Messaging
6
+ * Service id.
5
7
  *
6
8
  * `to` is in E.164 and is not branded: which numbers a product accepts, and how it normalises one,
7
9
  * is the product's decision and not something this package can hold for it.
@@ -1 +1 @@
1
- {"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../../src/lib/sender/port.ts"],"names":[],"mappings":"AAAA,oGAAoG;AAEpG;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IACrD,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED,sFAAsF;AACtF,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACnD"}
1
+ {"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../../src/lib/sender/port.ts"],"names":[],"mappings":"AAAA,oGAAoG;AAEpG;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IACrD,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED,sFAAsF;AACtF,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACnD"}
@@ -0,0 +1,3 @@
1
+ /** The Twilio adapter: the sender backed by Twilio's Messages API. */
2
+ export { TwilioSmsSender, type TwilioSmsSenderOptions } from './sender.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/twilio/index.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ /** The Twilio adapter: the sender backed by Twilio's Messages API. */
2
+ export { TwilioSmsSender } from './sender.js';
@@ -0,0 +1,25 @@
1
+ import type { ISmsSender, SendSmsResult, SmsMessage } from '../sender/port.js';
2
+ /** How a `TwilioSmsSender` reaches the account. */
3
+ export interface TwilioSmsSenderOptions {
4
+ /** The account's `AC...` id: the path segment and the Basic-auth user. */
5
+ readonly accountSid: string;
6
+ /** The account's auth token: the Basic-auth password. */
7
+ readonly authToken: string;
8
+ /** Scheme and host the API path is appended to, Twilio's own by default. */
9
+ readonly origin?: string;
10
+ /** How long a send waits for Twilio before refusing with `SmsProviderUnavailable`, 10 000 by default. */
11
+ readonly timeoutMs?: number;
12
+ }
13
+ /**
14
+ * Backs the SMS port with Twilio's Messages API over the runtime's own `fetch`. `send` resolving
15
+ * means Twilio queued the message, not that the handset received it. Twilio takes no idempotency key,
16
+ * so `reference` is not sent and a send retried after a lost answer can deliver twice.
17
+ */
18
+ export declare class TwilioSmsSender implements ISmsSender {
19
+ private readonly options;
20
+ constructor(options: TwilioSmsSenderOptions);
21
+ send(message: SmsMessage): Promise<SendSmsResult>;
22
+ /** Sends `form` to the account's Messages resource; a rejected or timed-out call is the provider failing. */
23
+ private post;
24
+ }
25
+ //# sourceMappingURL=sender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sender.d.ts","sourceRoot":"","sources":["../../../src/lib/twilio/sender.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAwC/E,mDAAmD;AACnD,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yGAAyG;IACzG,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAaD;;;;GAIG;AACH,qBAAa,eAAgB,YAAW,UAAU;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,sBAAsB;IAEtD,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC;IAgCvD,6GAA6G;YAC/F,IAAI;CAgBnB"}
@@ -0,0 +1,101 @@
1
+ import { SmsMessageRejected, SmsProviderUnavailable, SmsRateLimited, SmsRecipientInvalid, SmsRecipientUnreachable, SmsSendFailed, SmsSenderNotRegistered, } from '../sender/errors.js';
2
+ /** Twilio's REST host; the default `origin`. */
3
+ const TWILIO_ORIGIN = 'https://api.twilio.com';
4
+ /** A Messaging Service id, which Twilio takes as `MessagingServiceSid` rather than `From`. */
5
+ const MESSAGING_SERVICE_SID = /^MG[0-9a-fA-F]{32}$/;
6
+ /** E.164: a plus, then 8 to 15 digits, the first not 0. Anything else never reaches the form body. */
7
+ const E164 = /^\+[1-9]\d{7,14}$/;
8
+ /** HTTP 429: Twilio throttling the account, whatever the body says. */
9
+ const TOO_MANY_REQUESTS = 429;
10
+ /**
11
+ * How long a send waits for Twilio before giving up, unless `timeoutMs` says otherwise. A Worker waits
12
+ * on a stalled subrequest as long as its client stays connected, so a send with no bound would hang
13
+ * the caller rather than fail fast.
14
+ */
15
+ const TWILIO_SEND_TIMEOUT_MS = 10_000;
16
+ /**
17
+ * Every Twilio error `code` a caller acts on differently, and the class it raises (Twilio's error
18
+ * dictionary). `21408` is the account's geographic permissions refusing the country, fixed in the
19
+ * account as a sender is registered. Any other code, `20003` and `20005` among them, raises
20
+ * `SmsSendFailed`.
21
+ */
22
+ const REFUSALS = {
23
+ '20429': SmsRateLimited,
24
+ '21211': SmsRecipientInvalid,
25
+ '21212': SmsSenderNotRegistered,
26
+ '21408': SmsSenderNotRegistered,
27
+ '21606': SmsSenderNotRegistered,
28
+ '21610': SmsRecipientUnreachable,
29
+ '21611': SmsRateLimited,
30
+ '21612': SmsRecipientUnreachable,
31
+ '21614': SmsRecipientInvalid,
32
+ '21617': SmsMessageRejected,
33
+ };
34
+ /** The class a Twilio error `code` raises - `SmsSendFailed` wherever it is not one of the table's own keys. */
35
+ function refusalClassOf(code) {
36
+ const key = typeof code === 'number' || typeof code === 'string' ? String(code) : undefined;
37
+ return key !== undefined && Object.hasOwn(REFUSALS, key) ? REFUSALS[key] : SmsSendFailed;
38
+ }
39
+ /** Whether `value` is a JSON object rather than an array, a primitive or null. */
40
+ function isObject(value) {
41
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
42
+ }
43
+ /**
44
+ * Backs the SMS port with Twilio's Messages API over the runtime's own `fetch`. `send` resolving
45
+ * means Twilio queued the message, not that the handset received it. Twilio takes no idempotency key,
46
+ * so `reference` is not sent and a send retried after a lost answer can deliver twice.
47
+ */
48
+ export class TwilioSmsSender {
49
+ options;
50
+ constructor(options) {
51
+ this.options = options;
52
+ }
53
+ async send(message) {
54
+ if (!E164.test(message.to))
55
+ throw new SmsRecipientInvalid();
56
+ const form = new URLSearchParams({ To: message.to });
57
+ form.set(MESSAGING_SERVICE_SID.test(message.from) ? 'MessagingServiceSid' : 'From', message.from);
58
+ form.set('Body', message.text);
59
+ const response = await this.post(form);
60
+ if (response.status === TOO_MANY_REQUESTS) {
61
+ const body = await response.text().catch(() => undefined);
62
+ throw new SmsRateLimited({ cause: { status: response.status, body } });
63
+ }
64
+ if (response.status >= 500) {
65
+ const body = await response.text().catch(() => undefined);
66
+ throw new SmsProviderUnavailable({ cause: { status: response.status, body } });
67
+ }
68
+ let answer;
69
+ try {
70
+ answer = await response.json();
71
+ }
72
+ catch (cause) {
73
+ throw new SmsProviderUnavailable({ cause });
74
+ }
75
+ if (response.ok) {
76
+ if (isObject(answer) && typeof answer['sid'] === 'string')
77
+ return { messageId: answer['sid'] };
78
+ throw new SmsSendFailed({ cause: answer });
79
+ }
80
+ const Refusal = refusalClassOf(isObject(answer) ? answer['code'] : undefined);
81
+ throw new Refusal({ cause: answer });
82
+ }
83
+ /** Sends `form` to the account's Messages resource; a rejected or timed-out call is the provider failing. */
84
+ async post(form) {
85
+ const origin = (this.options.origin ?? TWILIO_ORIGIN).replace(/\/+$/, '');
86
+ try {
87
+ return await fetch(`${origin}/2010-04-01/Accounts/${encodeURIComponent(this.options.accountSid)}/Messages.json`, {
88
+ method: 'POST',
89
+ headers: {
90
+ authorization: `Basic ${btoa(`${this.options.accountSid}:${this.options.authToken}`)}`,
91
+ 'content-type': 'application/x-www-form-urlencoded',
92
+ },
93
+ body: form,
94
+ signal: AbortSignal.timeout(this.options.timeoutMs ?? TWILIO_SEND_TIMEOUT_MS),
95
+ });
96
+ }
97
+ catch (cause) {
98
+ throw new SmsProviderUnavailable({ cause });
99
+ }
100
+ }
101
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/sms",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
- "description": "The SMS seam: a composed message, the port that sends it, its typed refusals, and the eSMS adapter behind that port. Knows nothing of templates, locales or delivery records - those stay in the product.",
7
+ "description": "The SMS seam: a composed message, the port that sends it, its typed refusals, and the eSMS and Twilio adapters behind that port. Knows nothing of templates, locales or delivery records - those stay in the product.",
8
8
  "main": "./dist/index.js",
9
9
  "module": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
@@ -25,6 +25,11 @@
25
25
  "types": "./dist/lib/esms/index.d.ts",
26
26
  "import": "./dist/lib/esms/index.js",
27
27
  "default": "./dist/lib/esms/index.js"
28
+ },
29
+ "./twilio": {
30
+ "types": "./dist/lib/twilio/index.d.ts",
31
+ "import": "./dist/lib/twilio/index.js",
32
+ "default": "./dist/lib/twilio/index.js"
28
33
  }
29
34
  },
30
35
  "dependencies": {