sms-verification-api 0.9.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/.env.example +20 -0
- package/DEPLOYMENT.md +151 -0
- package/README.md +475 -0
- package/docs/app/(home)/layout.tsx +7 -0
- package/docs/app/(home)/page.tsx +38 -0
- package/docs/app/docs/[[...slug]]/page.tsx +59 -0
- package/docs/app/docs/layout.tsx +12 -0
- package/docs/app/docs-og/[...slug]/route.ts +24 -0
- package/docs/app/globals.css +587 -0
- package/docs/app/layout.config.tsx +13 -0
- package/docs/app/layout.tsx +27 -0
- package/docs/app/logo.tsx +35 -0
- package/docs/content/docs/API_AUTHENTICATION.md +91 -0
- package/docs/content/docs/DEPLOYMENT.md +181 -0
- package/docs/content/docs/api/post.mdx +35 -0
- package/docs/content/docs/api/verify.mdx +34 -0
- package/docs/content/docs/meta.json +8 -0
- package/docs/content/docs/verify-legal-name.md +339 -0
- package/docs/lib/source.ts +14 -0
- package/docs/mdx-components.tsx +12 -0
- package/docs/next.config.mjs +51 -0
- package/docs/openapi.json +329 -0
- package/docs/package.json +37 -0
- package/docs/postcss.config.mjs +5 -0
- package/docs/scripts/generate-docs.mjs +23 -0
- package/docs/source.config.ts +5 -0
- package/docs/tsconfig.json +29 -0
- package/docs/worker.js +35 -0
- package/docs/wrangler.toml +26 -0
- package/examples/client.js +119 -0
- package/examples/demo.html +325 -0
- package/examples/libphonenumber-example.js +120 -0
- package/openapi.json +329 -0
- package/package.json +71 -0
- package/scripts/deploy.sh +63 -0
- package/src/identity-verification-server.ts +553 -0
- package/src/index.js +8 -0
- package/src/sns.js +236 -0
- package/src/verify-phone-server.js +448 -0
- package/src/verify-phone.ts +551 -0
- package/test/api.test.js +201 -0
- package/test/integration.test.js +152 -0
- package/test/metadata-test.js +73 -0
- package/test/server.test.js +143 -0
- package/test/setup.js +32 -0
- package/test/utils.test.js +186 -0
- package/test/verify.test.js +23 -0
- package/test/voip.test.js +112 -0
- package/vitest.config.js +10 -0
- package/wrangler.toml +27 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { parsePhoneNumber, isValidPhoneNumber as isValidPhoneNumberLib, getNumberType } from 'libphonenumber-js';
|
|
2
|
+
|
|
3
|
+
interface VerifyPhoneOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The phone number to send the SMS to (e.g., "+1234567890")
|
|
6
|
+
*/
|
|
7
|
+
phoneNumber: string;
|
|
8
|
+
/**
|
|
9
|
+
* The verification code to send (required)
|
|
10
|
+
*/
|
|
11
|
+
code: string;
|
|
12
|
+
/**
|
|
13
|
+
* AWS access key ID
|
|
14
|
+
*/
|
|
15
|
+
accessKeyId?: string;
|
|
16
|
+
/**
|
|
17
|
+
* AWS secret access key
|
|
18
|
+
*/
|
|
19
|
+
secretAccessKey?: string;
|
|
20
|
+
/**
|
|
21
|
+
* AWS region (default: 'us-east-1')
|
|
22
|
+
*/
|
|
23
|
+
awsRegion?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether to block VoIP numbers (default: false)
|
|
26
|
+
*/
|
|
27
|
+
blockVoip?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Method to use for VoIP detection: 'api' (external API) or 'libphonenumber' (default: 'api')
|
|
30
|
+
*/
|
|
31
|
+
voipDetectionMethod?: 'api' | 'libphonenumber';
|
|
32
|
+
/**
|
|
33
|
+
* Whether to use libphonenumber-js for phone number formatting and validation (default: false)
|
|
34
|
+
*/
|
|
35
|
+
useLibPhoneNumber?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Metadata type to use with libphonenumber-js: 'minimal' (75KB) or 'full' (140KB, default: 'minimal')
|
|
38
|
+
* Full metadata provides better phone number type detection (MOBILE, FIXED_LINE, VOIP, etc.)
|
|
39
|
+
*/
|
|
40
|
+
metadataType?: 'minimal' | 'full';
|
|
41
|
+
/**
|
|
42
|
+
* SMS sender ID (max 11 characters, default: 'Verify')
|
|
43
|
+
*/
|
|
44
|
+
senderId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* SMS type ('Transactional' or 'Promotional', default: 'Transactional')
|
|
47
|
+
*/
|
|
48
|
+
smsType?: 'Transactional' | 'Promotional';
|
|
49
|
+
/**
|
|
50
|
+
* Custom message template. Use {code} as placeholder for the code.
|
|
51
|
+
* (default: 'Your verification code is: {code}.')
|
|
52
|
+
*/
|
|
53
|
+
messageTemplate?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Verify phone number by sending an SMS text with a code via AWS SNS.
|
|
58
|
+
*
|
|
59
|
+
* 
|
|
60
|
+
* @param {string} options.phoneNumber - The phone number to send the SMS to (e.g., "+1234567890")
|
|
61
|
+
* @param {string} options.code - The verification code to send (required)
|
|
62
|
+
* @param {string} options.accessKeyId - AWS access key ID
|
|
63
|
+
* @param {string} options.secretAccessKey - AWS secret access key
|
|
64
|
+
* @param {string} [options.awsRegion='us-east-1'] - AWS region
|
|
65
|
+
* @param {boolean} [options.blockVoip=false] - Whether to block VoIP numbers
|
|
66
|
+
* @param {string} [options.voipDetectionMethod='api'] - Method for VoIP detection: 'api' (external API) or 'libphonenumber' (local analysis)
|
|
67
|
+
* @param {boolean} [options.useLibPhoneNumber=false] - Whether to use libphonenumber-js for phone number formatting and validation
|
|
68
|
+
* @param {string} [options.metadataType='minimal'] - Metadata type: 'minimal' (75KB) or 'full' (140KB) for better phone type detection
|
|
69
|
+
* @param {string} [options.senderId='Verify'] - SMS sender ID (max 11 characters)
|
|
70
|
+
* @param {string} [options.smsType='Transactional'] - SMS type ('Transactional' or 'Promotional')
|
|
71
|
+
* @param {string} [options.messageTemplate] - Custom message template. Use {code} as placeholder for the code.
|
|
72
|
+
* @returns {Promise<Object>} Response object with success status, message, messageId, and code
|
|
73
|
+
*/
|
|
74
|
+
export default async function verifyPhone(options = {} as VerifyPhoneOptions) {
|
|
75
|
+
var {
|
|
76
|
+
phoneNumber,
|
|
77
|
+
code,
|
|
78
|
+
accessKeyId = process?.env?.AWS_ACCESS_KEY_ID,
|
|
79
|
+
secretAccessKey = process?.env?.AWS_SECRET_ACCESS_KEY,
|
|
80
|
+
awsRegion = process?.env?.AWS_REGION,
|
|
81
|
+
blockVoip = false,
|
|
82
|
+
voipDetectionMethod = 'api',
|
|
83
|
+
useLibPhoneNumber = false,
|
|
84
|
+
metadataType = 'minimal',
|
|
85
|
+
senderId = 'Verify',
|
|
86
|
+
smsType = 'Transactional',
|
|
87
|
+
messageTemplate = 'Your verification code is: {code}.'
|
|
88
|
+
} = options;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Validate that code is provided
|
|
92
|
+
if (!code) {
|
|
93
|
+
throw new Error('Verification code is required');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate code format (alphanumeric, min 4 characters)
|
|
97
|
+
if (!/^[a-zA-Z0-9]{4,}$/.test(code)) {
|
|
98
|
+
throw new Error('Code must be alphanumeric and at least 4 characters');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Format and validate phone number
|
|
102
|
+
const formattedPhone = useLibPhoneNumber
|
|
103
|
+
? formatPhoneNumberLibPhoneNumber(phoneNumber)
|
|
104
|
+
: formatPhoneNumber(phoneNumber);
|
|
105
|
+
|
|
106
|
+
if (!isValidPhoneNumber(formattedPhone, useLibPhoneNumber)) {
|
|
107
|
+
throw new Error('Invalid phone number format. Please use E.164 format (e.g., +1234567890)');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check if VoIP blocking is enabled
|
|
111
|
+
if (blockVoip) {
|
|
112
|
+
const isVoip = voipDetectionMethod === 'libphonenumber'
|
|
113
|
+
? await isPhoneNumberVoipLibPhoneNumber(formattedPhone, metadataType)
|
|
114
|
+
: await isPhoneNumberVoip(formattedPhone);
|
|
115
|
+
|
|
116
|
+
if (isVoip) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: 'VoIP numbers are not allowed',
|
|
120
|
+
details: 'This phone number appears to be a VoIP number, which is not supported for verification',
|
|
121
|
+
isVoip: true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Create SNS client
|
|
127
|
+
const snsClient = new SNSClient({ accessKeyId, secretAccessKey, awsRegion });
|
|
128
|
+
|
|
129
|
+
// Prepare message
|
|
130
|
+
const message = messageTemplate.replace('{code}', code);
|
|
131
|
+
|
|
132
|
+
// Prepare SNS parameters
|
|
133
|
+
const params = {
|
|
134
|
+
Message: message,
|
|
135
|
+
PhoneNumber: formattedPhone,
|
|
136
|
+
'MessageAttributes.entry.1.Name': 'AWS.SNS.SMS.SenderID',
|
|
137
|
+
'MessageAttributes.entry.1.Value.DataType': 'String',
|
|
138
|
+
'MessageAttributes.entry.1.Value.StringValue': senderId,
|
|
139
|
+
'MessageAttributes.entry.2.Name': 'AWS.SNS.SMS.SMSType',
|
|
140
|
+
'MessageAttributes.entry.2.Value.DataType': 'String',
|
|
141
|
+
'MessageAttributes.entry.2.Value.StringValue': smsType
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Send SMS
|
|
145
|
+
const response = await snsClient.makeRequest('Publish', params);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
success: true,
|
|
149
|
+
message: 'Verification code sent successfully',
|
|
150
|
+
messageId: response.MessageId,
|
|
151
|
+
code: code,
|
|
152
|
+
phoneNumber: formattedPhone,
|
|
153
|
+
expiresIn: 600 // 10 minutes in seconds
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
error: error.message,
|
|
160
|
+
details: error.stack || undefined
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Format phone number to E.164 format using libphonenumber-js
|
|
167
|
+
* @param {string} phone - The input phone number
|
|
168
|
+
* @returns {string} - The formatted E.164 phone number
|
|
169
|
+
*/
|
|
170
|
+
function formatPhoneNumberLibPhoneNumber(phone) {
|
|
171
|
+
try {
|
|
172
|
+
const phoneNumber = parsePhoneNumber(phone);
|
|
173
|
+
if (phoneNumber && phoneNumber.isValid()) {
|
|
174
|
+
return phoneNumber.format('E.164');
|
|
175
|
+
}
|
|
176
|
+
// Fallback to basic formatting if libphonenumber-js fails
|
|
177
|
+
return formatPhoneNumber(phone);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.warn('libphonenumber-js formatting failed, falling back to basic formatting:', error);
|
|
180
|
+
return formatPhoneNumber(phone);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Format phone number to E.164 format
|
|
186
|
+
* @param {string} phone - The input phone number
|
|
187
|
+
* @returns {string} - The formatted E.164 phone number
|
|
188
|
+
*/
|
|
189
|
+
function formatPhoneNumber(phone) {
|
|
190
|
+
const cleaned = phone.replace(/\D/g, '');
|
|
191
|
+
|
|
192
|
+
if (cleaned.length === 10) {
|
|
193
|
+
return `+1${cleaned}`;
|
|
194
|
+
} else if (cleaned.length === 11 && cleaned.startsWith('1')) {
|
|
195
|
+
return `+${cleaned}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return phone.startsWith('+') ? phone : `+${cleaned}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate phone number against E.164 format
|
|
203
|
+
* @param {string} phone - The phone number to validate
|
|
204
|
+
* @param {boolean} useLibPhoneNumber - Whether to use libphonenumber-js for validation
|
|
205
|
+
* @returns {boolean} - True if valid, false otherwise
|
|
206
|
+
*/
|
|
207
|
+
function isValidPhoneNumber(phone, useLibPhoneNumber = false) {
|
|
208
|
+
if (useLibPhoneNumber) {
|
|
209
|
+
try {
|
|
210
|
+
const phoneNumber = parsePhoneNumber(phone);
|
|
211
|
+
return phoneNumber ? phoneNumber.isValid() : false;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.warn('libphonenumber-js validation failed, falling back to regex validation:', error);
|
|
214
|
+
// Fallback to regex validation
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const phoneRegex = /^\+[1-9]\d{1,14}$/;
|
|
219
|
+
return phoneRegex.test(phone) && phone.length >= 7 && phone.length <= 16;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* AWS SNS HTTP API Client for sending SMS verification codes
|
|
224
|
+
* Works directly in the browser using Web Crypto API
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
class SNSClient {
|
|
228
|
+
constructor(options = {}) {
|
|
229
|
+
this.accessKeyId = options.accessKeyId;
|
|
230
|
+
this.secretAccessKey = options.secretAccessKey;
|
|
231
|
+
this.region = options.awsRegion || 'us-east-1';
|
|
232
|
+
this.endpoint = `https://sns.${this.region}.amazonaws.com`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
stringToUint8Array(str) {
|
|
236
|
+
return new TextEncoder().encode(str);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
arrayBufferToHex(buffer) {
|
|
240
|
+
return Array.from(new Uint8Array(buffer))
|
|
241
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
242
|
+
.join('');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async sign(method, url, headers, payload) {
|
|
246
|
+
const now = new Date();
|
|
247
|
+
const amzDate = now.toISOString().replace(/[:\-]|\.\d{3}/g, '');
|
|
248
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
249
|
+
|
|
250
|
+
const canonicalUri = '/';
|
|
251
|
+
const canonicalQuerystring = url.split('?')[1] || '';
|
|
252
|
+
|
|
253
|
+
headers['host'] = `sns.${this.region}.amazonaws.com`;
|
|
254
|
+
headers['x-amz-date'] = amzDate;
|
|
255
|
+
|
|
256
|
+
const payloadHash = await crypto.subtle.digest('SHA-256', this.stringToUint8Array(payload))
|
|
257
|
+
.then(buffer => this.arrayBufferToHex(buffer));
|
|
258
|
+
headers['x-amz-content-sha256'] = payloadHash;
|
|
259
|
+
|
|
260
|
+
const sortedHeaders = Object.keys(headers).sort().map(key =>
|
|
261
|
+
`${key.toLowerCase()}:${headers[key]}`
|
|
262
|
+
).join('\n');
|
|
263
|
+
|
|
264
|
+
const signedHeaders = Object.keys(headers).sort().map(key =>
|
|
265
|
+
key.toLowerCase()
|
|
266
|
+
).join(';');
|
|
267
|
+
|
|
268
|
+
const canonicalRequest = [
|
|
269
|
+
method,
|
|
270
|
+
canonicalUri,
|
|
271
|
+
canonicalQuerystring,
|
|
272
|
+
sortedHeaders,
|
|
273
|
+
'',
|
|
274
|
+
signedHeaders,
|
|
275
|
+
payloadHash
|
|
276
|
+
].join('\n');
|
|
277
|
+
|
|
278
|
+
const algorithm = 'AWS4-HMAC-SHA256';
|
|
279
|
+
const credentialScope = `${dateStamp}/${this.region}/sns/aws4_request`;
|
|
280
|
+
|
|
281
|
+
const canonicalRequestHash = await crypto.subtle.digest('SHA-256', this.stringToUint8Array(canonicalRequest))
|
|
282
|
+
.then(buffer => this.arrayBufferToHex(buffer));
|
|
283
|
+
|
|
284
|
+
const stringToSign = [
|
|
285
|
+
algorithm,
|
|
286
|
+
amzDate,
|
|
287
|
+
credentialScope,
|
|
288
|
+
canonicalRequestHash
|
|
289
|
+
].join('\n');
|
|
290
|
+
|
|
291
|
+
const kDate = await crypto.subtle.importKey(
|
|
292
|
+
'raw',
|
|
293
|
+
this.stringToUint8Array(`AWS4${this.secretAccessKey}`),
|
|
294
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
295
|
+
false,
|
|
296
|
+
['sign']
|
|
297
|
+
).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array(dateStamp)));
|
|
298
|
+
|
|
299
|
+
const kRegion = await crypto.subtle.importKey(
|
|
300
|
+
'raw',
|
|
301
|
+
kDate,
|
|
302
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
303
|
+
false,
|
|
304
|
+
['sign']
|
|
305
|
+
).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array(this.region)));
|
|
306
|
+
|
|
307
|
+
const kService = await crypto.subtle.importKey(
|
|
308
|
+
'raw',
|
|
309
|
+
kRegion,
|
|
310
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
311
|
+
false,
|
|
312
|
+
['sign']
|
|
313
|
+
).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array('sns')));
|
|
314
|
+
|
|
315
|
+
const kSigning = await crypto.subtle.importKey(
|
|
316
|
+
'raw',
|
|
317
|
+
kService,
|
|
318
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
319
|
+
false,
|
|
320
|
+
['sign']
|
|
321
|
+
).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array('aws4_request')));
|
|
322
|
+
|
|
323
|
+
const signature = await crypto.subtle.importKey(
|
|
324
|
+
'raw',
|
|
325
|
+
kSigning,
|
|
326
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
327
|
+
false,
|
|
328
|
+
['sign']
|
|
329
|
+
).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array(stringToSign)))
|
|
330
|
+
.then(buffer => this.arrayBufferToHex(buffer));
|
|
331
|
+
|
|
332
|
+
headers['authorization'] = `${algorithm} Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
333
|
+
|
|
334
|
+
return headers;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async makeRequest(action, params = {}) {
|
|
338
|
+
const queryParams = new URLSearchParams({
|
|
339
|
+
Action: action,
|
|
340
|
+
Version: '2010-03-31',
|
|
341
|
+
...params
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const url = `${this.endpoint}/?${queryParams.toString()}`;
|
|
345
|
+
const payload = '';
|
|
346
|
+
const headers = {
|
|
347
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const signedHeaders = await this.sign('GET', url, headers, payload);
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const response = await fetch(url, {
|
|
354
|
+
method: 'GET',
|
|
355
|
+
headers: signedHeaders
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const text = await response.text();
|
|
359
|
+
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
throw new Error(`SNS API Error: ${response.status} - ${text}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return this.parseXMLResponse(text);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
throw new Error(`SNS Request failed: ${error.message}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
parseXMLResponse(xmlText) {
|
|
371
|
+
const messageIdMatch = xmlText.match(/<MessageId>([^<]+)<\/MessageId>/);
|
|
372
|
+
const errorCodeMatch = xmlText.match(/<Code>([^<]+)<\/Code>/);
|
|
373
|
+
const errorMessageMatch = xmlText.match(/<Message>([^<]+)<\/Message>/);
|
|
374
|
+
|
|
375
|
+
if (errorCodeMatch && errorMessageMatch) {
|
|
376
|
+
throw new Error(`${errorCodeMatch[1]}: ${errorMessageMatch[1]}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (messageIdMatch) {
|
|
380
|
+
return { MessageId: messageIdMatch[1] };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { raw: xmlText };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Checks if a phone number is a Bandwidth-only VoIP number (e.g. Google Voice).
|
|
389
|
+
*
|
|
390
|
+
* @see https://www.sent.dm/resources/phone-lookup
|
|
391
|
+
* @param {string} phone - The phone number in E.164 format (e.g., +1234567890).
|
|
392
|
+
* @returns {Promise<boolean>} True if phone is Bandwidth-only VoIP
|
|
393
|
+
*/
|
|
394
|
+
async function isPhoneNumberVoip(phone) {
|
|
395
|
+
try {
|
|
396
|
+
const response = await fetch(`https://www.sent.dm/api/phone-lookup?phone=${encodeURIComponent(phone)}`, {
|
|
397
|
+
headers: {
|
|
398
|
+
'Accept': 'application/json'
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (!response.ok) {
|
|
403
|
+
console.warn('Phone lookup failed:', response.status);
|
|
404
|
+
return false; // Default to allowing the number if lookup fails
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const phoneData = await response.json();
|
|
408
|
+
|
|
409
|
+
if (!phoneData || !phoneData.carrier) return false;
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
phoneData.carrier.name.toLowerCase().includes("bandwidth") ||
|
|
413
|
+
phoneData.carrier.type.toLowerCase() === "voip" ||
|
|
414
|
+
phoneData.portability.line_type.toLowerCase() === "mobile"
|
|
415
|
+
);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.warn('Phone lookup error:', error);
|
|
418
|
+
return false; // Default to allowing the number if lookup fails
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Checks if a phone number is likely a VoIP number using libphonenumber-js.
|
|
424
|
+
* This method analyzes the phone number structure and patterns to identify
|
|
425
|
+
* common VoIP number characteristics.
|
|
426
|
+
*
|
|
427
|
+
* @param {string} phone - The phone number in E.164 format (e.g., +1234567890).
|
|
428
|
+
* @param {string} metadataType - Metadata type: 'minimal' (75KB) or 'full' (140KB)
|
|
429
|
+
* @returns {Promise<boolean>} True if phone is likely VoIP
|
|
430
|
+
*/
|
|
431
|
+
async function isPhoneNumberVoipLibPhoneNumber(phone, metadataType = 'minimal') {
|
|
432
|
+
try {
|
|
433
|
+
// Parse the phone number using libphonenumber-js
|
|
434
|
+
const phoneNumber = parsePhoneNumber(phone);
|
|
435
|
+
|
|
436
|
+
if (!phoneNumber) {
|
|
437
|
+
console.warn('Could not parse phone number with libphonenumber-js');
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Check if the number is valid
|
|
442
|
+
if (!phoneNumber.isValid()) {
|
|
443
|
+
console.warn('Phone number is not valid according to libphonenumber-js');
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Get the country code
|
|
448
|
+
const country = phoneNumber.country;
|
|
449
|
+
|
|
450
|
+
// VoIP detection heuristics based on common patterns
|
|
451
|
+
|
|
452
|
+
// 1. Check for common VoIP area codes in the US
|
|
453
|
+
if (country === 'US') {
|
|
454
|
+
const nationalNumber = phoneNumber.nationalNumber;
|
|
455
|
+
|
|
456
|
+
// Common VoIP area codes (these are often used by VoIP providers)
|
|
457
|
+
const voipAreaCodes = [
|
|
458
|
+
'800', '888', '877', '866', '855', '844', '833', // Toll-free numbers
|
|
459
|
+
'900', '976', // Premium rate numbers
|
|
460
|
+
'700', // Personal communication services
|
|
461
|
+
'500', '521', '522', '523', '524', '525', '526', '527', '528', '529', // Personal communication services
|
|
462
|
+
'600', '601', '602', '603', '604', '605', '606', '607', '608', '609' // Personal communication services
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
if (voipAreaCodes.includes(nationalNumber.substring(0, 3))) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 2. Check for non-geographic numbers (often VoIP)
|
|
471
|
+
if (phoneNumber.isNonGeographic()) {
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 3. Use phone number type detection when available (requires 'full' metadata)
|
|
476
|
+
if (metadataType === 'full') {
|
|
477
|
+
try {
|
|
478
|
+
const numberType = getNumberType(phone);
|
|
479
|
+
|
|
480
|
+
// Direct VoIP detection
|
|
481
|
+
if (numberType === 'VOIP') {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Premium rate and toll-free numbers are often used by VoIP services
|
|
486
|
+
if (numberType === 'PREMIUM_RATE' || numberType === 'TOLL_FREE') {
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Shared cost numbers might be VoIP
|
|
491
|
+
if (numberType === 'SHARED_COST') {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Mobile numbers are generally safe (not VoIP)
|
|
496
|
+
if (numberType === 'MOBILE') {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Fixed line numbers are generally safe (not VoIP)
|
|
501
|
+
if (numberType === 'FIXED_LINE') {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Fixed line or mobile could be either, so we'll use other heuristics
|
|
506
|
+
if (numberType === 'FIXED_LINE_OR_MOBILE') {
|
|
507
|
+
// Continue with pattern analysis below
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.warn('Phone number type detection failed:', error);
|
|
512
|
+
// Continue with pattern analysis below
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 4. Pattern-based heuristics for when type detection is not available
|
|
517
|
+
const nationalNumber = phoneNumber.nationalNumber;
|
|
518
|
+
|
|
519
|
+
// Check for repeated digits (e.g., 555-5555)
|
|
520
|
+
if (/(\d)\1{2,}/.test(nationalNumber)) {
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Check for sequential digits (e.g., 123-4567)
|
|
525
|
+
if (/(?:0(?=1)|1(?=2)|2(?=3)|3(?=4)|4(?=5)|5(?=6)|6(?=7)|7(?=8)|8(?=9)){3,}/.test(nationalNumber)) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Check for numbers ending in common VoIP patterns
|
|
530
|
+
const voipEndings = ['0000', '1111', '2222', '3333', '4444', '5555', '6666', '7777', '8888', '9999'];
|
|
531
|
+
if (voipEndings.some(ending => nationalNumber.endsWith(ending))) {
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Check for numbers that are too "perfect" (often used by VoIP providers)
|
|
536
|
+
const digits = nationalNumber.split('');
|
|
537
|
+
const uniqueDigits = new Set(digits);
|
|
538
|
+
|
|
539
|
+
// If the number has very few unique digits, it might be VoIP
|
|
540
|
+
if (uniqueDigits.size <= 3 && nationalNumber.length >= 7) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Default to not VoIP if no patterns match
|
|
545
|
+
return false;
|
|
546
|
+
|
|
547
|
+
} catch (error) {
|
|
548
|
+
console.warn('libphonenumber-js VoIP detection error:', error);
|
|
549
|
+
return false; // Default to allowing the number if detection fails
|
|
550
|
+
}
|
|
551
|
+
}
|