@power-rent/phone-validation-adapter 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/LICENSE +21 -0
- package/README.md +459 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +76 -0
- package/dist/server.js.map +1 -0
- package/dist/twilioLookupValidation.d.ts +1 -0
- package/dist/twilioLookupValidation.js +46 -0
- package/dist/twilioLookupValidation.js.map +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +14 -0
- package/dist/utils.js.map +1 -0
- package/dist/validApiKeys.d.ts +2 -0
- package/dist/validApiKeys.js +6 -0
- package/dist/validApiKeys.js.map +1 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Power Rent
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# Phone Validation Adapter
|
|
2
|
+
|
|
3
|
+
REST API adapter for phone number validation using Twilio Lookup API. This project was created to solve a critical issue: legacy applications running on Node.js < 14 cannot directly install the Twilio SDK due to version incompatibility. This adapter provides a simple REST interface that allows legacy systems to validate phone numbers by calling this service, eliminating the need to upgrade their entire Node.js infrastructure.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- Node.js >= 18.0.0
|
|
10
|
+
- Twilio Account with API credentials
|
|
11
|
+
|
|
12
|
+
### Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Environment Variables
|
|
19
|
+
|
|
20
|
+
Create a `.env` file in the root directory:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
TWILIO_ACCOUNT_SID=your_account_sid
|
|
24
|
+
TWILIO_AUTH_TOKEN=your_auth_token
|
|
25
|
+
PORT=3000
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Important:** The `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` are required. Without them, the validation service will return HTTP 503.
|
|
29
|
+
|
|
30
|
+
### API Key Configuration
|
|
31
|
+
|
|
32
|
+
All requests to the validation endpoint require an API key and project name for authentication. API keys are stored in `src/validApiKeys.ts`:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const validApiKeys: Record<string, string> = {
|
|
36
|
+
RLC: process.env.API_KEY_RLC || '',
|
|
37
|
+
TJS: process.env.API_KEY_TJS || ''
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Project Names:**
|
|
42
|
+
- `RLC` - Power Rent Rental Company
|
|
43
|
+
- `TJS` - TJ Services
|
|
44
|
+
|
|
45
|
+
**To add a new project:**
|
|
46
|
+
|
|
47
|
+
1. Add environment variable to `.env`:
|
|
48
|
+
```
|
|
49
|
+
API_KEY_NEW_PROJECT="your-secret-api-key"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
2. Update `src/validApiKeys.ts`:
|
|
53
|
+
```typescript
|
|
54
|
+
const validApiKeys: Record<string, string> = {
|
|
55
|
+
RLC: process.env.API_KEY_RLC || '',
|
|
56
|
+
TJS: process.env.API_KEY_TJS || '',
|
|
57
|
+
NEW_PROJECT: process.env.API_KEY_NEW_PROJECT || ''
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
3. Rebuild and restart:
|
|
62
|
+
```bash
|
|
63
|
+
npm run build
|
|
64
|
+
npm run start
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Important Security Notes:**
|
|
68
|
+
- Never commit actual API keys to version control
|
|
69
|
+
- Store keys in environment variables or a secrets manager
|
|
70
|
+
- Each client application should have its own unique key
|
|
71
|
+
- Rotate keys regularly (every 90 days recommended)
|
|
72
|
+
|
|
73
|
+
## API Endpoints
|
|
74
|
+
|
|
75
|
+
### POST /validate
|
|
76
|
+
|
|
77
|
+
Validates a phone number using Twilio Lookup API.
|
|
78
|
+
|
|
79
|
+
**Authentication Required:** `x-api-key` and `x-project-name` headers with valid credentials.
|
|
80
|
+
|
|
81
|
+
#### Request
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
curl -X POST http://localhost:3000/validate \
|
|
85
|
+
-H "Content-Type: application/json" \
|
|
86
|
+
-H "x-api-key: apple-blossom-elephant-rocket" \
|
|
87
|
+
-H "x-project-name: RLC" \
|
|
88
|
+
-d '{"phoneNumber": "+1234567890"}'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Request body:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"phoneNumber": "+1234567890"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Request headers:
|
|
100
|
+
|
|
101
|
+
| Header | Value | Required | Description |
|
|
102
|
+
|--------|-------|----------|-------------|
|
|
103
|
+
| `x-api-key` | API Key | Yes | Project-specific API key for authentication |
|
|
104
|
+
| `x-project-name` | Project Name | Yes | Project identifier (RLC or TJS) |
|
|
105
|
+
| `Content-Type` | application/json | Yes | Request content type |
|
|
106
|
+
|
|
107
|
+
#### Response (Success - HTTP 200)
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"success": true,
|
|
112
|
+
"isValid": true
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Response (Invalid Number - HTTP 200)
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"success": true,
|
|
121
|
+
"isValid": false
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Response (Unauthorized - HTTP 401)
|
|
126
|
+
|
|
127
|
+
Returned when API key or project name is missing or invalid:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"success": false,
|
|
132
|
+
"error": "Invalid API key"
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Or:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"success": false,
|
|
141
|
+
"error": "Invalid project name"
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Response (Client Error - HTTP 400)
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"success": false,
|
|
150
|
+
"error": "phoneNumber is required and must be a non-empty string"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Response (Service Unavailable - HTTP 503)
|
|
155
|
+
|
|
156
|
+
Returned when Twilio credentials are not configured:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"success": false,
|
|
161
|
+
"error": "Validation service is temporarily unavailable"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Response (Server Error - HTTP 500)
|
|
166
|
+
|
|
167
|
+
Returned when Twilio API fails or times out:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"success": false,
|
|
172
|
+
"error": "Phone validation failed. Please try again later."
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Error Handling Flow
|
|
177
|
+
|
|
178
|
+
This diagram shows how errors are handled at different stages:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
POST /validate
|
|
182
|
+
│
|
|
183
|
+
├─ API Key Validation
|
|
184
|
+
│ ├─ x-project-name header missing?
|
|
185
|
+
│ │ └─ HTTP 401 (Unauthorized)
|
|
186
|
+
│ │ └─ Client error: invalid project name
|
|
187
|
+
│ │
|
|
188
|
+
│ ├─ x-project-name exists in whitelist?
|
|
189
|
+
│ │ └─ Continue to x-api-key validation
|
|
190
|
+
│ │
|
|
191
|
+
│ ├─ x-api-key header missing?
|
|
192
|
+
│ │ └─ HTTP 401 (Unauthorized)
|
|
193
|
+
│ │ └─ Client error: invalid API key
|
|
194
|
+
│ │
|
|
195
|
+
│ └─ x-api-key matches project?
|
|
196
|
+
│ └─ Continue to Input validation
|
|
197
|
+
│
|
|
198
|
+
├─ Input Validation
|
|
199
|
+
│ ├─ Missing or empty phoneNumber?
|
|
200
|
+
│ │ └─ HTTP 400 (Bad Request)
|
|
201
|
+
│ │ └─ Client error: invalid input
|
|
202
|
+
│ │
|
|
203
|
+
│ └─ phoneNumber is valid string?
|
|
204
|
+
│ └─ Continue to Twilio validation
|
|
205
|
+
│
|
|
206
|
+
├─ Configuration Check (twilioLookupValidation.ts)
|
|
207
|
+
│ ├─ TWILIO_ACCOUNT_SID missing?
|
|
208
|
+
│ │ ├─ Throw ConfigurationError
|
|
209
|
+
│ │ ├─ Log to Sentry
|
|
210
|
+
│ │ └─ server.ts catches it
|
|
211
|
+
│ │ └─ HTTP 503 (Service Unavailable)
|
|
212
|
+
│ │ └─ User sees: "Service temporarily unavailable"
|
|
213
|
+
│ │
|
|
214
|
+
│ ├─ TWILIO_AUTH_TOKEN missing?
|
|
215
|
+
│ │ ├─ Throw ConfigurationError
|
|
216
|
+
│ │ ├─ Log to Sentry
|
|
217
|
+
│ │ └─ server.ts catches it
|
|
218
|
+
│ │ └─ HTTP 503 (Service Unavailable)
|
|
219
|
+
│ │
|
|
220
|
+
│ └─ Credentials OK?
|
|
221
|
+
│ └─ Call Twilio API with 5s timeout
|
|
222
|
+
│
|
|
223
|
+
├─ Twilio API Call
|
|
224
|
+
│ ├─ Request times out (> 5s)?
|
|
225
|
+
│ │ ├─ Throw ValidationError
|
|
226
|
+
│ │ ├─ Log to Sentry
|
|
227
|
+
│ │ └─ server.ts catches it
|
|
228
|
+
│ │ └─ HTTP 500 (Internal Server Error)
|
|
229
|
+
│ │ └─ User sees: "Validation failed, try again"
|
|
230
|
+
│ │
|
|
231
|
+
│ ├─ Twilio API returns error?
|
|
232
|
+
│ │ ├─ Throw ValidationError
|
|
233
|
+
│ │ ├─ Log to Sentry
|
|
234
|
+
│ │ └─ server.ts catches it
|
|
235
|
+
│ │ └─ HTTP 500 (Internal Server Error)
|
|
236
|
+
│ │
|
|
237
|
+
│ ├─ Phone is valid?
|
|
238
|
+
│ │ └─ Return true
|
|
239
|
+
│ │ └─ HTTP 200 + isValid: true
|
|
240
|
+
│ │ └─ User can submit form
|
|
241
|
+
│ │
|
|
242
|
+
│ └─ Phone is invalid?
|
|
243
|
+
│ └─ Return false
|
|
244
|
+
│ └─ HTTP 200 + isValid: false
|
|
245
|
+
│ └─ User sees validation error message
|
|
246
|
+
│
|
|
247
|
+
└─ Error Logging (All paths)
|
|
248
|
+
└─ Sentry with error type tag
|
|
249
|
+
├─ ConfigurationError → errorType: 'configuration'
|
|
250
|
+
└─ ValidationError → errorType: 'validation'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Key Design Decisions
|
|
254
|
+
|
|
255
|
+
### 1. Explicit Error Types
|
|
256
|
+
|
|
257
|
+
Two custom error classes distinguish between different failure modes:
|
|
258
|
+
|
|
259
|
+
- **ConfigurationError**: Missing Twilio credentials (HTTP 503)
|
|
260
|
+
- **ValidationError**: Twilio API failures or timeouts (HTTP 500)
|
|
261
|
+
|
|
262
|
+
This prevents silent failures where configuration problems would be masked as valid numbers.
|
|
263
|
+
|
|
264
|
+
### 2. Timeout Protection
|
|
265
|
+
|
|
266
|
+
Twilio API calls have a 5-second timeout. If exceeded:
|
|
267
|
+
- ValidationError is thrown
|
|
268
|
+
- HTTP 500 is returned
|
|
269
|
+
- User is informed of the failure
|
|
270
|
+
|
|
271
|
+
### 3. No Silent Failures
|
|
272
|
+
|
|
273
|
+
Previous implementation returned `true` for missing credentials. This caused:
|
|
274
|
+
- Users to believe their phone was validated
|
|
275
|
+
- Form submissions with invalid phone numbers
|
|
276
|
+
- Data quality issues
|
|
277
|
+
|
|
278
|
+
Current implementation:
|
|
279
|
+
- Fails explicitly with HTTP 503
|
|
280
|
+
- Informs users the service is unavailable
|
|
281
|
+
- Prevents invalid data submission
|
|
282
|
+
|
|
283
|
+
## Architecture & Security
|
|
284
|
+
|
|
285
|
+
This adapter is designed as a **backend microservice** that bridges the gap between legacy applications and Twilio. Understanding the architecture is critical for proper deployment and security.
|
|
286
|
+
|
|
287
|
+
### Client-Server Architecture
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
┌─────────────────────────────────┐
|
|
291
|
+
│ Client Application │
|
|
292
|
+
│ (Frontend/Backend/Legacy app) │
|
|
293
|
+
└────────────┬────────────────────┘
|
|
294
|
+
│ HTTP POST /validate
|
|
295
|
+
│ with x-api-key header
|
|
296
|
+
↓
|
|
297
|
+
┌─────────────────────────────────────────────┐
|
|
298
|
+
│ Phone Validation Adapter │
|
|
299
|
+
│ (this service) │
|
|
300
|
+
│ ✓ Validates API key │
|
|
301
|
+
│ ✓ Validates phone number format │
|
|
302
|
+
│ ✓ Calls Twilio API │
|
|
303
|
+
│ ✓ Returns validation result │
|
|
304
|
+
└────────────┬────────────────────────────────┘
|
|
305
|
+
│
|
|
306
|
+
↓
|
|
307
|
+
┌───────────────────┐
|
|
308
|
+
│ Twilio API │
|
|
309
|
+
└───────────────────┘
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Backend Responsibilities
|
|
313
|
+
|
|
314
|
+
The backend service (this adapter) is responsible for:
|
|
315
|
+
|
|
316
|
+
1. **Store API Keys Securely**
|
|
317
|
+
|
|
318
|
+
API keys should **never** be hardcoded in production. Instead, use one of these approaches:
|
|
319
|
+
|
|
320
|
+
- **Environment Variables** (recommended for simple deployments):
|
|
321
|
+
```bash
|
|
322
|
+
VALID_API_KEYS=key1,key2,key3
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
- **Secrets Manager** (AWS Secrets Manager, HashiCorp Vault, etc.):
|
|
326
|
+
```typescript
|
|
327
|
+
// Pseudo-code
|
|
328
|
+
const keys = await secretsManager.getSecret('phone-validation-keys');
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
- **Database** (for dynamic key management):
|
|
332
|
+
```typescript
|
|
333
|
+
// Pseudo-code
|
|
334
|
+
const keys = await database.query('SELECT api_key FROM valid_keys WHERE active = true');
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
2. **Issue Keys to Clients**
|
|
338
|
+
|
|
339
|
+
Provide a secure endpoint where authenticated clients can request API keys:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// Pseudo-code example
|
|
343
|
+
app.post('/api-keys/request', (req, res) => {
|
|
344
|
+
// Verify client identity (JWT, OAuth, mTLS, etc.)
|
|
345
|
+
// Generate or retrieve API key
|
|
346
|
+
// Return key to client
|
|
347
|
+
res.json({ apiKey: 'generated-key' });
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Important:** This endpoint should:
|
|
352
|
+
- Require strong authentication (JWT, OAuth, mTLS)
|
|
353
|
+
- Rate limit requests
|
|
354
|
+
- Log all key requests
|
|
355
|
+
- Expire keys automatically
|
|
356
|
+
- Allow key rotation
|
|
357
|
+
|
|
358
|
+
3. **Validate Keys Before Processing**
|
|
359
|
+
|
|
360
|
+
Always verify API keys on the server before calling Twilio:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// Current implementation does this via middleware
|
|
364
|
+
const authenticateApiKey = (req, res, next) => {
|
|
365
|
+
const apiKey = req.headers['x-api-key'];
|
|
366
|
+
const projectName = req.headers['x-project-name'];
|
|
367
|
+
|
|
368
|
+
if (!checkIfKeyExists(projectName)) {
|
|
369
|
+
res.status(401).json({ error: 'Invalid project name' });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!isValidApiKey({ key: apiKey, projectName })) {
|
|
374
|
+
res.status(401).json({ error: 'Invalid API key' });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
next();
|
|
378
|
+
};
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Benefits:
|
|
382
|
+
- Keys never exposed to clients
|
|
383
|
+
- Project-specific key validation
|
|
384
|
+
- Prevents unauthorized Twilio API calls
|
|
385
|
+
- Reduces costs (Twilio charges per request)
|
|
386
|
+
- Enables per-project rate limiting
|
|
387
|
+
- Audit trail for all validation attempts
|
|
388
|
+
|
|
389
|
+
### Frontend/Client Responsibilities
|
|
390
|
+
|
|
391
|
+
Client applications should:
|
|
392
|
+
|
|
393
|
+
1. **Never hardcode API keys** in frontend code
|
|
394
|
+
2. **Request keys from backend** via secure endpoint
|
|
395
|
+
3. **Pass keys in request headers** (`x-api-key`)
|
|
396
|
+
4. **Handle 401 responses** gracefully
|
|
397
|
+
5. **Implement retry logic** for transient failures
|
|
398
|
+
|
|
399
|
+
Example client usage:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// Get API key from backend (must be authenticated)
|
|
403
|
+
const apiKey = await fetch('/api/phone-validation-key', {
|
|
404
|
+
headers: { Authorization: 'Bearer ' + userToken }
|
|
405
|
+
}).then(r => r.json()).then(r => r.apiKey);
|
|
406
|
+
|
|
407
|
+
// Get project name from backend config or user settings
|
|
408
|
+
const projectName = 'RLC'; // or 'TJS'
|
|
409
|
+
|
|
410
|
+
// Use key to validate phone
|
|
411
|
+
const result = await fetch('https://validator-service.example.com/validate', {
|
|
412
|
+
method: 'POST',
|
|
413
|
+
headers: {
|
|
414
|
+
'Content-Type': 'application/json',
|
|
415
|
+
'x-api-key': apiKey,
|
|
416
|
+
'x-project-name': projectName
|
|
417
|
+
},
|
|
418
|
+
body: JSON.stringify({ phoneNumber: '+1234567890' })
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Security Best Practices
|
|
423
|
+
|
|
424
|
+
1. **Rotate API Keys Regularly** - Change keys every 90 days
|
|
425
|
+
2. **Use HTTPS** - All communication must be encrypted
|
|
426
|
+
3. **Rate Limit** - Prevent abuse of the validation endpoint
|
|
427
|
+
4. **Monitor Usage** - Track validation requests and costs
|
|
428
|
+
5. **Separate Credentials** - Twilio keys ≠ Adapter keys
|
|
429
|
+
6. **Audit Logging** - Log all API key usage
|
|
430
|
+
7. **Principle of Least Privilege** - Give clients minimum permissions needed
|
|
431
|
+
|
|
432
|
+
## Development
|
|
433
|
+
|
|
434
|
+
### Start Development Server
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
npm run dev
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Server runs on `http://localhost:3000` by default.
|
|
441
|
+
|
|
442
|
+
### Build
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
npm run build
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Output goes to `dist/` directory.
|
|
449
|
+
|
|
450
|
+
### Testing
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
npm run lint
|
|
454
|
+
npm run format:check
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## License
|
|
458
|
+
|
|
459
|
+
MIT
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "dotenv/config";
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { twilioLookupValidation } from "./twilioLookupValidation.js";
|
|
4
|
+
import isValidApiKey, { checkIfKeyExists } from "./utils.js";
|
|
5
|
+
const app = express();
|
|
6
|
+
const PORT = process.env.PORT || 3000;
|
|
7
|
+
app.use(express.json());
|
|
8
|
+
const authenticateApiKey = (req, res, next) => {
|
|
9
|
+
const apiKey = req.headers["x-api-key"];
|
|
10
|
+
const projectName = req.headers["x-project-name"];
|
|
11
|
+
if (!checkIfKeyExists(projectName)) {
|
|
12
|
+
res.status(401).json({
|
|
13
|
+
success: false,
|
|
14
|
+
error: "Invalid project name"
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!isValidApiKey({ key: apiKey, projectName })) {
|
|
19
|
+
res.status(401).json({
|
|
20
|
+
success: false,
|
|
21
|
+
error: "Invalid API key"
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
next();
|
|
26
|
+
};
|
|
27
|
+
const validatePhoneNumber = (req, res, next) => {
|
|
28
|
+
const body = req.body;
|
|
29
|
+
const phoneNumber = body?.phoneNumber;
|
|
30
|
+
if (!phoneNumber || typeof phoneNumber !== "string" || phoneNumber.trim().length === 0) {
|
|
31
|
+
res.status(400).json({
|
|
32
|
+
success: false,
|
|
33
|
+
error: "phoneNumber is required and must be a non-empty string"
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
next();
|
|
38
|
+
};
|
|
39
|
+
app.post("/validate", authenticateApiKey, validatePhoneNumber, async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const body = req.body;
|
|
42
|
+
const phoneNumber = body.phoneNumber;
|
|
43
|
+
const isValid = await twilioLookupValidation(phoneNumber);
|
|
44
|
+
res.json({
|
|
45
|
+
success: true,
|
|
46
|
+
isValid
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
51
|
+
const errorName = error instanceof Error ? error.name : "Error";
|
|
52
|
+
console.error("Error in /validate route:", { name: errorName, message: errorMessage });
|
|
53
|
+
if (errorName === "ConfigurationError") {
|
|
54
|
+
res.status(503).json({
|
|
55
|
+
success: false,
|
|
56
|
+
error: "Validation service is temporarily unavailable"
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
res.status(500).json({
|
|
61
|
+
success: false,
|
|
62
|
+
error: "Phone validation failed. Please try again later."
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
app.use((err, _req, res, _next) => {
|
|
67
|
+
console.error("Middleware error:", err);
|
|
68
|
+
res.status(400).json({
|
|
69
|
+
success: false,
|
|
70
|
+
error: "Invalid JSON in request body"
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
app.listen(PORT, () => {
|
|
74
|
+
console.log(`Server is running on port ${PORT}`);
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const twilioLookupValidation: (phoneNumber: string) => Promise<boolean>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import twilio from "twilio";
|
|
2
|
+
import * as Sentry from "@sentry/node";
|
|
3
|
+
class ConfigurationError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ConfigurationError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
class ValidationError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "ValidationError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export const twilioLookupValidation = async (phoneNumber) => {
|
|
16
|
+
try {
|
|
17
|
+
const accountSid = process.env.TWILIO_ACCOUNT_SID;
|
|
18
|
+
const authToken = process.env.TWILIO_AUTH_TOKEN;
|
|
19
|
+
if (!accountSid || !authToken) {
|
|
20
|
+
const error = new ConfigurationError("TWILIO_ACCOUNT_SID or TWILIO_AUTH_TOKEN is not set");
|
|
21
|
+
console.error("ConfigurationError in twilioLookupValidation", error);
|
|
22
|
+
Sentry.captureException(error, {
|
|
23
|
+
tags: { errorType: "configuration", service: "twilio" }
|
|
24
|
+
});
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
const client = twilio(accountSid, authToken);
|
|
28
|
+
const phone = phoneNumber.trim();
|
|
29
|
+
const response = await client.lookups.v2.phoneNumbers(phone).fetch();
|
|
30
|
+
return response.valid;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof ConfigurationError) {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
const validationError = error instanceof ValidationError
|
|
37
|
+
? error
|
|
38
|
+
: new ValidationError(error instanceof Error ? error.message : "Unknown error during phone validation");
|
|
39
|
+
console.error("ValidationError in twilioLookupValidation", validationError);
|
|
40
|
+
Sentry.captureException(validationError, {
|
|
41
|
+
tags: { errorType: "validation", service: "twilio" }
|
|
42
|
+
});
|
|
43
|
+
throw validationError;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=twilioLookupValidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilioLookupValidation.js","sourceRoot":"","sources":["../src/twilioLookupValidation.ts"],"names":[],"mappings":"AAEA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AAkBvC,MAAM,kBAAmB,SAAQ,KAAK;IACpC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,MAAM,eAAgB,SAAQ,KAAK;IACjC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,EAAE,WAAmB,EAAoB,EAAE;IACpF,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;QAClD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QAEhD,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,IAAI,kBAAkB,CAAC,oDAAoD,CAAC,CAAC;YAE3F,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;YACrE,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE;gBAC7B,IAAI,EAAE,EAAE,SAAS,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE;aACxD,CAAC,CAAC;YACH,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;QAEjC,MAAM,QAAQ,GAAyB,MAAM,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC;QAE3F,OAAO,QAAQ,CAAC,KAAK,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,kBAAkB,EAAE,CAAC;YACxC,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,eAAe,GACnB,KAAK,YAAY,eAAe;YAC9B,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,IAAI,eAAe,CACjB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,CACjF,CAAC;QAGR,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,eAAe,CAAC,CAAC;QAC5E,MAAM,CAAC,gBAAgB,CAAC,eAAe,EAAE;YACvC,IAAI,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE;SACrD,CAAC,CAAC;QACH,MAAM,eAAe,CAAC;IACxB,CAAC;AACH,CAAC,CAAC"}
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import validApiKeys from "./validApiKeys.js";
|
|
2
|
+
export const checkIfKeyExists = (key) => {
|
|
3
|
+
return Object.keys(validApiKeys).includes(key);
|
|
4
|
+
};
|
|
5
|
+
const isValidApiKey = ({ key, projectName }) => {
|
|
6
|
+
if (!key || !projectName)
|
|
7
|
+
return false;
|
|
8
|
+
const isValidProjectName = Object.keys(validApiKeys).includes(projectName);
|
|
9
|
+
if (!isValidProjectName)
|
|
10
|
+
return false;
|
|
11
|
+
return validApiKeys[projectName] === key;
|
|
12
|
+
};
|
|
13
|
+
export default isValidApiKey;
|
|
14
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,mBAAmB,CAAC;AAE7C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAW,EAAW,EAAE;IACvD,OAAO,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACjD,CAAC,CAAC;AAOF,MAAM,aAAa,GAAG,CAAC,EAAE,GAAG,EAAE,WAAW,EAAQ,EAAW,EAAE;IAC5D,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW;QAAE,OAAO,KAAK,CAAC;IACvC,MAAM,kBAAkB,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC3E,IAAI,CAAC,kBAAkB;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,YAAY,CAAC,WAAW,CAAC,KAAK,GAAG,CAAC;AAC3C,CAAC,CAAC;AAEF,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validApiKeys.js","sourceRoot":"","sources":["../src/validApiKeys.ts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAA2B;IAC3C,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE;IAClC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE;CACnC,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@power-rent/phone-validation-adapter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "REST API for phone number validation using Twilio Lookup API",
|
|
5
|
+
"main": "./dist/server.js",
|
|
6
|
+
"types": "./dist/server.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "npm run clean && tsc",
|
|
18
|
+
"start": "node dist/server.js",
|
|
19
|
+
"dev": "tsx watch src/server.ts",
|
|
20
|
+
"test": "vitest",
|
|
21
|
+
"test:ui": "vitest --ui",
|
|
22
|
+
"test:run": "vitest run",
|
|
23
|
+
"test:coverage": "vitest run --coverage",
|
|
24
|
+
"lint": "eslint 'src/**/*.{ts,tsx}'",
|
|
25
|
+
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
|
|
26
|
+
"format": "prettier --write 'src/**/*.{ts,tsx}'",
|
|
27
|
+
"format:check": "prettier --check 'src/**/*.{ts,tsx}'",
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"changeset": "changeset",
|
|
30
|
+
"version": "changeset version",
|
|
31
|
+
"release": "npm run build && changeset publish"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"phone",
|
|
35
|
+
"validation",
|
|
36
|
+
"adapter",
|
|
37
|
+
"twilio",
|
|
38
|
+
"e164",
|
|
39
|
+
"telephone",
|
|
40
|
+
"validator",
|
|
41
|
+
"strategy-pattern"
|
|
42
|
+
],
|
|
43
|
+
"author": "",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/power-rent/phone-validation-adapter.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/power-rent/phone-validation-adapter/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/power-rent/phone-validation-adapter#readme",
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@changesets/cli": "^2.29.8",
|
|
58
|
+
"@types/express": "^5.0.6",
|
|
59
|
+
"@types/node": "^20.0.0",
|
|
60
|
+
"@types/supertest": "^6.0.3",
|
|
61
|
+
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
|
62
|
+
"@typescript-eslint/parser": "^8.55.0",
|
|
63
|
+
"eslint": "^8.0.0",
|
|
64
|
+
"prettier": "^3.0.0",
|
|
65
|
+
"supertest": "^7.2.2",
|
|
66
|
+
"tsx": "^4.0.0",
|
|
67
|
+
"typescript": "^5.0.0",
|
|
68
|
+
"vitest": "^4.0.18"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@sentry/node": "^7.120.0",
|
|
72
|
+
"dotenv": "^16.4.5",
|
|
73
|
+
"express": "^5.2.1",
|
|
74
|
+
"twilio": "^3.84.1"
|
|
75
|
+
}
|
|
76
|
+
}
|