core-services-sdk 1.3.9 → 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.9",
3
+ "version": "1.3.11",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -23,9 +23,9 @@
23
23
  "@aws-sdk/credential-provider-node": "^3.862.0",
24
24
  "@sendgrid/mail": "^8.1.5",
25
25
  "amqplib": "^0.10.8",
26
- "crypto": "^1.0.1",
27
26
  "dot": "^1.1.3",
28
27
  "fastify": "^5.4.0",
28
+ "google-libphonenumber": "^3.2.42",
29
29
  "http-status": "^2.1.0",
30
30
  "mongodb": "^6.18.0",
31
31
  "node-fetch": "^3.3.2",
@@ -40,4 +40,4 @@
40
40
  "url": "^0.11.4",
41
41
  "vitest": "^3.2.4"
42
42
  }
43
- }
43
+ }
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
+ })
@@ -1,9 +1,7 @@
1
+ // @ts-nocheck
1
2
  import nodemailer from 'nodemailer'
2
- import { describe, it, expect, vi, beforeEach } from 'vitest'
3
3
 
4
- // ===================
5
- // SAFELY MOCK MODULES
6
- // ===================
4
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
7
5
 
8
6
  vi.mock('nodemailer', () => {
9
7
  const createTransport = vi.fn((options) => ({
@@ -17,23 +15,24 @@ vi.mock('nodemailer', () => {
17
15
  }
18
16
  })
19
17
 
20
- vi.mock('nodemailer-sendgrid-transport', () => ({
21
- __esModule: true,
22
- default: vi.fn((opts) => ({
23
- sendgrid: true,
24
- options: opts,
25
- })),
26
- }))
18
+ vi.mock('@sendgrid/mail', () => {
19
+ const setApiKey = vi.fn()
20
+ const send = vi.fn()
21
+ const sgMail = { setApiKey, send }
22
+ globalThis.__mockedSgMail__ = sgMail
23
+ return {
24
+ __esModule: true,
25
+ default: sgMail,
26
+ }
27
+ })
27
28
 
28
29
  vi.mock('@aws-sdk/client-ses', () => {
29
- const mockSesInstance = { mocked: true } // consistent reference
30
- const sesClient = vi.fn(() => mockSesInstance)
31
-
32
- globalThis.__mockedSesClient__ = sesClient
30
+ const mockSesInstance = { mocked: true }
31
+ const SESClient = vi.fn(() => mockSesInstance)
32
+ globalThis.__mockedSesClient__ = SESClient
33
33
  globalThis.__mockedSesInstance__ = mockSesInstance
34
-
35
34
  return {
36
- SESClient: sesClient,
35
+ SESClient,
37
36
  SendRawEmailCommand: 'mocked-command',
38
37
  }
39
38
  })
@@ -41,15 +40,9 @@ vi.mock('@aws-sdk/client-ses', () => {
41
40
  vi.mock('@aws-sdk/credential-provider-node', () => {
42
41
  const defaultProvider = vi.fn(() => 'mocked-default-provider')
43
42
  globalThis.__mockedDefaultProvider__ = defaultProvider
44
- return {
45
- defaultProvider,
46
- }
43
+ return { defaultProvider }
47
44
  })
48
45
 
49
- // ==========================
50
- // NOW IMPORT MODULE UNDER TEST
51
- // ==========================
52
-
53
46
  import { TransportFactory } from '../../src/mailer/transport.factory.js'
54
47
 
55
48
  describe('TransportFactory', () => {
@@ -66,7 +59,6 @@ describe('TransportFactory', () => {
66
59
  auth: { user: 'user', pass: 'pass' },
67
60
  }
68
61
 
69
- // @ts-ignore
70
62
  const transport = TransportFactory.create(config)
71
63
 
72
64
  expect(nodemailer.createTransport).toHaveBeenCalledWith({
@@ -75,8 +67,6 @@ describe('TransportFactory', () => {
75
67
  secure: config.secure,
76
68
  auth: config.auth,
77
69
  })
78
-
79
- // @ts-ignore
80
70
  expect(transport.type).toBe('mock-transport')
81
71
  })
82
72
 
@@ -86,46 +76,33 @@ describe('TransportFactory', () => {
86
76
  auth: { user: 'user@gmail.com', pass: 'pass' },
87
77
  }
88
78
 
89
- // @ts-ignore
90
79
  const transport = TransportFactory.create(config)
91
80
 
92
81
  expect(nodemailer.createTransport).toHaveBeenCalledWith({
93
82
  service: 'gmail',
94
83
  auth: config.auth,
95
84
  })
96
-
97
- // @ts-ignore
98
85
  expect(transport.type).toBe('mock-transport')
99
86
  })
100
87
 
101
88
  it('should create sendgrid transport', () => {
102
89
  const config = {
103
90
  type: 'sendgrid',
104
- apiKey: 'SG.xxxx',
91
+ apiKey: 'SG.key',
105
92
  }
106
93
 
107
- // @ts-ignore
108
94
  const transport = TransportFactory.create(config)
109
95
 
110
- // @ts-ignore
111
- const args = nodemailer.createTransport.mock.calls[0][0]
112
-
113
- expect(args).toEqual({
114
- sendgrid: true,
115
- options: {
116
- auth: { api_key: config.apiKey },
117
- },
118
- })
96
+ expect(globalThis.__mockedSgMail__.setApiKey).toHaveBeenCalledWith(
97
+ config.apiKey,
98
+ )
99
+ expect(transport).toEqual(globalThis.__mockedSgMail__)
100
+ })
119
101
 
120
- expect(transport).toEqual({
121
- type: 'mock-transport',
122
- options: {
123
- sendgrid: true,
124
- options: {
125
- auth: { api_key: config.apiKey },
126
- },
127
- },
128
- })
102
+ it('should throw if sendgrid API key is missing', () => {
103
+ expect(() => {
104
+ TransportFactory.create({ type: 'sendgrid' })
105
+ }).toThrow('Missing SendGrid API key')
129
106
  })
130
107
 
131
108
  it('should create ses transport with credentials', () => {
@@ -136,7 +113,6 @@ describe('TransportFactory', () => {
136
113
  region: 'us-west-2',
137
114
  }
138
115
 
139
- // @ts-ignore
140
116
  const transport = TransportFactory.create(config)
141
117
 
142
118
  expect(globalThis.__mockedSesClient__).toHaveBeenCalledWith({
@@ -147,19 +123,14 @@ describe('TransportFactory', () => {
147
123
  },
148
124
  })
149
125
 
150
- // @ts-ignore
151
126
  const args = nodemailer.createTransport.mock.calls[0][0]
152
-
153
127
  expect(args).toEqual({
154
128
  SES: {
155
129
  ses: globalThis.__mockedSesInstance__,
156
- aws: {
157
- SendRawEmailCommand: 'mocked-command',
158
- },
130
+ aws: { SendRawEmailCommand: 'mocked-command' },
159
131
  },
160
132
  })
161
133
 
162
- // @ts-ignore
163
134
  expect(transport.type).toBe('mock-transport')
164
135
  })
165
136
 
@@ -169,35 +140,27 @@ describe('TransportFactory', () => {
169
140
  region: 'us-east-1',
170
141
  }
171
142
 
172
- // @ts-ignore
173
143
  const transport = TransportFactory.create(config)
174
144
 
175
145
  expect(globalThis.__mockedDefaultProvider__).toHaveBeenCalled()
176
-
177
146
  expect(globalThis.__mockedSesClient__).toHaveBeenCalledWith({
178
147
  region: config.region,
179
148
  credentials: 'mocked-default-provider',
180
149
  })
181
150
 
182
- // @ts-ignore
183
151
  const args = nodemailer.createTransport.mock.calls[0][0]
184
-
185
152
  expect(args).toEqual({
186
153
  SES: {
187
154
  ses: globalThis.__mockedSesInstance__,
188
- aws: {
189
- SendRawEmailCommand: 'mocked-command',
190
- },
155
+ aws: { SendRawEmailCommand: 'mocked-command' },
191
156
  },
192
157
  })
193
158
 
194
- // @ts-ignore
195
159
  expect(transport.type).toBe('mock-transport')
196
160
  })
197
161
 
198
162
  it('should throw error for unsupported type', () => {
199
163
  expect(() => {
200
- // @ts-ignore
201
164
  TransportFactory.create({ type: 'invalid' })
202
165
  }).toThrow('Unsupported transport type: invalid')
203
166
  })