@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.
@@ -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
+ };
@@ -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
+ };