sms-verification-api 0.9.1
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/.env.example +20 -0
- package/DEPLOYMENT.md +151 -0
- package/README.md +475 -0
- package/docs/app/(home)/layout.tsx +7 -0
- package/docs/app/(home)/page.tsx +38 -0
- package/docs/app/docs/[[...slug]]/page.tsx +59 -0
- package/docs/app/docs/layout.tsx +12 -0
- package/docs/app/docs-og/[...slug]/route.ts +24 -0
- package/docs/app/globals.css +587 -0
- package/docs/app/layout.config.tsx +13 -0
- package/docs/app/layout.tsx +27 -0
- package/docs/app/logo.tsx +35 -0
- package/docs/content/docs/API_AUTHENTICATION.md +91 -0
- package/docs/content/docs/DEPLOYMENT.md +181 -0
- package/docs/content/docs/api/post.mdx +35 -0
- package/docs/content/docs/api/verify.mdx +34 -0
- package/docs/content/docs/meta.json +8 -0
- package/docs/content/docs/verify-legal-name.md +339 -0
- package/docs/lib/source.ts +14 -0
- package/docs/mdx-components.tsx +12 -0
- package/docs/next.config.mjs +51 -0
- package/docs/openapi.json +329 -0
- package/docs/package.json +37 -0
- package/docs/postcss.config.mjs +5 -0
- package/docs/scripts/generate-docs.mjs +23 -0
- package/docs/source.config.ts +5 -0
- package/docs/tsconfig.json +29 -0
- package/docs/worker.js +35 -0
- package/docs/wrangler.toml +26 -0
- package/examples/client.js +119 -0
- package/examples/demo.html +325 -0
- package/examples/libphonenumber-example.js +120 -0
- package/openapi.json +329 -0
- package/package.json +71 -0
- package/scripts/deploy.sh +63 -0
- package/src/identity-verification-server.ts +553 -0
- package/src/index.js +8 -0
- package/src/sns.js +236 -0
- package/src/verify-phone-server.js +448 -0
- package/src/verify-phone.ts +551 -0
- package/test/api.test.js +201 -0
- package/test/integration.test.js +152 -0
- package/test/metadata-test.js +73 -0
- package/test/server.test.js +143 -0
- package/test/setup.js +32 -0
- package/test/utils.test.js +186 -0
- package/test/verify.test.js +23 -0
- package/test/voip.test.js +112 -0
- package/vitest.config.js +10 -0
- package/wrangler.toml +27 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Import utility functions from the main file
|
|
4
|
+
// We'll need to extract these functions or test them indirectly
|
|
5
|
+
// For now, let's test the phone number validation logic
|
|
6
|
+
|
|
7
|
+
describe('Utility Functions', () => {
|
|
8
|
+
describe('Phone Number Validation', () => {
|
|
9
|
+
const isValidPhoneNumber = (phone) => {
|
|
10
|
+
const phoneRegex = /^\+[1-9]\d{1,14}$/;
|
|
11
|
+
return phoneRegex.test(phone) && phone.length >= 7 && phone.length <= 16;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const formatPhoneNumber = (phone) => {
|
|
15
|
+
// Remove all non-digit characters
|
|
16
|
+
const cleaned = phone.replace(/\D/g, '');
|
|
17
|
+
|
|
18
|
+
// Add country code if not present (assuming US)
|
|
19
|
+
if (cleaned.length === 10) {
|
|
20
|
+
return `+1${cleaned}`;
|
|
21
|
+
} else if (cleaned.length === 11 && cleaned.startsWith('1')) {
|
|
22
|
+
return `+${cleaned}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Return as is if it already looks like E.164
|
|
26
|
+
return phone.startsWith('+') ? phone : `+${cleaned}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('isValidPhoneNumber', () => {
|
|
30
|
+
it('should validate correct E.164 phone numbers', () => {
|
|
31
|
+
const validNumbers = [
|
|
32
|
+
'+1234567890',
|
|
33
|
+
'+11234567890',
|
|
34
|
+
'+44123456789',
|
|
35
|
+
'+61412345678',
|
|
36
|
+
'+123456789012345'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
validNumbers.forEach(number => {
|
|
40
|
+
expect(isValidPhoneNumber(number)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should reject invalid phone numbers', () => {
|
|
45
|
+
const invalidNumbers = [
|
|
46
|
+
'1234567890', // Missing +
|
|
47
|
+
'+', // Just plus sign
|
|
48
|
+
'+123', // Too short
|
|
49
|
+
'+12345678901234567890', // Too long
|
|
50
|
+
'+01234567890', // Starts with 0
|
|
51
|
+
'not-a-number',
|
|
52
|
+
'+abc123def'
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
invalidNumbers.forEach(number => {
|
|
56
|
+
expect(isValidPhoneNumber(number)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('formatPhoneNumber', () => {
|
|
62
|
+
it('should format 10-digit US numbers correctly', () => {
|
|
63
|
+
const testCases = [
|
|
64
|
+
{ input: '1234567890', expected: '+11234567890' },
|
|
65
|
+
{ input: '(123) 456-7890', expected: '+11234567890' },
|
|
66
|
+
{ input: '123-456-7890', expected: '+11234567890' },
|
|
67
|
+
{ input: '123.456.7890', expected: '+11234567890' },
|
|
68
|
+
{ input: '123 456 7890', expected: '+11234567890' }
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
testCases.forEach(({ input, expected }) => {
|
|
72
|
+
expect(formatPhoneNumber(input)).toBe(expected);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should format 11-digit US numbers correctly', () => {
|
|
77
|
+
const testCases = [
|
|
78
|
+
{ input: '11234567890', expected: '+11234567890' },
|
|
79
|
+
{ input: '1-123-456-7890', expected: '+11234567890' },
|
|
80
|
+
{ input: '1 (123) 456-7890', expected: '+11234567890' }
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
testCases.forEach(({ input, expected }) => {
|
|
84
|
+
expect(formatPhoneNumber(input)).toBe(expected);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should preserve already formatted E.164 numbers', () => {
|
|
89
|
+
const testCases = [
|
|
90
|
+
'+11234567890', // Already formatted correctly
|
|
91
|
+
'+44123456789',
|
|
92
|
+
'+61412345678'
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
testCases.forEach(number => {
|
|
96
|
+
expect(formatPhoneNumber(number)).toBe(number);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle edge cases', () => {
|
|
101
|
+
const testCases = [
|
|
102
|
+
{ input: '', expected: '+' },
|
|
103
|
+
{ input: 'abc', expected: '+' },
|
|
104
|
+
{ input: '123abc456', expected: '+123456' }
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
testCases.forEach(({ input, expected }) => {
|
|
108
|
+
expect(formatPhoneNumber(input)).toBe(expected);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('Combined Validation and Formatting', () => {
|
|
114
|
+
it('should format and validate phone numbers correctly', () => {
|
|
115
|
+
const testCases = [
|
|
116
|
+
{ input: '1234567890', formatted: '+11234567890', valid: true },
|
|
117
|
+
{ input: '(123) 456-7890', formatted: '+11234567890', valid: true },
|
|
118
|
+
{ input: '123-456-7890', formatted: '+11234567890', valid: true },
|
|
119
|
+
{ input: '+11234567890', formatted: '+11234567890', valid: true },
|
|
120
|
+
{ input: 'not-a-number', formatted: '+', valid: false },
|
|
121
|
+
{ input: '123', formatted: '+123', valid: false },
|
|
122
|
+
{ input: '12345678901234567890', formatted: '+12345678901234567890', valid: false }
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
testCases.forEach(({ input, formatted, valid }) => {
|
|
126
|
+
const formattedResult = formatPhoneNumber(input);
|
|
127
|
+
expect(formattedResult).toBe(formatted);
|
|
128
|
+
expect(isValidPhoneNumber(formattedResult)).toBe(valid);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('Verification Code Generation', () => {
|
|
135
|
+
const generateVerificationCode = () => {
|
|
136
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
137
|
+
let result = '';
|
|
138
|
+
for (let i = 0; i < 6; i++) {
|
|
139
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
it('should generate 6-character alphanumeric codes', () => {
|
|
145
|
+
for (let i = 0; i < 100; i++) {
|
|
146
|
+
const code = generateVerificationCode();
|
|
147
|
+
expect(code).toMatch(/^[A-Z0-9]{6}$/);
|
|
148
|
+
expect(code.length).toBe(6);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should generate codes with only uppercase letters and digits', () => {
|
|
153
|
+
for (let i = 0; i < 100; i++) {
|
|
154
|
+
const code = generateVerificationCode();
|
|
155
|
+
expect(code).toMatch(/^[A-Z0-9]+$/);
|
|
156
|
+
expect(code).not.toMatch(/[a-z]/); // No lowercase letters
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should generate different codes on multiple calls', () => {
|
|
161
|
+
const codes = new Set();
|
|
162
|
+
for (let i = 0; i < 100; i++) {
|
|
163
|
+
codes.add(generateVerificationCode());
|
|
164
|
+
}
|
|
165
|
+
// With 100 calls, we should have at least 90 unique codes
|
|
166
|
+
// (allowing for some randomness overlap)
|
|
167
|
+
expect(codes.size).toBeGreaterThan(90);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('UUID Generation', () => {
|
|
172
|
+
it('should generate valid UUIDs', () => {
|
|
173
|
+
const uuid = crypto.randomUUID();
|
|
174
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
175
|
+
expect(uuid).toMatch(uuidRegex);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should generate unique UUIDs', () => {
|
|
179
|
+
const uuids = new Set();
|
|
180
|
+
for (let i = 0; i < 100; i++) {
|
|
181
|
+
uuids.add(crypto.randomUUID());
|
|
182
|
+
}
|
|
183
|
+
expect(uuids.size).toBe(100);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import verifyPhone from '../src/verify-phone.js';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
// Basic usage with custom code
|
|
7
|
+
const result = await verifyPhone({
|
|
8
|
+
phoneNumber: '+1234567890',
|
|
9
|
+
code: 'ABC123',
|
|
10
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
11
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
12
|
+
awsRegion: 'us-east-1',
|
|
13
|
+
blockVoip: true,
|
|
14
|
+
senderId: 'MyApp',
|
|
15
|
+
messageTemplate: 'MyApp: Your code is {code}. Valid for 10 minutes.'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (result.success) {
|
|
19
|
+
console.log('SMS sent successfully:', result.messageId);
|
|
20
|
+
console.log('Code sent:', result.code);
|
|
21
|
+
} else {
|
|
22
|
+
console.error('Failed to send SMS:', result.error);
|
|
23
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createApp } from '../src/verify-phone-server.js';
|
|
3
|
+
|
|
4
|
+
describe('VoIP Blocking Functionality', () => {
|
|
5
|
+
let app;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
app = createApp(globalThis.env);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('blockVoip parameter', () => {
|
|
12
|
+
it('should allow requests when blockVoip is not specified', async () => {
|
|
13
|
+
const req = new Request('http://localhost/api/send-verification?phoneNumber=%2B1234567890', {
|
|
14
|
+
headers: {
|
|
15
|
+
'X-API-Key': 'test-api-key'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const res = await app.request(req, globalThis.env);
|
|
19
|
+
const data = await res.json();
|
|
20
|
+
|
|
21
|
+
expect(res.status).toBe(200);
|
|
22
|
+
expect(data.success).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should allow requests when blockVoip is false', async () => {
|
|
26
|
+
const req = new Request('http://localhost/api/send-verification?phoneNumber=%2B1234567890&blockVoip=false', {
|
|
27
|
+
headers: {
|
|
28
|
+
'X-API-Key': 'test-api-key'
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const res = await app.request(req, globalThis.env);
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
|
|
34
|
+
expect(res.status).toBe(200);
|
|
35
|
+
expect(data.success).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should accept blockVoip=true parameter', async () => {
|
|
39
|
+
const req = new Request('http://localhost/api/send-verification?phoneNumber=%2B1234567890&blockVoip=true', {
|
|
40
|
+
headers: {
|
|
41
|
+
'X-API-Key': 'test-api-key'
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const res = await app.request(req, globalThis.env);
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
|
|
47
|
+
expect(res.status).toBe(200);
|
|
48
|
+
expect(data.success).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('VoIP detection logic', () => {
|
|
53
|
+
it('should correctly identify VoIP numbers', async () => {
|
|
54
|
+
// Import the function from the module
|
|
55
|
+
const module = await import('../src/verify-phone-server.js');
|
|
56
|
+
const { isPhoneNumberVoip } = module;
|
|
57
|
+
|
|
58
|
+
// Mock the grab function to return VoIP data
|
|
59
|
+
const originalGrab = globalThis.grab;
|
|
60
|
+
globalThis.grab = {
|
|
61
|
+
instance: () => ({
|
|
62
|
+
"phone-lookup": async () => ({
|
|
63
|
+
carrier: {
|
|
64
|
+
name: 'Bandwidth',
|
|
65
|
+
type: 'voip'
|
|
66
|
+
},
|
|
67
|
+
portability: {
|
|
68
|
+
line_type: 'landline'
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Test VoIP number
|
|
75
|
+
const isVoip = await isPhoneNumberVoip('+1234567890');
|
|
76
|
+
expect(isVoip).toBe(true);
|
|
77
|
+
|
|
78
|
+
// Mock the grab function to return mobile data
|
|
79
|
+
globalThis.grab = {
|
|
80
|
+
instance: () => ({
|
|
81
|
+
"phone-lookup": async () => ({
|
|
82
|
+
carrier: {
|
|
83
|
+
name: 'Verizon Wireless',
|
|
84
|
+
type: 'mobile'
|
|
85
|
+
},
|
|
86
|
+
portability: {
|
|
87
|
+
line_type: 'mobile'
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Test mobile number
|
|
94
|
+
const isMobile = await isPhoneNumberVoip('+1234567890');
|
|
95
|
+
expect(isMobile).toBe(false);
|
|
96
|
+
|
|
97
|
+
// Mock the grab function to return null data
|
|
98
|
+
globalThis.grab = {
|
|
99
|
+
instance: () => ({
|
|
100
|
+
"phone-lookup": async () => null
|
|
101
|
+
})
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Test null data
|
|
105
|
+
const isNull = await isPhoneNumberVoip('+1234567890');
|
|
106
|
+
expect(isNull).toBe(false);
|
|
107
|
+
|
|
108
|
+
// Restore original grab function
|
|
109
|
+
globalThis.grab = originalGrab;
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
package/vitest.config.js
ADDED
package/wrangler.toml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name = "sms-verification-api"
|
|
2
|
+
main = "src/index.js"
|
|
3
|
+
compatibility_date = "2024-01-01"
|
|
4
|
+
compatibility_flags = ["nodejs_compat"]
|
|
5
|
+
|
|
6
|
+
# Environment variables
|
|
7
|
+
[vars]
|
|
8
|
+
AWS_REGION = "us-east-1"
|
|
9
|
+
SMS_SENDER_ID = "Verify"
|
|
10
|
+
|
|
11
|
+
# Secrets (these should be set via wrangler secret put)
|
|
12
|
+
TRESTLE_API_KEY = "URx3hLxr6o9uSghxffe2S72oMXuM12Ry9zW80Src"
|
|
13
|
+
|
|
14
|
+
# Staging environment
|
|
15
|
+
[env.staging]
|
|
16
|
+
name = "sms-verification-api-staging"
|
|
17
|
+
vars = { ENVIRONMENT = "staging" }
|
|
18
|
+
|
|
19
|
+
# Production environment
|
|
20
|
+
[env.production]
|
|
21
|
+
name = "sms-verification-api-production"
|
|
22
|
+
vars = { ENVIRONMENT = "production" }
|
|
23
|
+
|
|
24
|
+
# Build configuration
|
|
25
|
+
[build]
|
|
26
|
+
command = "bun run build"
|
|
27
|
+
watch_dir = "src"
|