@absolutejs/dispatch-twilio 0.0.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/README.md +81 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +10 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @absolutejs/dispatch-twilio
|
|
2
|
+
|
|
3
|
+
Twilio-backed `SmsAdapter` for
|
|
4
|
+
[@absolutejs/dispatch](https://github.com/absolutejs/dispatch).
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
bun add @absolutejs/dispatch @absolutejs/dispatch-twilio twilio
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import twilio from 'twilio';
|
|
16
|
+
import { createDispatcher } from '@absolutejs/dispatch';
|
|
17
|
+
import { createTwilioAdapter } from '@absolutejs/dispatch-twilio';
|
|
18
|
+
|
|
19
|
+
const twilioClient = twilio(
|
|
20
|
+
process.env.TWILIO_ACCOUNT_SID!,
|
|
21
|
+
process.env.TWILIO_AUTH_TOKEN!
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const dispatcher = createDispatcher({
|
|
25
|
+
sms: createTwilioAdapter({
|
|
26
|
+
client: twilioClient,
|
|
27
|
+
defaultFrom: '+15551234567',
|
|
28
|
+
// OR — instead of a single from number, use a Messaging Service:
|
|
29
|
+
// messagingServiceSid: 'MG...',
|
|
30
|
+
// Optional webhook for status updates (queued → sent → delivered/failed):
|
|
31
|
+
// statusCallback: 'https://hooks.acme.io/twilio',
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const result = await dispatcher.sms({
|
|
36
|
+
to: '+12025550100',
|
|
37
|
+
body: 'Your verification code: 482910',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(result.id); // Twilio Message SID (SM...)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
createTwilioAdapter({
|
|
47
|
+
client, // Required — your twilio(sid, token)
|
|
48
|
+
defaultFrom?, // E.164 origination — required if no service
|
|
49
|
+
messagingServiceSid?, // Use service-based routing instead of `from`
|
|
50
|
+
statusCallback?, // Webhook URL for delivery status updates
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Sender precedence
|
|
55
|
+
|
|
56
|
+
1. `message.from` (per-call) — always wins
|
|
57
|
+
2. `defaultFrom` (adapter option)
|
|
58
|
+
3. `messagingServiceSid` (adapter option)
|
|
59
|
+
|
|
60
|
+
If none are set, the adapter throws.
|
|
61
|
+
|
|
62
|
+
## Error mapping
|
|
63
|
+
|
|
64
|
+
Twilio's SDK rejects on most errors (rate limit, invalid number,
|
|
65
|
+
account suspended). The adapter lets the rejection propagate so
|
|
66
|
+
`@absolutejs/dispatch`'s error path runs (failed counter, ERROR span,
|
|
67
|
+
`dispatch.sms.failed` audit).
|
|
68
|
+
|
|
69
|
+
A returned response with `errorCode != null` is the
|
|
70
|
+
bulk-send/queue-rejection case — Twilio doesn't throw but signals
|
|
71
|
+
the error in the response. The adapter throws on this too:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
{ errorCode: 21610, errorMessage: 'Recipient unsubscribed' }
|
|
75
|
+
// → throws "Twilio errorCode 21610: Recipient unsubscribed"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
[Apache 2.0](../LICENSE). Tier B substrate-adjacent — rides
|
|
81
|
+
`@absolutejs/dispatch` (BSL Tier A) and `twilio` (MIT).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @absolutejs/dispatch-twilio — Twilio-backed `SmsAdapter` for
|
|
3
|
+
* `@absolutejs/dispatch`.
|
|
4
|
+
*
|
|
5
|
+
* Takes the user's Twilio client. Maps `SmsMessage` to Twilio's
|
|
6
|
+
* `messages.create` params; surfaces the Message SID as
|
|
7
|
+
* `DispatchResult.id`.
|
|
8
|
+
*
|
|
9
|
+
* Twilio's `messages.create` throws on API errors (rate limiting,
|
|
10
|
+
* invalid number, account suspended, etc), so the adapter's error
|
|
11
|
+
* path is "let Twilio's rejection propagate." `@absolutejs/dispatch`
|
|
12
|
+
* captures it on the `dispatch.sms.send` span, bumps the failed
|
|
13
|
+
* counter, and emits `dispatch.sms.failed`.
|
|
14
|
+
*
|
|
15
|
+
* **Two ways to identify the sender.** Twilio supports either a phone
|
|
16
|
+
* number (`from`) OR a Messaging Service SID (`messagingServiceSid`).
|
|
17
|
+
* Pass `messagingServiceSid` in adapter options to use service-based
|
|
18
|
+
* sending (per-region routing, sender pool, content opt-out
|
|
19
|
+
* management); leave it unset to require `from` per message or
|
|
20
|
+
* `defaultFrom`.
|
|
21
|
+
*/
|
|
22
|
+
import type { SmsAdapter } from '@absolutejs/dispatch';
|
|
23
|
+
/**
|
|
24
|
+
* Minimal subset of Twilio's client we use. `client.messages.create`
|
|
25
|
+
* is the canonical send entry. Twilio's typed SDK is large; we don't
|
|
26
|
+
* pull its types in — `TwilioClientLike` describes only the shape
|
|
27
|
+
* we touch.
|
|
28
|
+
*/
|
|
29
|
+
export type TwilioClientLike = {
|
|
30
|
+
messages: {
|
|
31
|
+
create: (params: {
|
|
32
|
+
to: string;
|
|
33
|
+
body: string;
|
|
34
|
+
from?: string;
|
|
35
|
+
messagingServiceSid?: string;
|
|
36
|
+
statusCallback?: string;
|
|
37
|
+
}) => Promise<{
|
|
38
|
+
sid?: string;
|
|
39
|
+
status?: string;
|
|
40
|
+
errorCode?: number | null;
|
|
41
|
+
errorMessage?: string | null;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export type CreateTwilioAdapterOptions = {
|
|
46
|
+
/** The Twilio client (`twilio(accountSid, authToken)`). */
|
|
47
|
+
client: TwilioClientLike;
|
|
48
|
+
/**
|
|
49
|
+
* Default origination number (E.164). Required if your messages
|
|
50
|
+
* don't supply `from` AND you're not using a Messaging Service.
|
|
51
|
+
*/
|
|
52
|
+
defaultFrom?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Twilio Messaging Service SID. When set, the adapter uses
|
|
55
|
+
* service-based routing instead of a single origination number.
|
|
56
|
+
* Pass this OR `defaultFrom`, not both — per-message `from`
|
|
57
|
+
* overrides this on a per-call basis.
|
|
58
|
+
*/
|
|
59
|
+
messagingServiceSid?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Twilio status callback URL — invoked when message status
|
|
62
|
+
* changes (queued → sent → delivered/failed). Wire this to your
|
|
63
|
+
* own webhook to record delivery in audit.
|
|
64
|
+
*/
|
|
65
|
+
statusCallback?: string;
|
|
66
|
+
};
|
|
67
|
+
export declare const createTwilioAdapter: (options: CreateTwilioAdapterOptions) => SmsAdapter;
|
|
68
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,sBAAsB,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC9B,QAAQ,EAAE;QACT,MAAM,EAAE,CAAC,MAAM,EAAE;YAChB,EAAE,EAAE,MAAM,CAAC;YACX,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,mBAAmB,CAAC,EAAE,MAAM,CAAC;YAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;SACxB,KAAK,OAAO,CAAC;YACb,GAAG,CAAC,EAAE,MAAM,CAAC;YACb,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;YAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;SAC7B,CAAC,CAAC;KACH,CAAC;CACF,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACxC,2DAA2D;IAC3D,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC/B,SAAS,0BAA0B,KACjC,UA+CF,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
var createTwilioAdapter = (options) => {
|
|
4
|
+
const { client } = options;
|
|
5
|
+
return {
|
|
6
|
+
name: "twilio",
|
|
7
|
+
send: async (message) => {
|
|
8
|
+
const from = message.from ?? options.defaultFrom;
|
|
9
|
+
if (from === undefined && options.messagingServiceSid === undefined) {
|
|
10
|
+
throw new Error("[dispatch-twilio] no sender configured. " + "Pass `message.from`, `createTwilioAdapter({ defaultFrom })`, or " + "`createTwilioAdapter({ messagingServiceSid })`.");
|
|
11
|
+
}
|
|
12
|
+
const params = {
|
|
13
|
+
body: message.body,
|
|
14
|
+
to: message.to
|
|
15
|
+
};
|
|
16
|
+
if (from !== undefined)
|
|
17
|
+
params.from = from;
|
|
18
|
+
else if (options.messagingServiceSid !== undefined) {
|
|
19
|
+
params.messagingServiceSid = options.messagingServiceSid;
|
|
20
|
+
}
|
|
21
|
+
if (options.statusCallback !== undefined) {
|
|
22
|
+
params.statusCallback = options.statusCallback;
|
|
23
|
+
}
|
|
24
|
+
const response = await client.messages.create(params);
|
|
25
|
+
if (response.errorCode !== null && response.errorCode !== undefined) {
|
|
26
|
+
throw new Error(`[dispatch-twilio] Twilio errorCode ${response.errorCode}: ${response.errorMessage ?? "(no message)"}`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
at: Date.now(),
|
|
30
|
+
...response.sid !== undefined ? { id: response.sid } : {},
|
|
31
|
+
provider: "twilio"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
createTwilioAdapter
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
//# debugId=92068E85E39BDEE164756E2164756E21
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @absolutejs/dispatch-twilio — Twilio-backed `SmsAdapter` for\n * `@absolutejs/dispatch`.\n *\n * Takes the user's Twilio client. Maps `SmsMessage` to Twilio's\n * `messages.create` params; surfaces the Message SID as\n * `DispatchResult.id`.\n *\n * Twilio's `messages.create` throws on API errors (rate limiting,\n * invalid number, account suspended, etc), so the adapter's error\n * path is \"let Twilio's rejection propagate.\" `@absolutejs/dispatch`\n * captures it on the `dispatch.sms.send` span, bumps the failed\n * counter, and emits `dispatch.sms.failed`.\n *\n * **Two ways to identify the sender.** Twilio supports either a phone\n * number (`from`) OR a Messaging Service SID (`messagingServiceSid`).\n * Pass `messagingServiceSid` in adapter options to use service-based\n * sending (per-region routing, sender pool, content opt-out\n * management); leave it unset to require `from` per message or\n * `defaultFrom`.\n */\nimport type { SmsAdapter, SmsMessage } from '@absolutejs/dispatch';\n\n/**\n * Minimal subset of Twilio's client we use. `client.messages.create`\n * is the canonical send entry. Twilio's typed SDK is large; we don't\n * pull its types in — `TwilioClientLike` describes only the shape\n * we touch.\n */\nexport type TwilioClientLike = {\n\tmessages: {\n\t\tcreate: (params: {\n\t\t\tto: string;\n\t\t\tbody: string;\n\t\t\tfrom?: string;\n\t\t\tmessagingServiceSid?: string;\n\t\t\tstatusCallback?: string;\n\t\t}) => Promise<{\n\t\t\tsid?: string;\n\t\t\tstatus?: string;\n\t\t\terrorCode?: number | null;\n\t\t\terrorMessage?: string | null;\n\t\t}>;\n\t};\n};\n\nexport type CreateTwilioAdapterOptions = {\n\t/** The Twilio client (`twilio(accountSid, authToken)`). */\n\tclient: TwilioClientLike;\n\t/**\n\t * Default origination number (E.164). Required if your messages\n\t * don't supply `from` AND you're not using a Messaging Service.\n\t */\n\tdefaultFrom?: string;\n\t/**\n\t * Twilio Messaging Service SID. When set, the adapter uses\n\t * service-based routing instead of a single origination number.\n\t * Pass this OR `defaultFrom`, not both — per-message `from`\n\t * overrides this on a per-call basis.\n\t */\n\tmessagingServiceSid?: string;\n\t/**\n\t * Twilio status callback URL — invoked when message status\n\t * changes (queued → sent → delivered/failed). Wire this to your\n\t * own webhook to record delivery in audit.\n\t */\n\tstatusCallback?: string;\n};\n\nexport const createTwilioAdapter = (\n\toptions: CreateTwilioAdapterOptions\n): SmsAdapter => {\n\tconst { client } = options;\n\n\treturn {\n\t\tname: 'twilio',\n\t\tsend: async (message: SmsMessage) => {\n\t\t\tconst from = message.from ?? options.defaultFrom;\n\t\t\tif (\n\t\t\t\tfrom === undefined &&\n\t\t\t\toptions.messagingServiceSid === undefined\n\t\t\t) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'[dispatch-twilio] no sender configured. ' +\n\t\t\t\t\t\t'Pass `message.from`, `createTwilioAdapter({ defaultFrom })`, or ' +\n\t\t\t\t\t\t'`createTwilioAdapter({ messagingServiceSid })`.'\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst params: Parameters<TwilioClientLike['messages']['create']>[0] = {\n\t\t\t\tbody: message.body,\n\t\t\t\tto: message.to\n\t\t\t};\n\t\t\tif (from !== undefined) params.from = from;\n\t\t\telse if (options.messagingServiceSid !== undefined) {\n\t\t\t\tparams.messagingServiceSid = options.messagingServiceSid;\n\t\t\t}\n\t\t\tif (options.statusCallback !== undefined) {\n\t\t\t\tparams.statusCallback = options.statusCallback;\n\t\t\t}\n\t\t\tconst response = await client.messages.create(params);\n\t\t\t// Twilio's SDK rejects on API errors via thrown errors, so a\n\t\t\t// returned response with `errorCode != null` is the\n\t\t\t// unusual-but-documented case (some bulk-send flows).\n\t\t\tif (\n\t\t\t\tresponse.errorCode !== null &&\n\t\t\t\tresponse.errorCode !== undefined\n\t\t\t) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[dispatch-twilio] Twilio errorCode ${response.errorCode}: ${response.errorMessage ?? '(no message)'}`\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tat: Date.now(),\n\t\t\t\t...(response.sid !== undefined ? { id: response.sid } : {}),\n\t\t\t\tprovider: 'twilio'\n\t\t\t};\n\t\t}\n\t};\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;AAqEO,IAAM,sBAAsB,CAClC,YACgB;AAAA,EAChB,QAAQ,WAAW;AAAA,EAEnB,OAAO;AAAA,IACN,MAAM;AAAA,IACN,MAAM,OAAO,YAAwB;AAAA,MACpC,MAAM,OAAO,QAAQ,QAAQ,QAAQ;AAAA,MACrC,IACC,SAAS,aACT,QAAQ,wBAAwB,WAC/B;AAAA,QACD,MAAM,IAAI,MACT,6CACC,qEACA,iDACF;AAAA,MACD;AAAA,MACA,MAAM,SAAgE;AAAA,QACrE,MAAM,QAAQ;AAAA,QACd,IAAI,QAAQ;AAAA,MACb;AAAA,MACA,IAAI,SAAS;AAAA,QAAW,OAAO,OAAO;AAAA,MACjC,SAAI,QAAQ,wBAAwB,WAAW;AAAA,QACnD,OAAO,sBAAsB,QAAQ;AAAA,MACtC;AAAA,MACA,IAAI,QAAQ,mBAAmB,WAAW;AAAA,QACzC,OAAO,iBAAiB,QAAQ;AAAA,MACjC;AAAA,MACA,MAAM,WAAW,MAAM,OAAO,SAAS,OAAO,MAAM;AAAA,MAIpD,IACC,SAAS,cAAc,QACvB,SAAS,cAAc,WACtB;AAAA,QACD,MAAM,IAAI,MACT,sCAAsC,SAAS,cAAc,SAAS,gBAAgB,gBACvF;AAAA,MACD;AAAA,MACA,OAAO;AAAA,QACN,IAAI,KAAK,IAAI;AAAA,WACT,SAAS,QAAQ,YAAY,EAAE,IAAI,SAAS,IAAI,IAAI,CAAC;AAAA,QACzD,UAAU;AAAA,MACX;AAAA;AAAA,EAEF;AAAA;",
|
|
8
|
+
"debugId": "92068E85E39BDEE164756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/dispatch-twilio",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Twilio-backed SmsAdapter for @absolutejs/dispatch. Maps SmsMessage to Twilio messages.create; surfaces the Message SID as DispatchResult.id.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/absolutejs/dispatch-adapters.git",
|
|
8
|
+
"directory": "twilio"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"author": "Alex Kahn",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"dispatch",
|
|
21
|
+
"sms",
|
|
22
|
+
"twilio",
|
|
23
|
+
"absolutejs",
|
|
24
|
+
"transactional"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun --external @absolutejs/dispatch --external twilio && tsc --project tsconfig.build.json",
|
|
28
|
+
"test": "bun test",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
31
|
+
"release": "bun run format && bun run build && bun publish"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@absolutejs/dispatch": ">= 0.0.1",
|
|
35
|
+
"twilio": ">= 5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@absolutejs/dispatch": "^0.0.1",
|
|
39
|
+
"@types/bun": "1.3.14",
|
|
40
|
+
"prettier": "3.5.3",
|
|
41
|
+
"typescript": "5.8.3"
|
|
42
|
+
},
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"import": "./dist/index.js",
|
|
47
|
+
"default": "./dist/index.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist",
|
|
52
|
+
"README.md"
|
|
53
|
+
]
|
|
54
|
+
}
|