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.
|
|
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
|
@@ -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('
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 }
|
|
30
|
-
const
|
|
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
|
|
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.
|
|
91
|
+
apiKey: 'SG.key',
|
|
105
92
|
}
|
|
106
93
|
|
|
107
|
-
// @ts-ignore
|
|
108
94
|
const transport = TransportFactory.create(config)
|
|
109
95
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
expect(
|
|
114
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
})
|