verify-phone-sms 0.9.4

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