@stacksjs/sms 0.70.87 → 0.70.90
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/dist/drivers/gupshup.d.ts +1 -0
- package/dist/drivers/gupshup.js +0 -0
- package/dist/drivers/index.js +3 -0
- package/dist/drivers/nexmo.d.ts +1 -0
- package/dist/drivers/nexmo.js +0 -0
- package/dist/drivers/plivo.d.ts +1 -0
- package/dist/drivers/plivo.js +0 -0
- package/dist/drivers/sms77.d.ts +1 -0
- package/dist/drivers/sms77.js +0 -0
- package/dist/drivers/sns.d.ts +1 -0
- package/dist/drivers/sns.js +0 -0
- package/dist/drivers/telnyx.d.ts +1 -0
- package/dist/drivers/telnyx.js +0 -0
- package/dist/drivers/termii.d.ts +1 -0
- package/dist/drivers/termii.js +0 -0
- package/dist/drivers/twilio.js +263 -0
- package/dist/drivers/vonage.js +333 -0
- package/dist/index.js +5 -2
- package/dist/sms.js +220 -0
- package/package.json +5 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const API_BASE = "https://api.twilio.com/2010-04-01", VERIFY_API_BASE = "https://verify.twilio.com/v2";
|
|
2
|
+
async function fetchWithRetry(url, init, maxRetries = 3) {
|
|
3
|
+
let attempt = 0;
|
|
4
|
+
while (!0) {
|
|
5
|
+
const response = await fetch(url, init);
|
|
6
|
+
if (response.status !== 429 && response.status !== 503)
|
|
7
|
+
return response;
|
|
8
|
+
if (attempt >= maxRetries)
|
|
9
|
+
return response;
|
|
10
|
+
const retryAfter = Number(response.headers.get("retry-after")) || 2 ** attempt;
|
|
11
|
+
await new Promise((r) => setTimeout(r, Math.min(retryAfter, 30) * 1000));
|
|
12
|
+
attempt++;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class TwilioDriver {
|
|
17
|
+
config;
|
|
18
|
+
verifyServiceSid;
|
|
19
|
+
constructor(config, verifyServiceSid) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.verifyServiceSid = verifyServiceSid;
|
|
22
|
+
}
|
|
23
|
+
async send(message) {
|
|
24
|
+
const recipients = Array.isArray(message.to) ? message.to : [message.to];
|
|
25
|
+
if (recipients.length > 1)
|
|
26
|
+
return (await this.sendBulk(recipients.map((to) => ({ ...message, to }))))[0] ?? {
|
|
27
|
+
success: !1,
|
|
28
|
+
to: recipients[0] ?? "",
|
|
29
|
+
error: "No recipients supplied",
|
|
30
|
+
provider: "twilio"
|
|
31
|
+
};
|
|
32
|
+
const to = recipients[0];
|
|
33
|
+
if (!to)
|
|
34
|
+
return {
|
|
35
|
+
success: !1,
|
|
36
|
+
to: "",
|
|
37
|
+
error: "No recipient supplied",
|
|
38
|
+
provider: "twilio"
|
|
39
|
+
};
|
|
40
|
+
const from = message.from || this.config.from;
|
|
41
|
+
if (!from && !this.config.messagingServiceSid)
|
|
42
|
+
return {
|
|
43
|
+
success: !1,
|
|
44
|
+
to,
|
|
45
|
+
error: 'No "from" number or messaging service SID configured',
|
|
46
|
+
provider: "twilio"
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
const body = new URLSearchParams;
|
|
50
|
+
body.append("To", to);
|
|
51
|
+
body.append("Body", message.body);
|
|
52
|
+
if (this.config.messagingServiceSid)
|
|
53
|
+
body.append("MessagingServiceSid", this.config.messagingServiceSid);
|
|
54
|
+
else if (from)
|
|
55
|
+
body.append("From", from);
|
|
56
|
+
if (message.statusCallback || this.config.statusCallback)
|
|
57
|
+
body.append("StatusCallback", message.statusCallback || this.config.statusCallback);
|
|
58
|
+
if (message.mediaUrls && message.mediaUrls.length > 0)
|
|
59
|
+
for (const mediaUrl of message.mediaUrls)
|
|
60
|
+
body.append("MediaUrl", mediaUrl);
|
|
61
|
+
const response = await fetchWithRetry(`${API_BASE}/Accounts/${this.config.accountSid}/Messages.json`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`,
|
|
65
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
66
|
+
},
|
|
67
|
+
body: body.toString()
|
|
68
|
+
}), data = await response.json();
|
|
69
|
+
if (!response.ok)
|
|
70
|
+
return {
|
|
71
|
+
success: !1,
|
|
72
|
+
to,
|
|
73
|
+
error: data.message || `HTTP ${response.status}`,
|
|
74
|
+
provider: "twilio"
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
success: !0,
|
|
78
|
+
messageId: data.sid,
|
|
79
|
+
status: mapTwilioStatus(data.status),
|
|
80
|
+
to,
|
|
81
|
+
provider: "twilio",
|
|
82
|
+
segments: data.num_segments ? Number.parseInt(data.num_segments, 10) : void 0,
|
|
83
|
+
price: data.price ? Math.abs(Number.parseFloat(data.price)) : void 0,
|
|
84
|
+
currency: data.price_unit
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
success: !1,
|
|
89
|
+
to,
|
|
90
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
91
|
+
provider: "twilio"
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async sendBulk(messages) {
|
|
96
|
+
return Promise.all(messages.map((msg) => this.send(msg)));
|
|
97
|
+
}
|
|
98
|
+
async getStatus(messageId) {
|
|
99
|
+
const response = await fetch(`${API_BASE}/Accounts/${this.config.accountSid}/Messages/${messageId}.json`, {
|
|
100
|
+
headers: {
|
|
101
|
+
Authorization: `Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok)
|
|
105
|
+
throw Error(`Twilio message status API error: ${response.status}`);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
return {
|
|
108
|
+
messageId: data.sid,
|
|
109
|
+
to: data.to,
|
|
110
|
+
status: mapTwilioStatus(data.status),
|
|
111
|
+
timestamp: new Date(data.date_updated || data.date_created),
|
|
112
|
+
errorCode: data.error_code?.toString(),
|
|
113
|
+
errorMessage: data.error_message
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async verify(phoneNumber) {
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(`https://lookups.twilio.com/v2/PhoneNumbers/${encodeURIComponent(phoneNumber)}?Fields=line_type_intelligence`, {
|
|
119
|
+
headers: {
|
|
120
|
+
Authorization: `Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
return { valid: !1 };
|
|
125
|
+
const data = await response.json();
|
|
126
|
+
return {
|
|
127
|
+
valid: data.valid,
|
|
128
|
+
carrier: data.line_type_intelligence?.carrier_name,
|
|
129
|
+
type: data.line_type_intelligence?.type
|
|
130
|
+
};
|
|
131
|
+
} catch {
|
|
132
|
+
return { valid: !1 };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async getBalance() {
|
|
136
|
+
const response = await fetch(`${API_BASE}/Accounts/${this.config.accountSid}/Balance.json`, {
|
|
137
|
+
headers: {
|
|
138
|
+
Authorization: `Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok)
|
|
142
|
+
throw Error(`Twilio balance API error: ${response.status}`);
|
|
143
|
+
const data = await response.json();
|
|
144
|
+
return {
|
|
145
|
+
balance: Number.parseFloat(data.balance),
|
|
146
|
+
currency: data.currency
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async startVerification(request) {
|
|
150
|
+
if (!this.verifyServiceSid)
|
|
151
|
+
return {
|
|
152
|
+
success: !1,
|
|
153
|
+
status: "denied",
|
|
154
|
+
error: "Verify service SID not configured"
|
|
155
|
+
};
|
|
156
|
+
try {
|
|
157
|
+
const body = new URLSearchParams;
|
|
158
|
+
body.append("To", request.to);
|
|
159
|
+
body.append("Channel", request.channel || "sms");
|
|
160
|
+
if (request.locale)
|
|
161
|
+
body.append("Locale", request.locale);
|
|
162
|
+
if (request.customMessage)
|
|
163
|
+
body.append("CustomMessage", request.customMessage);
|
|
164
|
+
const response = await fetch(`${VERIFY_API_BASE}/Services/${this.verifyServiceSid}/Verifications`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: {
|
|
167
|
+
Authorization: `Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`,
|
|
168
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
169
|
+
},
|
|
170
|
+
body: body.toString()
|
|
171
|
+
}), data = await response.json();
|
|
172
|
+
if (!response.ok)
|
|
173
|
+
return {
|
|
174
|
+
success: !1,
|
|
175
|
+
status: "denied",
|
|
176
|
+
error: data.message || `HTTP ${response.status}`
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
success: !0,
|
|
180
|
+
verificationId: data.sid,
|
|
181
|
+
status: data.status === "pending" ? "pending" : "denied"
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
success: !1,
|
|
186
|
+
status: "denied",
|
|
187
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async checkVerification(request) {
|
|
192
|
+
if (!this.verifyServiceSid)
|
|
193
|
+
return {
|
|
194
|
+
success: !1,
|
|
195
|
+
status: "denied",
|
|
196
|
+
error: "Verify service SID not configured"
|
|
197
|
+
};
|
|
198
|
+
try {
|
|
199
|
+
const body = new URLSearchParams;
|
|
200
|
+
body.append("To", request.to);
|
|
201
|
+
body.append("Code", request.code);
|
|
202
|
+
const response = await fetch(`${VERIFY_API_BASE}/Services/${this.verifyServiceSid}/VerificationCheck`, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: {
|
|
205
|
+
Authorization: `Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`,
|
|
206
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
207
|
+
},
|
|
208
|
+
body: body.toString()
|
|
209
|
+
}), data = await response.json();
|
|
210
|
+
if (!response.ok)
|
|
211
|
+
return {
|
|
212
|
+
success: !1,
|
|
213
|
+
status: "denied",
|
|
214
|
+
error: data.message || `HTTP ${response.status}`
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
success: data.status === "approved",
|
|
218
|
+
verificationId: data.sid,
|
|
219
|
+
status: data.status === "approved" ? "approved" : "denied"
|
|
220
|
+
};
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return {
|
|
223
|
+
success: !1,
|
|
224
|
+
status: "denied",
|
|
225
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async cancelVerification(verificationId) {
|
|
230
|
+
if (!this.verifyServiceSid)
|
|
231
|
+
return !1;
|
|
232
|
+
try {
|
|
233
|
+
const body = new URLSearchParams;
|
|
234
|
+
body.append("Status", "canceled");
|
|
235
|
+
return (await fetch(`${VERIFY_API_BASE}/Services/${this.verifyServiceSid}/Verifications/${verificationId}`, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: {
|
|
238
|
+
Authorization: `Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`,
|
|
239
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
240
|
+
},
|
|
241
|
+
body: body.toString()
|
|
242
|
+
})).ok;
|
|
243
|
+
} catch {
|
|
244
|
+
return !1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function mapTwilioStatus(status) {
|
|
249
|
+
return {
|
|
250
|
+
queued: "queued",
|
|
251
|
+
sending: "sending",
|
|
252
|
+
sent: "sent",
|
|
253
|
+
delivered: "delivered",
|
|
254
|
+
undelivered: "undelivered",
|
|
255
|
+
failed: "failed",
|
|
256
|
+
canceled: "failed"
|
|
257
|
+
}[status] || "unknown";
|
|
258
|
+
}
|
|
259
|
+
export function createTwilioDriver(config, verifyServiceSid) {
|
|
260
|
+
return new TwilioDriver(config, verifyServiceSid);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export { TwilioDriver as default };
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
const API_BASE = "https://rest.nexmo.com", MESSAGES_API_BASE = "https://api.nexmo.com/v1/messages", _VERIFY_API_BASE = "https://api.nexmo.com/verify", VERIFY_V2_API_BASE = "https://api.nexmo.com/v2/verify";
|
|
2
|
+
|
|
3
|
+
export class VonageDriver {
|
|
4
|
+
config;
|
|
5
|
+
useMessagesApi;
|
|
6
|
+
constructor(config, useMessagesApi = !1) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.useMessagesApi = useMessagesApi;
|
|
9
|
+
}
|
|
10
|
+
async send(message) {
|
|
11
|
+
const recipients = Array.isArray(message.to) ? message.to : [message.to];
|
|
12
|
+
if (recipients.length > 1)
|
|
13
|
+
return (await this.sendBulk(recipients.map((to) => ({ ...message, to }))))[0] ?? {
|
|
14
|
+
success: !1,
|
|
15
|
+
to: recipients[0] ?? "",
|
|
16
|
+
error: "No recipients supplied",
|
|
17
|
+
provider: "vonage"
|
|
18
|
+
};
|
|
19
|
+
const to = recipients[0];
|
|
20
|
+
if (!to)
|
|
21
|
+
return {
|
|
22
|
+
success: !1,
|
|
23
|
+
to: "",
|
|
24
|
+
error: "No recipient supplied",
|
|
25
|
+
provider: "vonage"
|
|
26
|
+
};
|
|
27
|
+
const from = message.from || this.config.from;
|
|
28
|
+
if (!from)
|
|
29
|
+
return {
|
|
30
|
+
success: !1,
|
|
31
|
+
to,
|
|
32
|
+
error: 'No "from" number configured',
|
|
33
|
+
provider: "vonage"
|
|
34
|
+
};
|
|
35
|
+
if (!/^\+?[1-9]\d{1,14}$/.test(to))
|
|
36
|
+
return {
|
|
37
|
+
success: !1,
|
|
38
|
+
to,
|
|
39
|
+
error: `Invalid E.164 phone number: ${to}. Expected +<country><number> with 8\u201315 digits.`,
|
|
40
|
+
provider: "vonage"
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
if (this.useMessagesApi)
|
|
44
|
+
return await this.sendWithMessagesApi(to, from, message);
|
|
45
|
+
return await this.sendWithSmsApi(to, from, message);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return {
|
|
48
|
+
success: !1,
|
|
49
|
+
to,
|
|
50
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
51
|
+
provider: "vonage"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async sendWithSmsApi(to, from, message) {
|
|
56
|
+
const body = new URLSearchParams;
|
|
57
|
+
body.append("api_key", this.config.apiKey);
|
|
58
|
+
body.append("api_secret", this.config.apiSecret);
|
|
59
|
+
body.append("to", to);
|
|
60
|
+
body.append("from", from);
|
|
61
|
+
body.append("text", message.body);
|
|
62
|
+
if (message.statusCallback)
|
|
63
|
+
body.append("callback", message.statusCallback);
|
|
64
|
+
const response = await fetch(`${API_BASE}/sms/json`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
68
|
+
},
|
|
69
|
+
body: body.toString()
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok)
|
|
72
|
+
throw Error(`Vonage SMS API error: ${response.status}`);
|
|
73
|
+
const data = await response.json(), [msg] = data.messages ?? [];
|
|
74
|
+
if (msg) {
|
|
75
|
+
if (msg.status !== "0")
|
|
76
|
+
return {
|
|
77
|
+
success: !1,
|
|
78
|
+
to,
|
|
79
|
+
error: msg["error-text"] || `Status: ${msg.status}`,
|
|
80
|
+
provider: "vonage"
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
success: !0,
|
|
84
|
+
messageId: msg["message-id"],
|
|
85
|
+
status: "sent",
|
|
86
|
+
to,
|
|
87
|
+
provider: "vonage",
|
|
88
|
+
price: msg["message-price"] ? Number.parseFloat(msg["message-price"]) : void 0,
|
|
89
|
+
currency: "EUR"
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
success: !1,
|
|
94
|
+
to,
|
|
95
|
+
error: "No response from Vonage",
|
|
96
|
+
provider: "vonage"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async sendWithMessagesApi(to, from, message) {
|
|
100
|
+
const headers = {
|
|
101
|
+
"Content-Type": "application/json"
|
|
102
|
+
};
|
|
103
|
+
let authHeader;
|
|
104
|
+
if (this.config.applicationId && this.config.privateKey)
|
|
105
|
+
authHeader = `Bearer ${await this.generateJwt()}`;
|
|
106
|
+
else
|
|
107
|
+
authHeader = `Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;
|
|
108
|
+
headers.Authorization = authHeader;
|
|
109
|
+
const body = {
|
|
110
|
+
message_type: "text",
|
|
111
|
+
channel: "sms",
|
|
112
|
+
to,
|
|
113
|
+
from,
|
|
114
|
+
text: message.body
|
|
115
|
+
};
|
|
116
|
+
if (message.statusCallback)
|
|
117
|
+
body.webhook_url = message.statusCallback;
|
|
118
|
+
const response = await fetch(MESSAGES_API_BASE, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers,
|
|
121
|
+
body: JSON.stringify(body)
|
|
122
|
+
}), data = await response.json();
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
return {
|
|
125
|
+
success: !1,
|
|
126
|
+
to,
|
|
127
|
+
error: data.title || data.detail || `HTTP ${response.status}`,
|
|
128
|
+
provider: "vonage"
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
success: !0,
|
|
132
|
+
messageId: data.message_uuid,
|
|
133
|
+
status: "sent",
|
|
134
|
+
to,
|
|
135
|
+
provider: "vonage"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async sendBulk(messages) {
|
|
139
|
+
return Promise.all(messages.map((msg) => this.send(msg)));
|
|
140
|
+
}
|
|
141
|
+
async getStatus(messageId) {
|
|
142
|
+
const headers = {};
|
|
143
|
+
if (this.config.applicationId && this.config.privateKey) {
|
|
144
|
+
const jwt = await this.generateJwt();
|
|
145
|
+
headers.Authorization = `Bearer ${jwt}`;
|
|
146
|
+
} else
|
|
147
|
+
headers.Authorization = `Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;
|
|
148
|
+
const response = await fetch(`${MESSAGES_API_BASE}/${messageId}`, {
|
|
149
|
+
headers
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok)
|
|
152
|
+
throw Error(`Vonage message status API error: ${response.status}`);
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
return {
|
|
155
|
+
messageId: data.message_uuid,
|
|
156
|
+
to: data.to,
|
|
157
|
+
status: mapVonageStatus(data.status),
|
|
158
|
+
timestamp: new Date(data.timestamp),
|
|
159
|
+
errorCode: data.error?.code?.toString(),
|
|
160
|
+
errorMessage: data.error?.reason
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async verify(phoneNumber) {
|
|
164
|
+
try {
|
|
165
|
+
const body = new URLSearchParams;
|
|
166
|
+
body.append("api_key", this.config.apiKey);
|
|
167
|
+
body.append("api_secret", this.config.apiSecret);
|
|
168
|
+
body.append("number", phoneNumber);
|
|
169
|
+
const response = await fetch(`${API_BASE}/ni/basic/json`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: {
|
|
172
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
173
|
+
},
|
|
174
|
+
body: body.toString()
|
|
175
|
+
});
|
|
176
|
+
if (!response.ok)
|
|
177
|
+
throw Error(`Vonage number insight API error: ${response.status}`);
|
|
178
|
+
const data = await response.json();
|
|
179
|
+
if (data.status !== 0)
|
|
180
|
+
return { valid: !1 };
|
|
181
|
+
return {
|
|
182
|
+
valid: !0,
|
|
183
|
+
carrier: data.current_carrier?.name,
|
|
184
|
+
type: data.current_carrier?.network_type
|
|
185
|
+
};
|
|
186
|
+
} catch {
|
|
187
|
+
return { valid: !1 };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async getBalance() {
|
|
191
|
+
const auth = typeof Buffer < "u" ? Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString("base64") : btoa(`${this.config.apiKey}:${this.config.apiSecret}`), response = await fetch(`${API_BASE}/account/get-balance`, {
|
|
192
|
+
headers: { Authorization: `Basic ${auth}` }
|
|
193
|
+
});
|
|
194
|
+
if (!response.ok)
|
|
195
|
+
throw Error(`Vonage balance API error: ${response.status}`);
|
|
196
|
+
return {
|
|
197
|
+
balance: (await response.json()).value,
|
|
198
|
+
currency: "EUR"
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async startVerification(request) {
|
|
202
|
+
try {
|
|
203
|
+
const headers = {
|
|
204
|
+
"Content-Type": "application/json"
|
|
205
|
+
};
|
|
206
|
+
if (this.config.applicationId && this.config.privateKey) {
|
|
207
|
+
const jwt = await this.generateJwt();
|
|
208
|
+
headers.Authorization = `Bearer ${jwt}`;
|
|
209
|
+
} else
|
|
210
|
+
headers.Authorization = `Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;
|
|
211
|
+
const body = {
|
|
212
|
+
brand: this.config.from || "Verification",
|
|
213
|
+
workflow: [
|
|
214
|
+
{
|
|
215
|
+
channel: request.channel === "whatsapp" ? "whatsapp_interactive" : "sms",
|
|
216
|
+
to: request.to
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
};
|
|
220
|
+
if (request.codeLength)
|
|
221
|
+
body.code_length = request.codeLength;
|
|
222
|
+
if (request.locale)
|
|
223
|
+
body.locale = request.locale;
|
|
224
|
+
const response = await fetch(VERIFY_V2_API_BASE, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers,
|
|
227
|
+
body: JSON.stringify(body)
|
|
228
|
+
}), data = await response.json();
|
|
229
|
+
if (!response.ok)
|
|
230
|
+
return {
|
|
231
|
+
success: !1,
|
|
232
|
+
status: "denied",
|
|
233
|
+
error: data.title || data.detail || `HTTP ${response.status}`
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
success: !0,
|
|
237
|
+
verificationId: data.request_id,
|
|
238
|
+
status: "pending"
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return {
|
|
242
|
+
success: !1,
|
|
243
|
+
status: "denied",
|
|
244
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async checkVerification(request) {
|
|
249
|
+
if (!request.verificationId)
|
|
250
|
+
return {
|
|
251
|
+
success: !1,
|
|
252
|
+
status: "denied",
|
|
253
|
+
error: "Verification ID is required"
|
|
254
|
+
};
|
|
255
|
+
try {
|
|
256
|
+
const headers = {
|
|
257
|
+
"Content-Type": "application/json"
|
|
258
|
+
};
|
|
259
|
+
if (this.config.applicationId && this.config.privateKey) {
|
|
260
|
+
const jwt = await this.generateJwt();
|
|
261
|
+
headers.Authorization = `Bearer ${jwt}`;
|
|
262
|
+
} else
|
|
263
|
+
headers.Authorization = `Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;
|
|
264
|
+
const response = await fetch(`${VERIFY_V2_API_BASE}/${request.verificationId}`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers,
|
|
267
|
+
body: JSON.stringify({ code: request.code })
|
|
268
|
+
}), data = await response.json();
|
|
269
|
+
if (!response.ok)
|
|
270
|
+
return {
|
|
271
|
+
success: !1,
|
|
272
|
+
status: "denied",
|
|
273
|
+
error: data.title || data.detail || `HTTP ${response.status}`
|
|
274
|
+
};
|
|
275
|
+
return {
|
|
276
|
+
success: data.status === "completed",
|
|
277
|
+
verificationId: request.verificationId,
|
|
278
|
+
status: data.status === "completed" ? "approved" : "denied"
|
|
279
|
+
};
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return {
|
|
282
|
+
success: !1,
|
|
283
|
+
status: "denied",
|
|
284
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async cancelVerification(verificationId) {
|
|
289
|
+
try {
|
|
290
|
+
const headers = {};
|
|
291
|
+
if (this.config.applicationId && this.config.privateKey) {
|
|
292
|
+
const jwt = await this.generateJwt();
|
|
293
|
+
headers.Authorization = `Bearer ${jwt}`;
|
|
294
|
+
} else
|
|
295
|
+
headers.Authorization = `Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;
|
|
296
|
+
return (await fetch(`${VERIFY_V2_API_BASE}/${verificationId}`, {
|
|
297
|
+
method: "DELETE",
|
|
298
|
+
headers
|
|
299
|
+
})).ok;
|
|
300
|
+
} catch {
|
|
301
|
+
return !1;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async generateJwt() {
|
|
305
|
+
if (!this.config.applicationId || !this.config.privateKey)
|
|
306
|
+
throw Error("Application ID and private key required for JWT auth");
|
|
307
|
+
const header = {
|
|
308
|
+
typ: "JWT",
|
|
309
|
+
alg: "RS256"
|
|
310
|
+
}, now = Math.floor(Date.now() / 1000), payload = {
|
|
311
|
+
application_id: this.config.applicationId,
|
|
312
|
+
iat: now,
|
|
313
|
+
exp: now + 900,
|
|
314
|
+
jti: crypto.randomUUID()
|
|
315
|
+
}, encodedHeader = btoa(JSON.stringify(header)), encodedPayload = btoa(JSON.stringify(payload));
|
|
316
|
+
return `${encodedHeader}.${encodedPayload}.signature`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function mapVonageStatus(status) {
|
|
320
|
+
return {
|
|
321
|
+
submitted: "queued",
|
|
322
|
+
delivered: "delivered",
|
|
323
|
+
expired: "undelivered",
|
|
324
|
+
failed: "failed",
|
|
325
|
+
rejected: "failed",
|
|
326
|
+
accepted: "sent"
|
|
327
|
+
}[status] || "unknown";
|
|
328
|
+
}
|
|
329
|
+
export function createVonageDriver(config, useMessagesApi = !1) {
|
|
330
|
+
return new VonageDriver(config, useMessagesApi);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export { VonageDriver as default };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
var I=Object.defineProperty;var V=(x)=>x;function w(x,M){this[x]=V.bind(null,M)}var k=(x,M)=>{for(var G in M)I(x,G,{get:M[G],enumerable:!0,configurable:!0,set:w.bind(M,G)})};var N=(x,M)=>()=>(x&&(M=x(x=0)),M);var U={};k(U,{default:()=>C});import{env as Z}from"@stacksjs/env";var C;var _=N(()=>{C={enabled:!1,provider:"twilio",from:String(Z.SMS_FROM_NUMBER||""),defaultCountryCode:"US",messageType:"TRANSACTIONAL",drivers:{twilio:{accountSid:String(Z.TWILIO_ACCOUNT_SID||""),authToken:String(Z.TWILIO_AUTH_TOKEN||""),from:String(Z.TWILIO_FROM_NUMBER||""),messagingServiceSid:String(Z.TWILIO_MESSAGING_SERVICE_SID||"")},vonage:{apiKey:String(Z.VONAGE_API_KEY||""),apiSecret:String(Z.VONAGE_API_SECRET||""),from:String(Z.VONAGE_FROM_NUMBER||"")},pinpoint:{region:String(Z.AWS_REGION||"us-east-1"),accessKeyId:String(Z.AWS_ACCESS_KEY_ID||""),secretAccessKey:String(Z.AWS_SECRET_ACCESS_KEY||""),senderId:String(Z.SMS_SENDER_ID||""),originationNumber:String(Z.SMS_ORIGINATION_NUMBER||"")}},maxSpendPerMonth:100,optOut:{enabled:!0,keywords:["STOP","UNSUBSCRIBE","CANCEL","END","QUIT"]},templates:[],twoWay:{enabled:!1}}});async function h(x,M,G=3){let J=0;while(!0){let L=await fetch(x,M);if(L.status!==429&&L.status!==503)return L;if(J>=G)return L;let X=Number(L.headers.get("retry-after"))||2**J;await new Promise((Q)=>setTimeout(Q,Math.min(X,30)*1000)),J++}}class E{config;verifyServiceSid;constructor(x,M){this.config=x,this.verifyServiceSid=M}async send(x){let M=Array.isArray(x.to)?x.to:[x.to];if(M.length>1)return(await this.sendBulk(M.map((X)=>({...x,to:X}))))[0]??{success:!1,to:M[0]??"",error:"No recipients supplied",provider:"twilio"};let G=M[0];if(!G)return{success:!1,to:"",error:"No recipient supplied",provider:"twilio"};let J=x.from||this.config.from;if(!J&&!this.config.messagingServiceSid)return{success:!1,to:G,error:'No "from" number or messaging service SID configured',provider:"twilio"};try{let L=new URLSearchParams;if(L.append("To",G),L.append("Body",x.body),this.config.messagingServiceSid)L.append("MessagingServiceSid",this.config.messagingServiceSid);else if(J)L.append("From",J);if(x.statusCallback||this.config.statusCallback)L.append("StatusCallback",x.statusCallback||this.config.statusCallback);if(x.mediaUrls&&x.mediaUrls.length>0)for(let z of x.mediaUrls)L.append("MediaUrl",z);let X=await h(`https://api.twilio.com/2010-04-01/Accounts/${this.config.accountSid}/Messages.json`,{method:"POST",headers:{Authorization:`Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`,"Content-Type":"application/x-www-form-urlencoded"},body:L.toString()}),Q=await X.json();if(!X.ok)return{success:!1,to:G,error:Q.message||`HTTP ${X.status}`,provider:"twilio"};return{success:!0,messageId:Q.sid,status:B(Q.status),to:G,provider:"twilio",segments:Q.num_segments?Number.parseInt(Q.num_segments,10):void 0,price:Q.price?Math.abs(Number.parseFloat(Q.price)):void 0,currency:Q.price_unit}}catch(L){return{success:!1,to:G,error:L instanceof Error?L.message:"Unknown error",provider:"twilio"}}}async sendBulk(x){return Promise.all(x.map((M)=>this.send(M)))}async getStatus(x){let M=await fetch(`https://api.twilio.com/2010-04-01/Accounts/${this.config.accountSid}/Messages/${x}.json`,{headers:{Authorization:`Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`}});if(!M.ok)throw Error(`Twilio message status API error: ${M.status}`);let G=await M.json();return{messageId:G.sid,to:G.to,status:B(G.status),timestamp:new Date(G.date_updated||G.date_created),errorCode:G.error_code?.toString(),errorMessage:G.error_message}}async verify(x){try{let M=await fetch(`https://lookups.twilio.com/v2/PhoneNumbers/${encodeURIComponent(x)}?Fields=line_type_intelligence`,{headers:{Authorization:`Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`}});if(!M.ok)return{valid:!1};let G=await M.json();return{valid:G.valid,carrier:G.line_type_intelligence?.carrier_name,type:G.line_type_intelligence?.type}}catch{return{valid:!1}}}async getBalance(){let x=await fetch(`https://api.twilio.com/2010-04-01/Accounts/${this.config.accountSid}/Balance.json`,{headers:{Authorization:`Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`}});if(!x.ok)throw Error(`Twilio balance API error: ${x.status}`);let M=await x.json();return{balance:Number.parseFloat(M.balance),currency:M.currency}}async startVerification(x){if(!this.verifyServiceSid)return{success:!1,status:"denied",error:"Verify service SID not configured"};try{let M=new URLSearchParams;if(M.append("To",x.to),M.append("Channel",x.channel||"sms"),x.locale)M.append("Locale",x.locale);if(x.customMessage)M.append("CustomMessage",x.customMessage);let G=await fetch(`https://verify.twilio.com/v2/Services/${this.verifyServiceSid}/Verifications`,{method:"POST",headers:{Authorization:`Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`,"Content-Type":"application/x-www-form-urlencoded"},body:M.toString()}),J=await G.json();if(!G.ok)return{success:!1,status:"denied",error:J.message||`HTTP ${G.status}`};return{success:!0,verificationId:J.sid,status:J.status==="pending"?"pending":"denied"}}catch(M){return{success:!1,status:"denied",error:M instanceof Error?M.message:"Unknown error"}}}async checkVerification(x){if(!this.verifyServiceSid)return{success:!1,status:"denied",error:"Verify service SID not configured"};try{let M=new URLSearchParams;M.append("To",x.to),M.append("Code",x.code);let G=await fetch(`https://verify.twilio.com/v2/Services/${this.verifyServiceSid}/VerificationCheck`,{method:"POST",headers:{Authorization:`Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`,"Content-Type":"application/x-www-form-urlencoded"},body:M.toString()}),J=await G.json();if(!G.ok)return{success:!1,status:"denied",error:J.message||`HTTP ${G.status}`};return{success:J.status==="approved",verificationId:J.sid,status:J.status==="approved"?"approved":"denied"}}catch(M){return{success:!1,status:"denied",error:M instanceof Error?M.message:"Unknown error"}}}async cancelVerification(x){if(!this.verifyServiceSid)return!1;try{let M=new URLSearchParams;return M.append("Status","canceled"),(await fetch(`https://verify.twilio.com/v2/Services/${this.verifyServiceSid}/Verifications/${x}`,{method:"POST",headers:{Authorization:`Basic ${btoa(`${this.config.accountSid}:${this.config.authToken}`)}`,"Content-Type":"application/x-www-form-urlencoded"},body:M.toString()})).ok}catch{return!1}}}function B(x){return{queued:"queued",sending:"sending",sent:"sent",delivered:"delivered",undelivered:"undelivered",failed:"failed",canceled:"failed"}[x]||"unknown"}var P={};k(P,{default:()=>K,createVonageDriver:()=>q,VonageDriver:()=>K});class K{config;useMessagesApi;constructor(x,M=!1){this.config=x,this.useMessagesApi=M}async send(x){let M=Array.isArray(x.to)?x.to:[x.to];if(M.length>1)return(await this.sendBulk(M.map((X)=>({...x,to:X}))))[0]??{success:!1,to:M[0]??"",error:"No recipients supplied",provider:"vonage"};let G=M[0];if(!G)return{success:!1,to:"",error:"No recipient supplied",provider:"vonage"};let J=x.from||this.config.from;if(!J)return{success:!1,to:G,error:'No "from" number configured',provider:"vonage"};if(!/^\+?[1-9]\d{1,14}$/.test(G))return{success:!1,to:G,error:`Invalid E.164 phone number: ${G}. Expected +<country><number> with 8\u201315 digits.`,provider:"vonage"};try{if(this.useMessagesApi)return await this.sendWithMessagesApi(G,J,x);return await this.sendWithSmsApi(G,J,x)}catch(L){return{success:!1,to:G,error:L instanceof Error?L.message:"Unknown error",provider:"vonage"}}}async sendWithSmsApi(x,M,G){let J=new URLSearchParams;if(J.append("api_key",this.config.apiKey),J.append("api_secret",this.config.apiSecret),J.append("to",x),J.append("from",M),J.append("text",G.body),G.statusCallback)J.append("callback",G.statusCallback);let L=await fetch("https://rest.nexmo.com/sms/json",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:J.toString()});if(!L.ok)throw Error(`Vonage SMS API error: ${L.status}`);let X=await L.json(),[Q]=X.messages??[];if(Q){if(Q.status!=="0")return{success:!1,to:x,error:Q["error-text"]||`Status: ${Q.status}`,provider:"vonage"};return{success:!0,messageId:Q["message-id"],status:"sent",to:x,provider:"vonage",price:Q["message-price"]?Number.parseFloat(Q["message-price"]):void 0,currency:"EUR"}}return{success:!1,to:x,error:"No response from Vonage",provider:"vonage"}}async sendWithMessagesApi(x,M,G){let J={"Content-Type":"application/json"},L;if(this.config.applicationId&&this.config.privateKey)L=`Bearer ${await this.generateJwt()}`;else L=`Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;J.Authorization=L;let X={message_type:"text",channel:"sms",to:x,from:M,text:G.body};if(G.statusCallback)X.webhook_url=G.statusCallback;let Q=await fetch("https://api.nexmo.com/v1/messages",{method:"POST",headers:J,body:JSON.stringify(X)}),z=await Q.json();if(!Q.ok)return{success:!1,to:x,error:z.title||z.detail||`HTTP ${Q.status}`,provider:"vonage"};return{success:!0,messageId:z.message_uuid,status:"sent",to:x,provider:"vonage"}}async sendBulk(x){return Promise.all(x.map((M)=>this.send(M)))}async getStatus(x){let M={};if(this.config.applicationId&&this.config.privateKey){let L=await this.generateJwt();M.Authorization=`Bearer ${L}`}else M.Authorization=`Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;let G=await fetch(`https://api.nexmo.com/v1/messages/${x}`,{headers:M});if(!G.ok)throw Error(`Vonage message status API error: ${G.status}`);let J=await G.json();return{messageId:J.message_uuid,to:J.to,status:D(J.status),timestamp:new Date(J.timestamp),errorCode:J.error?.code?.toString(),errorMessage:J.error?.reason}}async verify(x){try{let M=new URLSearchParams;M.append("api_key",this.config.apiKey),M.append("api_secret",this.config.apiSecret),M.append("number",x);let G=await fetch("https://rest.nexmo.com/ni/basic/json",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:M.toString()});if(!G.ok)throw Error(`Vonage number insight API error: ${G.status}`);let J=await G.json();if(J.status!==0)return{valid:!1};return{valid:!0,carrier:J.current_carrier?.name,type:J.current_carrier?.network_type}}catch{return{valid:!1}}}async getBalance(){let x=typeof Buffer<"u"?Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString("base64"):btoa(`${this.config.apiKey}:${this.config.apiSecret}`),M=await fetch("https://rest.nexmo.com/account/get-balance",{headers:{Authorization:`Basic ${x}`}});if(!M.ok)throw Error(`Vonage balance API error: ${M.status}`);return{balance:(await M.json()).value,currency:"EUR"}}async startVerification(x){try{let M={"Content-Type":"application/json"};if(this.config.applicationId&&this.config.privateKey){let X=await this.generateJwt();M.Authorization=`Bearer ${X}`}else M.Authorization=`Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;let G={brand:this.config.from||"Verification",workflow:[{channel:x.channel==="whatsapp"?"whatsapp_interactive":"sms",to:x.to}]};if(x.codeLength)G.code_length=x.codeLength;if(x.locale)G.locale=x.locale;let J=await fetch("https://api.nexmo.com/v2/verify",{method:"POST",headers:M,body:JSON.stringify(G)}),L=await J.json();if(!J.ok)return{success:!1,status:"denied",error:L.title||L.detail||`HTTP ${J.status}`};return{success:!0,verificationId:L.request_id,status:"pending"}}catch(M){return{success:!1,status:"denied",error:M instanceof Error?M.message:"Unknown error"}}}async checkVerification(x){if(!x.verificationId)return{success:!1,status:"denied",error:"Verification ID is required"};try{let M={"Content-Type":"application/json"};if(this.config.applicationId&&this.config.privateKey){let L=await this.generateJwt();M.Authorization=`Bearer ${L}`}else M.Authorization=`Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;let G=await fetch(`https://api.nexmo.com/v2/verify/${x.verificationId}`,{method:"POST",headers:M,body:JSON.stringify({code:x.code})}),J=await G.json();if(!G.ok)return{success:!1,status:"denied",error:J.title||J.detail||`HTTP ${G.status}`};return{success:J.status==="completed",verificationId:x.verificationId,status:J.status==="completed"?"approved":"denied"}}catch(M){return{success:!1,status:"denied",error:M instanceof Error?M.message:"Unknown error"}}}async cancelVerification(x){try{let M={};if(this.config.applicationId&&this.config.privateKey){let J=await this.generateJwt();M.Authorization=`Bearer ${J}`}else M.Authorization=`Basic ${btoa(`${this.config.apiKey}:${this.config.apiSecret}`)}`;return(await fetch(`https://api.nexmo.com/v2/verify/${x}`,{method:"DELETE",headers:M})).ok}catch{return!1}}async generateJwt(){if(!this.config.applicationId||!this.config.privateKey)throw Error("Application ID and private key required for JWT auth");let x={typ:"JWT",alg:"RS256"},M=Math.floor(Date.now()/1000),G={application_id:this.config.applicationId,iat:M,exp:M+900,jti:crypto.randomUUID()},J=btoa(JSON.stringify(x)),L=btoa(JSON.stringify(G));return`${J}.${L}.signature`}}function D(x){return{submitted:"queued",delivered:"delivered",expired:"undelivered",failed:"failed",rejected:"failed",accepted:"sent"}[x]||"unknown"}function q(x,M=!1){return new K(x,M)}var O=null,W=null,Y={},H=null;async function A(){try{Y=(await Promise.resolve().then(() => (_(),U))).default}catch{}}async function $(){if(!H)H=A();await H}function m(x){Y={...Y,...x},O=null,W=null}function R(x){let M=x||Y.provider||"twilio";switch(M){case"twilio":{let G=Y.drivers?.twilio;if(!G?.accountSid||!G?.authToken)throw Error("Twilio configuration is incomplete. Please provide accountSid and authToken.");return new E({...G,from:G.from||Y.from})}case"vonage":{let G=Y.drivers?.vonage;if(!G?.apiKey||!G?.apiSecret)throw Error("Vonage configuration is incomplete. Please provide apiKey and apiSecret.");return new K({...G,from:G.from||Y.from})}default:throw Error(`Unsupported SMS provider: ${M}`)}}function y(x){let M=R(x);if(!("startVerification"in M))throw Error(`Provider does not support verification: ${x||Y.provider}`);return M}function F(){if(!O)O=R();return O}function j(){if(!W)W=y();return W}async function T(x){return await $(),F().send(x)}var g=T;async function d(x){return await $(),F().sendBulk(x)}async function n(x){await $();let M=F();if(M.getStatus)return M.getStatus(x);return null}async function i(x){await $();let M=F();if(M.verify)return M.verify(x);return{valid:!0}}async function o(){await $();let x=F();if(x.getBalance)return x.getBalance();return null}async function t(x){return await $(),j().startVerification(x)}async function a(x){return await $(),j().checkVerification(x)}async function r(x){await $();let M=j();if(M.cancelVerification)return M.cancelVerification(x);return!1}class S{message={};provider;to(x){return this.message.to=x,this}body(x){return this.message.body=x,this}text(x){return this.body(x)}from(x){return this.message.from=x,this}media(x){return this.message.mediaUrls=Array.isArray(x)?x:[x],this}callback(x){return this.message.statusCallback=x,this}via(x){return this.provider=x,this}async send(){if(await $(),!this.message.to)return{success:!1,to:"",error:"Recipient is required",provider:this.provider||Y.provider||"twilio"};if(!this.message.body)return{success:!1,to:Array.isArray(this.message.to)?this.message.to[0]??"":this.message.to,error:"Message body is required",provider:this.provider||Y.provider||"twilio"};return(this.provider?R(this.provider):F()).send(this.message)}}function s(){return new S}async function e(x,M,G={}){await $();let J=Y.templates?.find((X)=>X.name===M);if(!J)return{success:!1,to:Array.isArray(x)?x[0]??"":x,error:`Template not found: ${M}`,provider:Y.provider||"twilio"};let L=J.body;for(let[X,Q]of Object.entries(G)){let z=X.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");L=L.replace(new RegExp(`\\{${z}\\}`,"g"),Q)}return T({to:x,body:L})}function b(x,M){let G=x.replace(/[\s\-()]/g,"");if(G.startsWith("+"))return G;let J=M||Y.defaultCountryCode||"1";if(G.startsWith("00"))G=`+${G.slice(2)}`;else if(!G.startsWith("+"))G=`+${J}${G}`;return G}function xx(x){let M=/^\+[1-9]\d{6,14}$/,G=b(x);return M.test(G)}async function Mx(){await A()}function Gx(){return Y.enabled===!0}function Jx(){return{...Y}}export{i as verifyNumber,t as startVerification,s as sms,e as sendTemplate,g as sendSms,d as sendBulk,T as send,P as nexmo,xx as isValidPhoneNumber,Gx as isEnabled,Mx as init,y as getVerificationDriver,n as getStatus,R as getDriver,Jx as getConfig,o as getBalance,b as formatE164,Hx as createVonageDriver,Ex as createTwilioDriver,m as configure,a as checkVerification,r as cancelVerification,_x as VonageDriver,Wx as TwilioDriver,S as SmsBuilder,Fx as SMS};
|
|
1
|
+
export * from "./sms";
|
|
2
|
+
export { default as SMS } from "./sms";
|
|
3
|
+
export * from "./drivers";
|
|
4
|
+
export { TwilioDriver, createTwilioDriver } from "./drivers/twilio";
|
|
5
|
+
export { VonageDriver, createVonageDriver } from "./drivers/vonage";
|
package/dist/sms.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { TwilioDriver } from "./drivers/twilio";
|
|
2
|
+
import { VonageDriver } from "./drivers/vonage";
|
|
3
|
+
let defaultDriver = null, verificationDriver = null, smsConfig = {}, _configPromise = null;
|
|
4
|
+
async function loadConfig() {
|
|
5
|
+
try {
|
|
6
|
+
smsConfig = (await import("../../../../../config/sms")).default;
|
|
7
|
+
} catch {}
|
|
8
|
+
}
|
|
9
|
+
async function ensureConfig() {
|
|
10
|
+
if (!_configPromise)
|
|
11
|
+
_configPromise = loadConfig();
|
|
12
|
+
await _configPromise;
|
|
13
|
+
}
|
|
14
|
+
export function configure(config) {
|
|
15
|
+
smsConfig = { ...smsConfig, ...config };
|
|
16
|
+
defaultDriver = null;
|
|
17
|
+
verificationDriver = null;
|
|
18
|
+
}
|
|
19
|
+
export function getDriver(provider) {
|
|
20
|
+
const targetProvider = provider || smsConfig.provider || "twilio";
|
|
21
|
+
switch (targetProvider) {
|
|
22
|
+
case "twilio": {
|
|
23
|
+
const twilioConfig = smsConfig.drivers?.twilio;
|
|
24
|
+
if (!twilioConfig?.accountSid || !twilioConfig?.authToken)
|
|
25
|
+
throw Error("Twilio configuration is incomplete. Please provide accountSid and authToken.");
|
|
26
|
+
return new TwilioDriver({
|
|
27
|
+
...twilioConfig,
|
|
28
|
+
from: twilioConfig.from || smsConfig.from
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
case "vonage": {
|
|
32
|
+
const vonageConfig = smsConfig.drivers?.vonage;
|
|
33
|
+
if (!vonageConfig?.apiKey || !vonageConfig?.apiSecret)
|
|
34
|
+
throw Error("Vonage configuration is incomplete. Please provide apiKey and apiSecret.");
|
|
35
|
+
return new VonageDriver({
|
|
36
|
+
...vonageConfig,
|
|
37
|
+
from: vonageConfig.from || smsConfig.from
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
throw Error(`Unsupported SMS provider: ${targetProvider}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function getVerificationDriver(provider) {
|
|
45
|
+
const driver = getDriver(provider);
|
|
46
|
+
if (!("startVerification" in driver))
|
|
47
|
+
throw Error(`Provider does not support verification: ${provider || smsConfig.provider}`);
|
|
48
|
+
return driver;
|
|
49
|
+
}
|
|
50
|
+
function getDefaultDriver() {
|
|
51
|
+
if (!defaultDriver)
|
|
52
|
+
defaultDriver = getDriver();
|
|
53
|
+
return defaultDriver;
|
|
54
|
+
}
|
|
55
|
+
function getDefaultVerificationDriver() {
|
|
56
|
+
if (!verificationDriver)
|
|
57
|
+
verificationDriver = getVerificationDriver();
|
|
58
|
+
return verificationDriver;
|
|
59
|
+
}
|
|
60
|
+
export async function send(message) {
|
|
61
|
+
await ensureConfig();
|
|
62
|
+
return getDefaultDriver().send(message);
|
|
63
|
+
}
|
|
64
|
+
export const sendSms = send;
|
|
65
|
+
export async function sendBulk(messages) {
|
|
66
|
+
await ensureConfig();
|
|
67
|
+
return getDefaultDriver().sendBulk(messages);
|
|
68
|
+
}
|
|
69
|
+
export async function getStatus(messageId) {
|
|
70
|
+
await ensureConfig();
|
|
71
|
+
const driver = getDefaultDriver();
|
|
72
|
+
if (driver.getStatus)
|
|
73
|
+
return driver.getStatus(messageId);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
export async function verifyNumber(phoneNumber) {
|
|
77
|
+
await ensureConfig();
|
|
78
|
+
const driver = getDefaultDriver();
|
|
79
|
+
if (driver.verify)
|
|
80
|
+
return driver.verify(phoneNumber);
|
|
81
|
+
return { valid: !0 };
|
|
82
|
+
}
|
|
83
|
+
export async function getBalance() {
|
|
84
|
+
await ensureConfig();
|
|
85
|
+
const driver = getDefaultDriver();
|
|
86
|
+
if (driver.getBalance)
|
|
87
|
+
return driver.getBalance();
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
export async function startVerification(request) {
|
|
91
|
+
await ensureConfig();
|
|
92
|
+
return getDefaultVerificationDriver().startVerification(request);
|
|
93
|
+
}
|
|
94
|
+
export async function checkVerification(request) {
|
|
95
|
+
await ensureConfig();
|
|
96
|
+
return getDefaultVerificationDriver().checkVerification(request);
|
|
97
|
+
}
|
|
98
|
+
export async function cancelVerification(verificationId) {
|
|
99
|
+
await ensureConfig();
|
|
100
|
+
const driver = getDefaultVerificationDriver();
|
|
101
|
+
if (driver.cancelVerification)
|
|
102
|
+
return driver.cancelVerification(verificationId);
|
|
103
|
+
return !1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class SmsBuilder {
|
|
107
|
+
message = {};
|
|
108
|
+
provider;
|
|
109
|
+
to(phoneNumber) {
|
|
110
|
+
this.message.to = phoneNumber;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
body(text) {
|
|
114
|
+
this.message.body = text;
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
text(text) {
|
|
118
|
+
return this.body(text);
|
|
119
|
+
}
|
|
120
|
+
from(sender) {
|
|
121
|
+
this.message.from = sender;
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
media(urls) {
|
|
125
|
+
this.message.mediaUrls = Array.isArray(urls) ? urls : [urls];
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
callback(url) {
|
|
129
|
+
this.message.statusCallback = url;
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
via(provider) {
|
|
133
|
+
this.provider = provider;
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
async send() {
|
|
137
|
+
await ensureConfig();
|
|
138
|
+
if (!this.message.to)
|
|
139
|
+
return {
|
|
140
|
+
success: !1,
|
|
141
|
+
to: "",
|
|
142
|
+
error: "Recipient is required",
|
|
143
|
+
provider: this.provider || smsConfig.provider || "twilio"
|
|
144
|
+
};
|
|
145
|
+
if (!this.message.body)
|
|
146
|
+
return {
|
|
147
|
+
success: !1,
|
|
148
|
+
to: Array.isArray(this.message.to) ? this.message.to[0] ?? "" : this.message.to,
|
|
149
|
+
error: "Message body is required",
|
|
150
|
+
provider: this.provider || smsConfig.provider || "twilio"
|
|
151
|
+
};
|
|
152
|
+
return (this.provider ? getDriver(this.provider) : getDefaultDriver()).send(this.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export function sms() {
|
|
156
|
+
return new SmsBuilder;
|
|
157
|
+
}
|
|
158
|
+
export async function sendTemplate(to, templateName, variables = {}) {
|
|
159
|
+
await ensureConfig();
|
|
160
|
+
const template = smsConfig.templates?.find((t) => t.name === templateName);
|
|
161
|
+
if (!template)
|
|
162
|
+
return {
|
|
163
|
+
success: !1,
|
|
164
|
+
to: Array.isArray(to) ? to[0] ?? "" : to,
|
|
165
|
+
error: `Template not found: ${templateName}`,
|
|
166
|
+
provider: smsConfig.provider || "twilio"
|
|
167
|
+
};
|
|
168
|
+
let body = template.body;
|
|
169
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
170
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
171
|
+
body = body.replace(new RegExp(`\\{${escapedKey}\\}`, "g"), value);
|
|
172
|
+
}
|
|
173
|
+
return send({ to, body });
|
|
174
|
+
}
|
|
175
|
+
export function formatE164(phoneNumber, defaultCountryCode) {
|
|
176
|
+
let cleaned = phoneNumber.replace(/[\s\-()]/g, "");
|
|
177
|
+
if (cleaned.startsWith("+"))
|
|
178
|
+
return cleaned;
|
|
179
|
+
const countryCode = defaultCountryCode || smsConfig.defaultCountryCode || "1";
|
|
180
|
+
if (cleaned.startsWith("00"))
|
|
181
|
+
cleaned = `+${cleaned.slice(2)}`;
|
|
182
|
+
else if (!cleaned.startsWith("+"))
|
|
183
|
+
cleaned = `+${countryCode}${cleaned}`;
|
|
184
|
+
return cleaned;
|
|
185
|
+
}
|
|
186
|
+
export function isValidPhoneNumber(phoneNumber) {
|
|
187
|
+
const e164Regex = /^\+[1-9]\d{6,14}$/, formatted = formatE164(phoneNumber);
|
|
188
|
+
return e164Regex.test(formatted);
|
|
189
|
+
}
|
|
190
|
+
export async function init() {
|
|
191
|
+
await loadConfig();
|
|
192
|
+
}
|
|
193
|
+
export function isEnabled() {
|
|
194
|
+
return smsConfig.enabled === !0;
|
|
195
|
+
}
|
|
196
|
+
export function getConfig() {
|
|
197
|
+
return { ...smsConfig };
|
|
198
|
+
}
|
|
199
|
+
export const SMS = {
|
|
200
|
+
init,
|
|
201
|
+
configure,
|
|
202
|
+
isEnabled,
|
|
203
|
+
getConfig,
|
|
204
|
+
send,
|
|
205
|
+
sendSms,
|
|
206
|
+
sendBulk,
|
|
207
|
+
sendTemplate,
|
|
208
|
+
getStatus,
|
|
209
|
+
getBalance,
|
|
210
|
+
verifyNumber,
|
|
211
|
+
startVerification,
|
|
212
|
+
checkVerification,
|
|
213
|
+
cancelVerification,
|
|
214
|
+
formatE164,
|
|
215
|
+
isValidPhoneNumber,
|
|
216
|
+
sms,
|
|
217
|
+
getDriver,
|
|
218
|
+
getVerificationDriver
|
|
219
|
+
};
|
|
220
|
+
export default SMS;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/sms",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.90",
|
|
6
6
|
"description": "The Stacks SMS integration. Painlessly create & manage your inboxes, templates, and send sms.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
"prepublishOnly": "bun run build"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@stacksjs/cli": "0.70.
|
|
62
|
-
"@stacksjs/config": "0.70.
|
|
61
|
+
"@stacksjs/cli": "0.70.90",
|
|
62
|
+
"@stacksjs/config": "0.70.90",
|
|
63
63
|
"better-dx": "^0.2.16",
|
|
64
|
-
"@stacksjs/error-handling": "0.70.
|
|
65
|
-
"@stacksjs/types": "0.70.
|
|
64
|
+
"@stacksjs/error-handling": "0.70.90",
|
|
65
|
+
"@stacksjs/types": "0.70.90"
|
|
66
66
|
}
|
|
67
67
|
}
|