verify-phone-sms 0.9.4

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.
Files changed (56) hide show
  1. package/.env.example +20 -0
  2. package/CHANGELOG.md +71 -0
  3. package/DEPLOYMENT.md +151 -0
  4. package/README.md +475 -0
  5. package/bun.lock +1110 -0
  6. package/docs/.source/index.ts +11 -0
  7. package/docs/.source/source.config.mjs +9 -0
  8. package/docs/app/(home)/layout.tsx +7 -0
  9. package/docs/app/(home)/page.tsx +38 -0
  10. package/docs/app/docs/[[...slug]]/page.tsx +59 -0
  11. package/docs/app/docs/layout.tsx +12 -0
  12. package/docs/app/docs-og/[...slug]/route.ts +24 -0
  13. package/docs/app/globals.css +587 -0
  14. package/docs/app/layout.config.tsx +13 -0
  15. package/docs/app/layout.tsx +27 -0
  16. package/docs/app/logo.tsx +35 -0
  17. package/docs/bun.lock +923 -0
  18. package/docs/content/docs/API_AUTHENTICATION.md +91 -0
  19. package/docs/content/docs/DEPLOYMENT.md +181 -0
  20. package/docs/content/docs/api/post.mdx +35 -0
  21. package/docs/content/docs/api/verify.mdx +34 -0
  22. package/docs/content/docs/meta.json +8 -0
  23. package/docs/content/docs/verify-legal-name.md +339 -0
  24. package/docs/lib/source.ts +14 -0
  25. package/docs/mdx-components.tsx +12 -0
  26. package/docs/next.config.mjs +51 -0
  27. package/docs/openapi.json +329 -0
  28. package/docs/package.json +37 -0
  29. package/docs/postcss.config.mjs +5 -0
  30. package/docs/scripts/generate-docs.mjs +23 -0
  31. package/docs/source.config.ts +5 -0
  32. package/docs/tsconfig.json +29 -0
  33. package/docs/worker.js +35 -0
  34. package/docs/wrangler.toml +26 -0
  35. package/examples/client.ts +105 -0
  36. package/examples/demo.html +325 -0
  37. package/examples/libphonenumber-example.ts +120 -0
  38. package/openapi.json +329 -0
  39. package/package.json +75 -0
  40. package/scripts/deploy.sh +63 -0
  41. package/src/identity-verification-server.ts +677 -0
  42. package/src/index.ts +8 -0
  43. package/src/sns.ts +265 -0
  44. package/src/verify-phone-server.ts +503 -0
  45. package/src/verify-phone.ts +577 -0
  46. package/test/api.test.ts +205 -0
  47. package/test/integration.test.ts +152 -0
  48. package/test/metadata-test.ts +73 -0
  49. package/test/server.test.ts +143 -0
  50. package/test/setup.ts +32 -0
  51. package/test/utils.test.ts +186 -0
  52. package/test/verify.test.ts +20 -0
  53. package/test/voip.test.ts +113 -0
  54. package/tsconfig.json +24 -0
  55. package/vitest.config.ts +10 -0
  56. package/wrangler.toml +24 -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,20 @@
1
+ import verifyPhone from '../src/verify-phone';
2
+
3
+ // Basic usage with custom code
4
+ const result = await verifyPhone({
5
+ phoneNumber: '+1234567890',
6
+ code: 'ABC123',
7
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
8
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
9
+ awsRegion: 'us-east-1',
10
+ blockVoip: true,
11
+ senderId: 'MyApp',
12
+ messageTemplate: 'MyApp: Your code is {code}. Valid for 10 minutes.'
13
+ });
14
+
15
+ if (result.success) {
16
+ console.log('SMS sent successfully:', result.messageId);
17
+ console.log('Code sent:', result.code);
18
+ } else {
19
+ console.error('Failed to send SMS:', result.error);
20
+ }
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { createApp } from '../src/verify-phone-server';
3
+
4
+ describe('VoIP Blocking Functionality', () => {
5
+ let app: ReturnType<typeof createApp>;
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: any = 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: any = 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: any = 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');
56
+ const { isPhoneNumberVoip } = module;
57
+
58
+ // Mock the grab function to return VoIP data
59
+ const g = globalThis as any;
60
+ const originalGrab = g.grab;
61
+ g.grab = {
62
+ instance: () => ({
63
+ "phone-lookup": async () => ({
64
+ carrier: {
65
+ name: 'Bandwidth',
66
+ type: 'voip'
67
+ },
68
+ portability: {
69
+ line_type: 'landline'
70
+ }
71
+ })
72
+ })
73
+ };
74
+
75
+ // Test VoIP number
76
+ const isVoip = await isPhoneNumberVoip('+1234567890');
77
+ expect(isVoip).toBe(true);
78
+
79
+ // Mock the grab function to return mobile data
80
+ g.grab = {
81
+ instance: () => ({
82
+ "phone-lookup": async () => ({
83
+ carrier: {
84
+ name: 'Verizon Wireless',
85
+ type: 'mobile'
86
+ },
87
+ portability: {
88
+ line_type: 'mobile'
89
+ }
90
+ })
91
+ })
92
+ };
93
+
94
+ // Test mobile number
95
+ const isMobile = await isPhoneNumberVoip('+1234567890');
96
+ expect(isMobile).toBe(false);
97
+
98
+ // Mock the grab function to return null data
99
+ g.grab = {
100
+ instance: () => ({
101
+ "phone-lookup": async () => null
102
+ })
103
+ };
104
+
105
+ // Test null data
106
+ const isNull = await isPhoneNumberVoip('+1234567890');
107
+ expect(isNull).toBe(false);
108
+
109
+ // Restore original grab function
110
+ g.grab = originalGrab;
111
+ });
112
+ });
113
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022"],
7
+ "types": ["@cloudflare/workers-types", "node"],
8
+ "strict": false,
9
+ "noImplicitAny": false,
10
+ "strictNullChecks": false,
11
+ "esModuleInterop": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "skipLibCheck": true,
16
+ "noEmit": true,
17
+ "allowJs": false,
18
+ "jsx": "react-jsx",
19
+ "forceConsistentCasingInFileNames": true,
20
+ "verbatimModuleSyntax": false
21
+ },
22
+ "include": ["src/**/*.ts", "test/**/*.ts", "examples/**/*.ts", "vitest.config.ts"],
23
+ "exclude": ["node_modules", "docs", ".wrangler", "dist"]
24
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ setupFiles: ['./test/setup.ts'],
8
+ mockReset: true,
9
+ },
10
+ });
package/wrangler.toml ADDED
@@ -0,0 +1,24 @@
1
+ name = "sms-verification-api"
2
+ main = "src/index.ts"
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
+ # Staging environment
12
+ [env.staging]
13
+ name = "sms-verification-api-staging"
14
+ vars = { ENVIRONMENT = "staging" }
15
+
16
+ # Production environment
17
+ [env.production]
18
+ name = "sms-verification-api-production"
19
+ vars = { ENVIRONMENT = "production" }
20
+
21
+ # Build configuration
22
+ [build]
23
+ command = "bun run build"
24
+ watch_dir = "src"