@zeroxsolutions/sms 0.0.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 ADDED
@@ -0,0 +1,10 @@
1
+ ## 0.0.2 (2026-09-22)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **sms:** add the package, the send port and its refusals ([8eff593](https://github.com/zeroxsolutions/zeroxsolutions/commit/8eff593))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Claude Opus 5 (1M context)
10
+ - Lương Văn Tú
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @zeroxsolutions/sms
2
+
3
+ The SMS seam: a composed message, the port that sends it, and its typed refusals.
4
+
5
+ What it does not own is the reason an SMS exists at all - templates, message keys, locales, delivery
6
+ records, queues and retries are product decisions and stay in the product. This package takes a
7
+ message someone else composed and hands back the id the provider filed it under.
8
+
9
+ It is a sibling of `@zeroxsolutions/mail` rather than a subpath of it. That package's concern is
10
+ named for one channel, and a consumer installing something called mail to send an SMS reads a name
11
+ that describes nothing.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ pnpm add @zeroxsolutions/sms
17
+ ```
18
+
19
+ ## The port
20
+
21
+ The package ships no adapter, so a consumer implements `ISmsSender` itself and calls `send` with a
22
+ composed message:
23
+
24
+ ```ts
25
+ import type { ISmsSender, SendSmsResult, SmsMessage } from '@zeroxsolutions/sms';
26
+
27
+ /** Yours to write: one per provider, wrapping that provider's SDK. */
28
+ declare function sendThroughTheProviderSdk(message: SmsMessage): Promise<string>;
29
+
30
+ class ProviderSender implements ISmsSender {
31
+ async send(message: SmsMessage): Promise<SendSmsResult> {
32
+ return { messageId: await sendThroughTheProviderSdk(message) };
33
+ }
34
+ }
35
+
36
+ const { messageId } = await new ProviderSender().send({
37
+ from: 'TRIPVN',
38
+ to: '+84900000000',
39
+ text: 'Ma xac thuc cua ban la 123456.',
40
+ });
41
+ ```
42
+
43
+ `from` is the brandname or short code registered with the carriers, a single string where a mailbox
44
+ takes a name and an address pair. `to` is in E.164 and is not validated here: which numbers a product
45
+ accepts, and how it normalises one, is the product's own decision.
46
+
47
+ ## No adapter yet
48
+
49
+ This package publishes the port and its refusals and no implementation. An adapter wraps one
50
+ provider's SDK, and the table mapping that provider's codes onto the refusals below can only be
51
+ written against that provider's documentation. When an account exists, the adapter arrives as its own
52
+ subpath, `@zeroxsolutions/sms/<provider>`, with that SDK as an optional peer dependency, so a
53
+ consumer that never imports it does not install it.
54
+
55
+ Two things will constrain that choice, and both are the consumer's: the SDK has to run on workerd,
56
+ and A2P delivery in Vietnam goes through a brandname registered with the carriers, whose registration
57
+ also fixes the text a message may carry.
58
+
59
+ ## Refusals
60
+
61
+ A refusal is one plain `Error` subclass per meaning a caller acts on differently - no wire status, no
62
+ code, no envelope. A caller maps them by class.
63
+
64
+ | Class | What happened |
65
+ | --- | --- |
66
+ | `SmsRecipientInvalid` | the number is not routable |
67
+ | `SmsRecipientUnreachable` | the carrier would not deliver to the subscriber |
68
+ | `SmsSenderNotRegistered` | the sender is not a brandname this account may send under |
69
+ | `SmsMessageRejected` | the text breaks a provider or carrier rule |
70
+ | `SmsRateLimited` | the provider is throttling this account |
71
+ | `SmsBalanceExhausted` | the account has no credit left |
72
+ | `SmsProviderUnavailable` | the provider failed on its own side |
73
+ | `SmsSendFailed` | a refusal none of the above names |
74
+
75
+ Every message is a constant. The provider's own text names the number it refused, so it stays in
76
+ `cause`: log the class, never `cause` and never `String(error)`.
77
+
78
+ There is no opt-out class. The first consumer sends transactional codes, which an opt-out does not
79
+ cover. A consumer that sends marketing adds one, and it arrives as a minor release rather than as a
80
+ field on an existing class.
@@ -0,0 +1,4 @@
1
+ /** The SMS seam: one composed message, the port that sends it, and the refusals a provider raises through it. */
2
+ export { SmsBalanceExhausted, SmsMessageRejected, SmsProviderUnavailable, SmsRateLimited, SmsRecipientInvalid, SmsRecipientUnreachable, SmsSendFailed, SmsSenderNotRegistered, } from './lib/sender/errors.js';
3
+ export type { ISmsSender, SendSmsResult, SmsMessage } from './lib/sender/port.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iHAAiH;AACjH,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,aAAa,EACb,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ /** The SMS seam: one composed message, the port that sends it, and the refusals a provider raises through it. */
2
+ export { SmsBalanceExhausted, SmsMessageRejected, SmsProviderUnavailable, SmsRateLimited, SmsRecipientInvalid, SmsRecipientUnreachable, SmsSendFailed, SmsSenderNotRegistered, } from './lib/sender/errors.js';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What a provider's refusal means, one class per meaning a caller acts on differently. An adapter
3
+ * translates its own provider's codes onto these, so nothing above the port names a provider.
4
+ *
5
+ * Every message is a constant. The provider's own text - which often names the number - stays in
6
+ * `cause`, so a caller logs the class and never `cause` or `String(error)`.
7
+ *
8
+ * There is no opt-out class. The first consumer sends transactional codes, which an opt-out does not
9
+ * cover; a consumer that sends marketing adds one.
10
+ */
11
+ /** The number is not routable: malformed, or a range no carrier serves. Sending it again fails the same way. */
12
+ export declare class SmsRecipientInvalid extends Error {
13
+ constructor(options?: ErrorOptions);
14
+ }
15
+ /** The carrier refused the recipient: the subscriber is absent, barred, or blocking this sender. */
16
+ export declare class SmsRecipientUnreachable extends Error {
17
+ constructor(options?: ErrorOptions);
18
+ }
19
+ /** The sender is not a brandname this account may send under, so every message from it fails alike. */
20
+ export declare class SmsSenderNotRegistered extends Error {
21
+ constructor(options?: ErrorOptions);
22
+ }
23
+ /** The text breaks a provider or carrier rule: it departs from the registered pattern, exceeds a length, or carries a barred word. */
24
+ export declare class SmsMessageRejected extends Error {
25
+ constructor(options?: ErrorOptions);
26
+ }
27
+ /** The provider is throttling this account's send rate; the same message may be accepted later. */
28
+ export declare class SmsRateLimited extends Error {
29
+ constructor(options?: ErrorOptions);
30
+ }
31
+ /** The account has no credit left, so nothing more is accepted until it is topped up. */
32
+ export declare class SmsBalanceExhausted extends Error {
33
+ constructor(options?: ErrorOptions);
34
+ }
35
+ /** The provider failed on its own side; the message itself may be fine. */
36
+ export declare class SmsProviderUnavailable extends Error {
37
+ constructor(options?: ErrorOptions);
38
+ }
39
+ /** A refusal no other class names: a code the adapter does not know, or a throw that carried none. */
40
+ export declare class SmsSendFailed extends Error {
41
+ constructor(options?: ErrorOptions);
42
+ }
43
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/lib/sender/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;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,2EAA2E;AAC3E,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,CAAC,EAAE,YAAY;CAInC;AAED,sGAAsG;AACtG,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,CAAC,EAAE,YAAY;CAInC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * What a provider's refusal means, one class per meaning a caller acts on differently. An adapter
3
+ * translates its own provider's codes onto these, so nothing above the port names a provider.
4
+ *
5
+ * Every message is a constant. The provider's own text - which often names the number - stays in
6
+ * `cause`, so a caller logs the class and never `cause` or `String(error)`.
7
+ *
8
+ * There is no opt-out class. The first consumer sends transactional codes, which an opt-out does not
9
+ * cover; a consumer that sends marketing adds one.
10
+ */
11
+ /** The number is not routable: malformed, or a range no carrier serves. Sending it again fails the same way. */
12
+ export class SmsRecipientInvalid extends Error {
13
+ constructor(options) {
14
+ super('The recipient number is not routable', options);
15
+ this.name = 'SmsRecipientInvalid';
16
+ }
17
+ }
18
+ /** The carrier refused the recipient: the subscriber is absent, barred, or blocking this sender. */
19
+ export class SmsRecipientUnreachable extends Error {
20
+ constructor(options) {
21
+ super('The carrier would not deliver to this recipient', options);
22
+ this.name = 'SmsRecipientUnreachable';
23
+ }
24
+ }
25
+ /** The sender is not a brandname this account may send under, so every message from it fails alike. */
26
+ export class SmsSenderNotRegistered extends Error {
27
+ constructor(options) {
28
+ super('The sender is not registered with the provider', options);
29
+ this.name = 'SmsSenderNotRegistered';
30
+ }
31
+ }
32
+ /** The text breaks a provider or carrier rule: it departs from the registered pattern, exceeds a length, or carries a barred word. */
33
+ export class SmsMessageRejected extends Error {
34
+ constructor(options) {
35
+ super('The provider rejected the message as composed', options);
36
+ this.name = 'SmsMessageRejected';
37
+ }
38
+ }
39
+ /** The provider is throttling this account's send rate; the same message may be accepted later. */
40
+ export class SmsRateLimited extends Error {
41
+ constructor(options) {
42
+ super('The provider is limiting the send rate', options);
43
+ this.name = 'SmsRateLimited';
44
+ }
45
+ }
46
+ /** The account has no credit left, so nothing more is accepted until it is topped up. */
47
+ export class SmsBalanceExhausted extends Error {
48
+ constructor(options) {
49
+ super('The sending account has no balance left', options);
50
+ this.name = 'SmsBalanceExhausted';
51
+ }
52
+ }
53
+ /** The provider failed on its own side; the message itself may be fine. */
54
+ export class SmsProviderUnavailable extends Error {
55
+ constructor(options) {
56
+ super('The sms provider failed internally', options);
57
+ this.name = 'SmsProviderUnavailable';
58
+ }
59
+ }
60
+ /** A refusal no other class names: a code the adapter does not know, or a throw that carried none. */
61
+ export class SmsSendFailed extends Error {
62
+ constructor(options) {
63
+ super('The sms provider refused the message', options);
64
+ this.name = 'SmsSendFailed';
65
+ }
66
+ }
@@ -0,0 +1,22 @@
1
+ /** The port a product sends one composed message through; a runtime supplies the implementation. */
2
+ /**
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.
5
+ *
6
+ * `to` is in E.164 and is not branded: which numbers a product accepts, and how it normalises one,
7
+ * is the product's decision and not something this package can hold for it.
8
+ */
9
+ export interface SmsMessage {
10
+ readonly from: string;
11
+ readonly to: string;
12
+ readonly text: string;
13
+ }
14
+ /** What the provider hands back: the id its own logs and delivery reports name this message by. */
15
+ export interface SendSmsResult {
16
+ readonly messageId: string;
17
+ }
18
+ /** Sends one composed message. The only seam a product substitutes to send an SMS. */
19
+ export interface ISmsSender {
20
+ send(message: SmsMessage): Promise<SendSmsResult>;
21
+ }
22
+ //# sourceMappingURL=port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../../src/lib/sender/port.ts"],"names":[],"mappings":"AAAA,oGAAoG;AAEpG;;;;;;GAMG;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;CACvB;AAED,mGAAmG;AACnG,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,sFAAsF;AACtF,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACnD"}
@@ -0,0 +1,2 @@
1
+ /** The port a product sends one composed message through; a runtime supplies the implementation. */
2
+ export {};
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@zeroxsolutions/sms",
3
+ "version": "0.0.2",
4
+ "private": false,
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "description": "The SMS seam: a composed message, the port that sends it, and its typed refusals. Knows nothing of templates, locales, delivery records or which provider carries the message - those stay in the product.",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ "!**/*.tsbuildinfo"
16
+ ],
17
+ "exports": {
18
+ "./package.json": "./package.json",
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "default": "./dist/index.js"
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "tslib": "^2.3.0"
27
+ }
28
+ }