core-services-sdk 1.3.10 → 1.3.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.10",
3
+ "version": "1.3.11",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -25,6 +25,7 @@
25
25
  "amqplib": "^0.10.8",
26
26
  "dot": "^1.1.3",
27
27
  "fastify": "^5.4.0",
28
+ "google-libphonenumber": "^3.2.42",
28
29
  "http-status": "^2.1.0",
29
30
  "mongodb": "^6.18.0",
30
31
  "node-fetch": "^3.3.2",
package/src/core/index.js CHANGED
@@ -3,3 +3,4 @@ export * from './otp-generators.js'
3
3
  export * from './sanitize-objects.js'
4
4
  export * from './normalize-to-array.js'
5
5
  export * from './combine-unique-arrays.js'
6
+ export * from './normalize-phone-number.js'
@@ -0,0 +1,93 @@
1
+ // src/core/phone-validate.js
2
+ // Validate & normalize using google-libphonenumber
3
+
4
+ import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber'
5
+
6
+ const phoneUtil = PhoneNumberUtil.getInstance()
7
+
8
+ /**
9
+ * Trim and remove invisible RTL markers that can sneak in from copy/paste.
10
+ * @param {string} input
11
+ * @returns {string}
12
+ */
13
+ function clean(input) {
14
+ return String(input)
15
+ .trim()
16
+ .replace(/[\u200e\u200f]/g, '')
17
+ }
18
+
19
+ /**
20
+ * Convert a parsed libphonenumber object into a normalized result.
21
+ * @param {import('google-libphonenumber').PhoneNumber} parsed
22
+ * @returns {{e164:string,national:string,international:string,regionCode:string|undefined,type:number}}
23
+ */
24
+ function toResult(parsed) {
25
+ return {
26
+ e164: phoneUtil.format(parsed, PhoneNumberFormat.E164),
27
+ national: phoneUtil.format(parsed, PhoneNumberFormat.NATIONAL),
28
+ international: phoneUtil.format(parsed, PhoneNumberFormat.INTERNATIONAL),
29
+ regionCode: phoneUtil.getRegionCodeForNumber(parsed),
30
+ type: phoneUtil.getNumberType(parsed),
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Parse & validate an international number (must start with '+').
36
+ * Throws on invalid input.
37
+ *
38
+ * @param {string} input - International number, e.g. "+972541234567"
39
+ * @returns {{e164:string,national:string,international:string,regionCode:string|undefined,type:number}}
40
+ * @throws {Error} If the number is invalid
41
+ */
42
+ export function normalizePhoneOrThrowIntl(input) {
43
+ try {
44
+ const parsed = phoneUtil.parseAndKeepRawInput(clean(input))
45
+ if (!phoneUtil.isValidNumber(parsed)) throw new Error('x')
46
+ return toResult(parsed)
47
+ } catch {
48
+ throw new Error('Invalid phone number')
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Parse & validate a national number using a region hint.
54
+ * Throws on invalid input.
55
+ *
56
+ * @param {string} input - National number, e.g. "054-123-4567"
57
+ * @param {string} defaultRegion - ISO region like "IL" or "US"
58
+ * @returns {{e164:string,national:string,international:string,regionCode:string|undefined,type:number}}
59
+ * @throws {Error} If the number is invalid
60
+ */
61
+ export function normalizePhoneOrThrowWithRegion(input, defaultRegion) {
62
+ try {
63
+ const parsed = phoneUtil.parseAndKeepRawInput(clean(input), defaultRegion)
64
+ if (!phoneUtil.isValidNumber(parsed)) throw new Error('x')
65
+ return toResult(parsed)
66
+ } catch {
67
+ throw new Error('Invalid phone number')
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Smart normalization:
73
+ * - If input starts with '+', parse as international.
74
+ * - Otherwise require a defaultRegion and parse as national.
75
+ * Throws on invalid input or when defaultRegion is missing for non-international numbers.
76
+ *
77
+ * @param {string} input
78
+ * @param {{ defaultRegion?: string }} [opts]
79
+ * @returns {{e164:string,national:string,international:string,regionCode:string|undefined,type:number}}
80
+ * @throws {Error} If invalid or defaultRegion is missing for non-international input
81
+ */
82
+ export function normalizePhoneOrThrow(input, opts = {}) {
83
+ const cleaned = clean(input)
84
+ if (/^\+/.test(cleaned)) {
85
+ return normalizePhoneOrThrowIntl(cleaned)
86
+ }
87
+ const { defaultRegion } = opts
88
+ if (!defaultRegion) {
89
+ // keep this one specific; your test relies on a different message here
90
+ throw new Error('defaultRegion is required for non-international numbers')
91
+ }
92
+ return normalizePhoneOrThrowWithRegion(cleaned, defaultRegion)
93
+ }
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber'
3
+
4
+ import {
5
+ normalizePhoneOrThrow,
6
+ normalizePhoneOrThrowIntl,
7
+ normalizePhoneOrThrowWithRegion,
8
+ } from '../../src/core/normalize-phone-number.js'
9
+
10
+ const phoneUtil = PhoneNumberUtil.getInstance()
11
+
12
+ /**
13
+ * Get a valid example number for a given region from libphonenumber.
14
+ * This keeps tests stable across environments and avoids fake numbers.
15
+ * @param {string} region - e.g. "IL", "US"
16
+ */
17
+ function exampleForRegion(region) {
18
+ const ex = phoneUtil.getExampleNumber(region)
19
+ return {
20
+ e164: phoneUtil.format(ex, PhoneNumberFormat.E164),
21
+ national: phoneUtil.format(ex, PhoneNumberFormat.NATIONAL),
22
+ region,
23
+ }
24
+ }
25
+
26
+ describe('phone normalization helpers', () => {
27
+ it('normalizePhoneOrThrowIntl: parses a valid international number (US)', () => {
28
+ const us = exampleForRegion('US')
29
+ const out = normalizePhoneOrThrowIntl(us.e164)
30
+ expect(out.e164).toBe(us.e164)
31
+ expect(out.regionCode).toBe('US')
32
+ expect(out.international).toMatch(/^\+1\b/)
33
+ })
34
+
35
+ it('normalizePhoneOrThrowWithRegion: parses a national number with region (IL)', () => {
36
+ const il = exampleForRegion('IL')
37
+ // introduce some separators/spaces to mimic user input
38
+ const dirty = ` ${il.national.replace(/\s/g, '-')} `
39
+ const out = normalizePhoneOrThrowWithRegion(dirty, 'IL')
40
+ expect(out.e164).toBe(il.e164)
41
+ expect(out.regionCode).toBe('IL')
42
+ expect(out.international).toMatch(/^\+972/)
43
+ })
44
+
45
+ it('normalizePhoneOrThrow (smart): international path without region', () => {
46
+ const us = exampleForRegion('US')
47
+ const out = normalizePhoneOrThrow(us.e164)
48
+ expect(out.e164).toBe(us.e164)
49
+ expect(out.regionCode).toBe('US')
50
+ })
51
+
52
+ it('normalizePhoneOrThrow (smart): national path requires defaultRegion', () => {
53
+ const il = exampleForRegion('IL')
54
+ const dirty = il.national.replace(/\s/g, ' - ')
55
+ const out = normalizePhoneOrThrow(dirty, { defaultRegion: 'IL' })
56
+ expect(out.e164).toBe(il.e164)
57
+ expect(out.regionCode).toBe('IL')
58
+ })
59
+
60
+ it('normalizePhoneOrThrow (smart): throws if national with no defaultRegion', () => {
61
+ expect(() => normalizePhoneOrThrow('054-123-4567')).toThrow(
62
+ /defaultRegion is required/i,
63
+ )
64
+ })
65
+
66
+ it('all helpers: throw on truly invalid numbers', () => {
67
+ expect(() => normalizePhoneOrThrowIntl('++972')).toThrow(
68
+ /Invalid phone number/i,
69
+ )
70
+ expect(() => normalizePhoneOrThrowWithRegion('123', 'IL')).toThrow(
71
+ /Invalid phone number/i,
72
+ )
73
+ expect(() => normalizePhoneOrThrow('++972')).toThrow(
74
+ /Invalid phone number/i,
75
+ )
76
+ })
77
+
78
+ it('should normalize a valid international number', () => {
79
+ const result = normalizePhoneOrThrowIntl('+972523444444')
80
+
81
+ expect(result).toMatchObject({
82
+ e164: '+972523444444',
83
+ national: expect.stringContaining('052'),
84
+ international: expect.stringContaining('+972'),
85
+ regionCode: 'IL',
86
+ type: expect.any(Number), // e.g. 1 = MOBILE
87
+ })
88
+ })
89
+ })
90
+
91
+ describe('phone normalization — no region (international) & with region', () => {
92
+ // Valid full international IL mobile number (E.164)
93
+ const intlIl = '+972523444444' // 052-344-4444
94
+
95
+ it('normalizePhoneOrThrowIntl: accepts full international number without region', () => {
96
+ const out = normalizePhoneOrThrowIntl(intlIl)
97
+ expect(out.e164).toBe(intlIl)
98
+ expect(out.regionCode).toBe('IL')
99
+ expect(out.international).toMatch(/^\+972/)
100
+ expect(typeof out.type).toBe('number')
101
+ })
102
+
103
+ it('normalizePhoneOrThrow (smart): accepts +972... without defaultRegion', () => {
104
+ const out = normalizePhoneOrThrow(intlIl) // no opts.defaultRegion
105
+ expect(out.e164).toBe(intlIl)
106
+ expect(out.regionCode).toBe('IL')
107
+ })
108
+
109
+ it('normalizePhoneOrThrowWithRegion: accepts national number with region', () => {
110
+ const out = normalizePhoneOrThrowWithRegion('052-344-4444', 'IL')
111
+ expect(out.e164).toBe(intlIl)
112
+ expect(out.regionCode).toBe('IL')
113
+ expect(out.national).toMatch(/052/)
114
+ })
115
+ })