@devdataphone/sdk 1.0.0
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/index.d.ts +249 -0
- package/index.js +175 -0
- package/index.mjs +31 -0
- package/package.json +57 -0
- package/src/countries.js +240 -0
- package/src/formatter.js +299 -0
- package/src/generator.js +334 -0
- package/src/validator.js +364 -0
package/src/generator.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phone number generator module
|
|
3
|
+
* Generates valid E.164 formatted phone numbers for testing
|
|
4
|
+
* @module generator
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { COUNTRIES, US_AREA_CODES, CA_AREA_CODES, getCountry, isSupported } = require('./countries');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a random integer between min and max (inclusive)
|
|
13
|
+
* @param {number} min
|
|
14
|
+
* @param {number} max
|
|
15
|
+
* @returns {number}
|
|
16
|
+
*/
|
|
17
|
+
function randomInt(min, max) {
|
|
18
|
+
min = Math.ceil(min);
|
|
19
|
+
max = Math.floor(max);
|
|
20
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pad number with leading zeros
|
|
25
|
+
* @param {number|string} num
|
|
26
|
+
* @param {number} size
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function pad(num, size) {
|
|
30
|
+
let s = String(num);
|
|
31
|
+
while (s.length < size) s = '0' + s;
|
|
32
|
+
return s;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate random digits string
|
|
37
|
+
* @param {number} length
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function randomDigits(length) {
|
|
41
|
+
let result = '';
|
|
42
|
+
for (let i = 0; i < length; i++) {
|
|
43
|
+
result += randomInt(0, 9);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate US phone number using reserved 555-01xx range
|
|
50
|
+
* @returns {{raw: string, location: string, areaCode: string}}
|
|
51
|
+
*/
|
|
52
|
+
function generateUS() {
|
|
53
|
+
const selection = US_AREA_CODES[randomInt(0, US_AREA_CODES.length - 1)];
|
|
54
|
+
const exchange = 555;
|
|
55
|
+
const subscriber = randomInt(100, 199); // Reserved fictional range
|
|
56
|
+
return {
|
|
57
|
+
raw: `${selection.code}${exchange}${pad(subscriber, 4)}`,
|
|
58
|
+
formatted: `(${selection.code}) ${exchange}-${pad(subscriber, 4)}`,
|
|
59
|
+
location: `${selection.city}, ${selection.state}`,
|
|
60
|
+
areaCode: String(selection.code)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate Canada phone number
|
|
66
|
+
* @returns {{raw: string, location: string}}
|
|
67
|
+
*/
|
|
68
|
+
function generateCA() {
|
|
69
|
+
const selection = CA_AREA_CODES[randomInt(0, CA_AREA_CODES.length - 1)];
|
|
70
|
+
const exchange = 555;
|
|
71
|
+
const subscriber = randomInt(100, 199);
|
|
72
|
+
return {
|
|
73
|
+
raw: `${selection.code}${exchange}${pad(subscriber, 4)}`,
|
|
74
|
+
formatted: `(${selection.code}) ${exchange}-${pad(subscriber, 4)}`,
|
|
75
|
+
location: `${selection.city}, ${selection.province}`,
|
|
76
|
+
areaCode: String(selection.code)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate UK phone number using Ofcom reserved range
|
|
82
|
+
* @returns {{raw: string}}
|
|
83
|
+
*/
|
|
84
|
+
function generateUK() {
|
|
85
|
+
// Ofcom reserved: 07700 900000-900999
|
|
86
|
+
const subscriber = randomInt(0, 999);
|
|
87
|
+
return {
|
|
88
|
+
raw: `7700900${pad(subscriber, 3)}`,
|
|
89
|
+
formatted: `07700 900${pad(subscriber, 3)}`,
|
|
90
|
+
location: 'United Kingdom'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate Australia phone number using ACMA reserved range
|
|
96
|
+
* @returns {{raw: string}}
|
|
97
|
+
*/
|
|
98
|
+
function generateAU() {
|
|
99
|
+
// ACMA reserved: 0491 570 156-159
|
|
100
|
+
const lastThree = randomInt(156, 159);
|
|
101
|
+
return {
|
|
102
|
+
raw: `491570${lastThree}`,
|
|
103
|
+
formatted: `0491 570 ${lastThree}`,
|
|
104
|
+
location: 'Australia'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate China phone number
|
|
110
|
+
* @returns {{raw: string}}
|
|
111
|
+
*/
|
|
112
|
+
function generateCN() {
|
|
113
|
+
const country = getCountry('CN');
|
|
114
|
+
const prefixes = country.mobilePrefixes;
|
|
115
|
+
const prefix = prefixes[randomInt(0, prefixes.length - 1)];
|
|
116
|
+
const suffix = randomDigits(8);
|
|
117
|
+
return {
|
|
118
|
+
raw: `${prefix}${suffix}`,
|
|
119
|
+
formatted: `${prefix} ${suffix.slice(0, 4)} ${suffix.slice(4)}`,
|
|
120
|
+
location: 'China'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generate India phone number
|
|
126
|
+
* @returns {{raw: string}}
|
|
127
|
+
*/
|
|
128
|
+
function generateIN() {
|
|
129
|
+
const startDigits = ['6', '7', '8', '9'];
|
|
130
|
+
const start = startDigits[randomInt(0, startDigits.length - 1)];
|
|
131
|
+
const rest = randomDigits(9);
|
|
132
|
+
return {
|
|
133
|
+
raw: `${start}${rest}`,
|
|
134
|
+
formatted: `${start}${rest.slice(0, 4)} ${rest.slice(4)}`,
|
|
135
|
+
location: 'India'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate Germany phone number
|
|
141
|
+
* @returns {{raw: string}}
|
|
142
|
+
*/
|
|
143
|
+
function generateDE() {
|
|
144
|
+
const country = getCountry('DE');
|
|
145
|
+
const prefix = country.mobilePrefixes[randomInt(0, country.mobilePrefixes.length - 1)];
|
|
146
|
+
const suffix = randomDigits(7);
|
|
147
|
+
return {
|
|
148
|
+
raw: `${prefix}${suffix}`,
|
|
149
|
+
formatted: `0${prefix} ${suffix}`,
|
|
150
|
+
location: 'Germany'
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate France phone number
|
|
156
|
+
* @returns {{raw: string}}
|
|
157
|
+
*/
|
|
158
|
+
function generateFR() {
|
|
159
|
+
// French mobile starts with 06 or 07
|
|
160
|
+
const prefix = randomInt(0, 1) === 0 ? '6' : '7';
|
|
161
|
+
const rest = randomDigits(8);
|
|
162
|
+
return {
|
|
163
|
+
raw: `${prefix}${rest}`,
|
|
164
|
+
formatted: `0${prefix} ${rest.slice(0, 2)} ${rest.slice(2, 4)} ${rest.slice(4, 6)} ${rest.slice(6)}`,
|
|
165
|
+
location: 'France'
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate Japan phone number
|
|
171
|
+
* @returns {{raw: string}}
|
|
172
|
+
*/
|
|
173
|
+
function generateJP() {
|
|
174
|
+
const country = getCountry('JP');
|
|
175
|
+
const prefix = country.mobilePrefixes[randomInt(0, country.mobilePrefixes.length - 1)];
|
|
176
|
+
const rest = randomDigits(8);
|
|
177
|
+
return {
|
|
178
|
+
raw: `${prefix}${rest}`,
|
|
179
|
+
formatted: `0${prefix}-${rest.slice(0, 4)}-${rest.slice(4)}`,
|
|
180
|
+
location: 'Japan'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generate Brazil phone number
|
|
186
|
+
* @returns {{raw: string}}
|
|
187
|
+
*/
|
|
188
|
+
function generateBR() {
|
|
189
|
+
// Major area codes
|
|
190
|
+
const areaCodes = ['11', '21', '31', '41', '51', '61', '71', '81', '91'];
|
|
191
|
+
const areaCode = areaCodes[randomInt(0, areaCodes.length - 1)];
|
|
192
|
+
const rest = randomDigits(8);
|
|
193
|
+
return {
|
|
194
|
+
raw: `${areaCode}9${rest}`,
|
|
195
|
+
formatted: `(${areaCode}) 9${rest.slice(0, 4)}-${rest.slice(4)}`,
|
|
196
|
+
location: 'Brazil'
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generator function map
|
|
202
|
+
*/
|
|
203
|
+
const generators = {
|
|
204
|
+
US: generateUS,
|
|
205
|
+
CA: generateCA,
|
|
206
|
+
UK: generateUK,
|
|
207
|
+
AU: generateAU,
|
|
208
|
+
CN: generateCN,
|
|
209
|
+
IN: generateIN,
|
|
210
|
+
DE: generateDE,
|
|
211
|
+
FR: generateFR,
|
|
212
|
+
JP: generateJP,
|
|
213
|
+
BR: generateBR
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Format output number based on style
|
|
218
|
+
* @param {string} raw - Raw number digits
|
|
219
|
+
* @param {string} countryCode - Country code
|
|
220
|
+
* @param {string} format - Output format
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
function formatOutput(raw, countryCode, format) {
|
|
224
|
+
const country = getCountry(countryCode);
|
|
225
|
+
if (!country) return raw;
|
|
226
|
+
|
|
227
|
+
const dialCode = country.dialCode.replace('+', '');
|
|
228
|
+
|
|
229
|
+
switch (format) {
|
|
230
|
+
case 'e164':
|
|
231
|
+
return `+${dialCode}${raw}`;
|
|
232
|
+
case 'national':
|
|
233
|
+
// Return formatted version from generator
|
|
234
|
+
return raw;
|
|
235
|
+
case 'international':
|
|
236
|
+
return `+${dialCode} ${raw}`;
|
|
237
|
+
case 'digits':
|
|
238
|
+
return raw;
|
|
239
|
+
default:
|
|
240
|
+
return `+${dialCode}${raw}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate phone numbers
|
|
246
|
+
* @param {Object} options - Generation options
|
|
247
|
+
* @param {string} [options.region='US'] - Country code (US, UK, CN, etc.)
|
|
248
|
+
* @param {number} [options.count=1] - Number of phone numbers to generate (1-1000)
|
|
249
|
+
* @param {string} [options.format='e164'] - Output format ('e164', 'national', 'international', 'digits')
|
|
250
|
+
* @returns {Array<GeneratedNumber>}
|
|
251
|
+
* @throws {Error} If region is not supported or count is invalid
|
|
252
|
+
*/
|
|
253
|
+
function generate(options = {}) {
|
|
254
|
+
// Default options
|
|
255
|
+
const {
|
|
256
|
+
region = 'US',
|
|
257
|
+
count = 1,
|
|
258
|
+
format = 'e164'
|
|
259
|
+
} = options;
|
|
260
|
+
|
|
261
|
+
// Validate region
|
|
262
|
+
const normalizedRegion = String(region).toUpperCase();
|
|
263
|
+
if (!isSupported(normalizedRegion)) {
|
|
264
|
+
throw new Error(`Unsupported region: ${region}. Supported: ${Object.keys(generators).join(', ')}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Validate count
|
|
268
|
+
const numCount = parseInt(count, 10);
|
|
269
|
+
if (isNaN(numCount) || numCount < 1) {
|
|
270
|
+
throw new Error('Count must be a positive integer');
|
|
271
|
+
}
|
|
272
|
+
if (numCount > 1000) {
|
|
273
|
+
throw new Error('Count cannot exceed 1000');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Validate format
|
|
277
|
+
const validFormats = ['e164', 'national', 'international', 'digits'];
|
|
278
|
+
const normalizedFormat = String(format).toLowerCase();
|
|
279
|
+
if (!validFormats.includes(normalizedFormat)) {
|
|
280
|
+
throw new Error(`Invalid format: ${format}. Supported: ${validFormats.join(', ')}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Get generator function
|
|
284
|
+
const generatorFn = generators[normalizedRegion];
|
|
285
|
+
if (!generatorFn) {
|
|
286
|
+
throw new Error(`No generator available for region: ${normalizedRegion}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const country = getCountry(normalizedRegion);
|
|
290
|
+
const results = [];
|
|
291
|
+
|
|
292
|
+
for (let i = 0; i < numCount; i++) {
|
|
293
|
+
const generated = generatorFn();
|
|
294
|
+
|
|
295
|
+
let outputNumber;
|
|
296
|
+
if (normalizedFormat === 'national') {
|
|
297
|
+
outputNumber = generated.formatted;
|
|
298
|
+
} else if (normalizedFormat === 'digits') {
|
|
299
|
+
outputNumber = generated.raw;
|
|
300
|
+
} else {
|
|
301
|
+
outputNumber = formatOutput(generated.raw, normalizedRegion, normalizedFormat);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
results.push({
|
|
305
|
+
number: outputNumber,
|
|
306
|
+
e164: `${country.dialCode}${generated.raw}`,
|
|
307
|
+
national: generated.formatted,
|
|
308
|
+
country: country.name,
|
|
309
|
+
countryCode: normalizedRegion,
|
|
310
|
+
dialCode: country.dialCode,
|
|
311
|
+
location: generated.location || country.name,
|
|
312
|
+
raw: generated.raw
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return results;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Generate a single phone number (convenience method)
|
|
321
|
+
* @param {string} [region='US'] - Country code
|
|
322
|
+
* @param {string} [format='e164'] - Output format
|
|
323
|
+
* @returns {string}
|
|
324
|
+
*/
|
|
325
|
+
function generateOne(region = 'US', format = 'e164') {
|
|
326
|
+
const result = generate({ region, count: 1, format });
|
|
327
|
+
return result[0].number;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
generate,
|
|
332
|
+
generateOne,
|
|
333
|
+
generators
|
|
334
|
+
};
|
package/src/validator.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phone number validation module
|
|
3
|
+
* Validates phone numbers against E.164 standard
|
|
4
|
+
* @module validator
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { COUNTRIES, getCountry, isSupported } = require('./countries');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Remove all non-digit characters except leading +
|
|
13
|
+
* @param {string} input
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function sanitize(input) {
|
|
17
|
+
if (!input || typeof input !== 'string') return '';
|
|
18
|
+
|
|
19
|
+
// Preserve leading + if present
|
|
20
|
+
const hasPlus = input.trim().startsWith('+');
|
|
21
|
+
const digits = input.replace(/\D/g, '');
|
|
22
|
+
|
|
23
|
+
return hasPlus ? `+${digits}` : digits;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract country code from E.164 number
|
|
28
|
+
* @param {string} number - E.164 formatted number starting with +
|
|
29
|
+
* @returns {{countryCode: string, dialCode: string, nationalNumber: string}|null}
|
|
30
|
+
*/
|
|
31
|
+
function extractCountryCode(number) {
|
|
32
|
+
if (!number || !number.startsWith('+')) return null;
|
|
33
|
+
|
|
34
|
+
const digits = number.slice(1); // Remove +
|
|
35
|
+
|
|
36
|
+
// Sort countries by dial code length (descending) to match longer codes first
|
|
37
|
+
const sortedCountries = [...COUNTRIES].sort((a, b) =>
|
|
38
|
+
b.dialCode.length - a.dialCode.length
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
for (const country of sortedCountries) {
|
|
42
|
+
const dialDigits = country.dialCode.replace('+', '');
|
|
43
|
+
if (digits.startsWith(dialDigits)) {
|
|
44
|
+
return {
|
|
45
|
+
countryCode: country.code,
|
|
46
|
+
dialCode: country.dialCode,
|
|
47
|
+
nationalNumber: digits.slice(dialDigits.length)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate phone number length for a country
|
|
57
|
+
* @param {string} nationalNumber - National significant number
|
|
58
|
+
* @param {Object} country - Country data
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function validateLength(nationalNumber, country) {
|
|
62
|
+
if (!nationalNumber || !country) return false;
|
|
63
|
+
|
|
64
|
+
const len = nationalNumber.length;
|
|
65
|
+
const expected = country.numberLength;
|
|
66
|
+
|
|
67
|
+
if (typeof expected === 'number') {
|
|
68
|
+
return len === expected;
|
|
69
|
+
} else if (typeof expected === 'object' && expected.min && expected.max) {
|
|
70
|
+
return len >= expected.min && len <= expected.max;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Default: allow 7-15 digits (E.164 standard)
|
|
74
|
+
return len >= 7 && len <= 15;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate using country-specific regex
|
|
79
|
+
* @param {string} input - Input number
|
|
80
|
+
* @param {Object} country - Country data
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function validateWithRegex(input, country) {
|
|
84
|
+
if (!country || !country.regex) return false;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
return country.regex.test(input);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect number type (MOBILE, FIXED_LINE, etc.)
|
|
95
|
+
* @param {string} nationalNumber
|
|
96
|
+
* @param {Object} country
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function detectType(nationalNumber, country) {
|
|
100
|
+
if (!nationalNumber || !country) return 'UNKNOWN';
|
|
101
|
+
|
|
102
|
+
// Check mobile prefix
|
|
103
|
+
if (country.mobilePrefix) {
|
|
104
|
+
if (nationalNumber.startsWith(country.mobilePrefix)) {
|
|
105
|
+
return 'MOBILE';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check mobile prefixes array
|
|
110
|
+
if (country.mobilePrefixes) {
|
|
111
|
+
for (const prefix of country.mobilePrefixes) {
|
|
112
|
+
if (nationalNumber.startsWith(prefix)) {
|
|
113
|
+
return 'MOBILE';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check mobile start digits
|
|
119
|
+
if (country.mobileStartDigits) {
|
|
120
|
+
if (country.mobileStartDigits.includes(nationalNumber[0])) {
|
|
121
|
+
return 'MOBILE';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return 'UNKNOWN';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Validation result object
|
|
130
|
+
* @typedef {Object} ValidationResult
|
|
131
|
+
* @property {boolean} valid - Whether the number is valid
|
|
132
|
+
* @property {string|null} e164 - E.164 formatted number
|
|
133
|
+
* @property {string|null} country - Country name
|
|
134
|
+
* @property {string|null} countryCode - ISO country code
|
|
135
|
+
* @property {string|null} dialCode - International dial code
|
|
136
|
+
* @property {string|null} nationalNumber - National significant number
|
|
137
|
+
* @property {string} type - Number type (MOBILE, FIXED_LINE, UNKNOWN)
|
|
138
|
+
* @property {string|null} error - Error message if invalid
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validate a phone number
|
|
143
|
+
* @param {string} input - Phone number to validate
|
|
144
|
+
* @param {Object} [options] - Validation options
|
|
145
|
+
* @param {string} [options.defaultCountry] - Default country code for local numbers
|
|
146
|
+
* @param {boolean} [options.strict=false] - Enable strict validation
|
|
147
|
+
* @returns {ValidationResult}
|
|
148
|
+
*/
|
|
149
|
+
function validate(input, options = {}) {
|
|
150
|
+
const { defaultCountry, strict = false } = options;
|
|
151
|
+
|
|
152
|
+
// Handle empty input
|
|
153
|
+
if (!input || typeof input !== 'string') {
|
|
154
|
+
return {
|
|
155
|
+
valid: false,
|
|
156
|
+
e164: null,
|
|
157
|
+
country: null,
|
|
158
|
+
countryCode: null,
|
|
159
|
+
dialCode: null,
|
|
160
|
+
nationalNumber: null,
|
|
161
|
+
type: 'UNKNOWN',
|
|
162
|
+
error: 'Input is required'
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const trimmed = input.trim();
|
|
167
|
+
if (trimmed === '') {
|
|
168
|
+
return {
|
|
169
|
+
valid: false,
|
|
170
|
+
e164: null,
|
|
171
|
+
country: null,
|
|
172
|
+
countryCode: null,
|
|
173
|
+
dialCode: null,
|
|
174
|
+
nationalNumber: null,
|
|
175
|
+
type: 'UNKNOWN',
|
|
176
|
+
error: 'Input is empty'
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Sanitize input
|
|
181
|
+
const sanitized = sanitize(trimmed);
|
|
182
|
+
|
|
183
|
+
// Check if it's an international format
|
|
184
|
+
if (sanitized.startsWith('+')) {
|
|
185
|
+
const extracted = extractCountryCode(sanitized);
|
|
186
|
+
|
|
187
|
+
if (!extracted) {
|
|
188
|
+
return {
|
|
189
|
+
valid: false,
|
|
190
|
+
e164: null,
|
|
191
|
+
country: null,
|
|
192
|
+
countryCode: null,
|
|
193
|
+
dialCode: null,
|
|
194
|
+
nationalNumber: null,
|
|
195
|
+
type: 'UNKNOWN',
|
|
196
|
+
error: 'Unrecognized country code'
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const country = getCountry(extracted.countryCode);
|
|
201
|
+
|
|
202
|
+
// Validate length
|
|
203
|
+
if (!validateLength(extracted.nationalNumber, country)) {
|
|
204
|
+
return {
|
|
205
|
+
valid: false,
|
|
206
|
+
e164: null,
|
|
207
|
+
country: country.name,
|
|
208
|
+
countryCode: extracted.countryCode,
|
|
209
|
+
dialCode: extracted.dialCode,
|
|
210
|
+
nationalNumber: extracted.nationalNumber,
|
|
211
|
+
type: 'UNKNOWN',
|
|
212
|
+
error: `Invalid number length for ${country.name}`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Strict mode: validate with regex
|
|
217
|
+
if (strict && !validateWithRegex(trimmed, country)) {
|
|
218
|
+
return {
|
|
219
|
+
valid: false,
|
|
220
|
+
e164: null,
|
|
221
|
+
country: country.name,
|
|
222
|
+
countryCode: extracted.countryCode,
|
|
223
|
+
dialCode: extracted.dialCode,
|
|
224
|
+
nationalNumber: extracted.nationalNumber,
|
|
225
|
+
type: 'UNKNOWN',
|
|
226
|
+
error: `Number format does not match ${country.name} pattern`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const type = detectType(extracted.nationalNumber, country);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
valid: true,
|
|
234
|
+
e164: sanitized,
|
|
235
|
+
country: country.name,
|
|
236
|
+
countryCode: extracted.countryCode,
|
|
237
|
+
dialCode: extracted.dialCode,
|
|
238
|
+
nationalNumber: extracted.nationalNumber,
|
|
239
|
+
type,
|
|
240
|
+
error: null
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Handle local format numbers
|
|
245
|
+
if (defaultCountry) {
|
|
246
|
+
const country = getCountry(defaultCountry);
|
|
247
|
+
if (!country) {
|
|
248
|
+
return {
|
|
249
|
+
valid: false,
|
|
250
|
+
e164: null,
|
|
251
|
+
country: null,
|
|
252
|
+
countryCode: null,
|
|
253
|
+
dialCode: null,
|
|
254
|
+
nationalNumber: null,
|
|
255
|
+
type: 'UNKNOWN',
|
|
256
|
+
error: `Unsupported country: ${defaultCountry}`
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Remove leading 0 if present (common in local formats)
|
|
261
|
+
let nationalNumber = sanitized;
|
|
262
|
+
if (nationalNumber.startsWith('0')) {
|
|
263
|
+
nationalNumber = nationalNumber.slice(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Validate with regex if available
|
|
267
|
+
if (strict && !validateWithRegex(trimmed, country)) {
|
|
268
|
+
return {
|
|
269
|
+
valid: false,
|
|
270
|
+
e164: null,
|
|
271
|
+
country: country.name,
|
|
272
|
+
countryCode: country.code,
|
|
273
|
+
dialCode: country.dialCode,
|
|
274
|
+
nationalNumber,
|
|
275
|
+
type: 'UNKNOWN',
|
|
276
|
+
error: `Number format does not match ${country.name} pattern`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validate length
|
|
281
|
+
if (!validateLength(nationalNumber, country)) {
|
|
282
|
+
return {
|
|
283
|
+
valid: false,
|
|
284
|
+
e164: null,
|
|
285
|
+
country: country.name,
|
|
286
|
+
countryCode: country.code,
|
|
287
|
+
dialCode: country.dialCode,
|
|
288
|
+
nationalNumber,
|
|
289
|
+
type: 'UNKNOWN',
|
|
290
|
+
error: `Invalid number length for ${country.name}`
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const type = detectType(nationalNumber, country);
|
|
295
|
+
const dialDigits = country.dialCode.replace('+', '');
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
valid: true,
|
|
299
|
+
e164: `+${dialDigits}${nationalNumber}`,
|
|
300
|
+
country: country.name,
|
|
301
|
+
countryCode: country.code,
|
|
302
|
+
dialCode: country.dialCode,
|
|
303
|
+
nationalNumber,
|
|
304
|
+
type,
|
|
305
|
+
error: null
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// No country context and not E.164 format
|
|
310
|
+
return {
|
|
311
|
+
valid: false,
|
|
312
|
+
e164: null,
|
|
313
|
+
country: null,
|
|
314
|
+
countryCode: null,
|
|
315
|
+
dialCode: null,
|
|
316
|
+
nationalNumber: sanitized,
|
|
317
|
+
type: 'UNKNOWN',
|
|
318
|
+
error: 'Cannot determine country. Use E.164 format (+XXX...) or provide defaultCountry option'
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Quick validation check
|
|
324
|
+
* @param {string} input - Phone number to validate
|
|
325
|
+
* @param {string} [defaultCountry] - Default country code
|
|
326
|
+
* @returns {boolean}
|
|
327
|
+
*/
|
|
328
|
+
function isValid(input, defaultCountry) {
|
|
329
|
+
return validate(input, { defaultCountry }).valid;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Validate E.164 format specifically
|
|
334
|
+
* @param {string} input - Phone number to validate
|
|
335
|
+
* @returns {boolean}
|
|
336
|
+
*/
|
|
337
|
+
function isE164(input) {
|
|
338
|
+
if (!input || typeof input !== 'string') return false;
|
|
339
|
+
|
|
340
|
+
const trimmed = input.trim();
|
|
341
|
+
|
|
342
|
+
// Must start with +
|
|
343
|
+
if (!trimmed.startsWith('+')) return false;
|
|
344
|
+
|
|
345
|
+
// Extract digits
|
|
346
|
+
const digits = trimmed.slice(1);
|
|
347
|
+
|
|
348
|
+
// E.164: 1-15 digits after +
|
|
349
|
+
if (!/^\d{1,15}$/.test(digits)) return false;
|
|
350
|
+
|
|
351
|
+
// Must have a valid country code
|
|
352
|
+
const extracted = extractCountryCode(trimmed);
|
|
353
|
+
if (!extracted) return false;
|
|
354
|
+
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = {
|
|
359
|
+
validate,
|
|
360
|
+
isValid,
|
|
361
|
+
isE164,
|
|
362
|
+
sanitize,
|
|
363
|
+
extractCountryCode
|
|
364
|
+
};
|