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.
Files changed (50) hide show
  1. package/.env.example +20 -0
  2. package/DEPLOYMENT.md +151 -0
  3. package/README.md +475 -0
  4. package/docs/app/(home)/layout.tsx +7 -0
  5. package/docs/app/(home)/page.tsx +38 -0
  6. package/docs/app/docs/[[...slug]]/page.tsx +59 -0
  7. package/docs/app/docs/layout.tsx +12 -0
  8. package/docs/app/docs-og/[...slug]/route.ts +24 -0
  9. package/docs/app/globals.css +587 -0
  10. package/docs/app/layout.config.tsx +13 -0
  11. package/docs/app/layout.tsx +27 -0
  12. package/docs/app/logo.tsx +35 -0
  13. package/docs/content/docs/API_AUTHENTICATION.md +91 -0
  14. package/docs/content/docs/DEPLOYMENT.md +181 -0
  15. package/docs/content/docs/api/post.mdx +35 -0
  16. package/docs/content/docs/api/verify.mdx +34 -0
  17. package/docs/content/docs/meta.json +8 -0
  18. package/docs/content/docs/verify-legal-name.md +339 -0
  19. package/docs/lib/source.ts +14 -0
  20. package/docs/mdx-components.tsx +12 -0
  21. package/docs/next.config.mjs +51 -0
  22. package/docs/openapi.json +329 -0
  23. package/docs/package.json +37 -0
  24. package/docs/postcss.config.mjs +5 -0
  25. package/docs/scripts/generate-docs.mjs +23 -0
  26. package/docs/source.config.ts +5 -0
  27. package/docs/tsconfig.json +29 -0
  28. package/docs/worker.js +35 -0
  29. package/docs/wrangler.toml +26 -0
  30. package/examples/client.js +119 -0
  31. package/examples/demo.html +325 -0
  32. package/examples/libphonenumber-example.js +120 -0
  33. package/openapi.json +329 -0
  34. package/package.json +71 -0
  35. package/scripts/deploy.sh +63 -0
  36. package/src/identity-verification-server.ts +553 -0
  37. package/src/index.js +8 -0
  38. package/src/sns.js +236 -0
  39. package/src/verify-phone-server.js +448 -0
  40. package/src/verify-phone.ts +551 -0
  41. package/test/api.test.js +201 -0
  42. package/test/integration.test.js +152 -0
  43. package/test/metadata-test.js +73 -0
  44. package/test/server.test.js +143 -0
  45. package/test/setup.js +32 -0
  46. package/test/utils.test.js +186 -0
  47. package/test/verify.test.js +23 -0
  48. package/test/voip.test.js +112 -0
  49. package/vitest.config.js +10 -0
  50. package/wrangler.toml +27 -0
@@ -0,0 +1,553 @@
1
+ import { Hono } from 'hono'
2
+ import { cors } from 'hono/cors'
3
+ import { logger } from 'hono/logger'
4
+ import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
5
+
6
+ // Input/Output Schemas
7
+ const PersonInputSchema = z.object({
8
+ phone_number: z.string().min(10).max(15).describe('Phone number in E.164 or local format'),
9
+ legal_name: z.string().min(1).max(100).describe('Full legal name of the person'),
10
+ current_address: z.object({
11
+ street_line_1: z.string().min(1).max(1000),
12
+ street_line_2: z.string().max(1000).optional(),
13
+ city: z.string().min(1).max(500),
14
+ state_code: z.string().length(2),
15
+ postal_code: z.string().min(5).max(10),
16
+ country_code: z.string().length(2).default('US')
17
+ }).describe('Current address information')
18
+ })
19
+
20
+ const VerificationResponseSchema = z.object({
21
+ verification_score: z.number().min(0).max(100).describe('Confidence score 0-100'),
22
+ name_match_found: z.boolean(),
23
+ phone_validated: z.boolean(),
24
+ address_validated: z.boolean(),
25
+ questions: z.array(z.object({
26
+ id: z.string(),
27
+ question: z.string(),
28
+ type: z.enum(['address_history', 'phone_history', 'name_verification']),
29
+ options: z.array(z.string()).optional()
30
+ })),
31
+ historical_data: z.object({
32
+ previous_addresses: z.array(z.string()),
33
+ previous_phones: z.array(z.string()),
34
+ associated_names: z.array(z.string())
35
+ }),
36
+ recommendations: z.array(z.string()).describe('Suggestions for additional verification')
37
+ })
38
+
39
+ // TrestleIQ API Functions
40
+ async function makeRequest(endpoint, params, apiKey, baseUrl = 'https://api.trestleiq.com') {
41
+ const url = new URL(endpoint, baseUrl)
42
+ Object.entries(params).forEach(([key, value]) => {
43
+ if (value) url.searchParams.set(key, value)
44
+ })
45
+
46
+ const response = await fetch(url.toString(), {
47
+ headers: {
48
+ 'x-api-key': apiKey,
49
+ 'Accept': 'application/json'
50
+ }
51
+ })
52
+
53
+ if (!response.ok) {
54
+ throw new Error(`TrestleIQ API error: ${response.status} ${response.statusText}`)
55
+ }
56
+
57
+ return response.json()
58
+ }
59
+
60
+ function reversePhone(phoneNumber, apiKey, hints = {}) {
61
+ return makeRequest('/3.2/phone', {
62
+ phone: phoneNumber,
63
+ 'phone.country_hint': 'US',
64
+ ...(hints.name && { 'phone.name_hint': hints.name }),
65
+ ...(hints.postalCode && { 'phone.postal_code_hint': hints.postalCode })
66
+ }, apiKey)
67
+ }
68
+
69
+ function findPerson(name, address, apiKey) {
70
+ return makeRequest('/3.1/person', {
71
+ name,
72
+ 'address.street_line_1': address.street_line_1,
73
+ 'address.city': address.city,
74
+ 'address.state_code': address.state_code,
75
+ 'address.postal_code': address.postal_code,
76
+ 'address.country_code': address.country_code || 'US'
77
+ }, apiKey)
78
+ }
79
+
80
+ function reverseAddress(address, apiKey) {
81
+ return makeRequest('/3.1/location', {
82
+ street_line_1: address.street_line_1,
83
+ ...(address.street_line_2 && { street_line_2: address.street_line_2 }),
84
+ city: address.city,
85
+ state_code: address.state_code,
86
+ postal_code: address.postal_code,
87
+ country_code: address.country_code || 'US'
88
+ }, apiKey)
89
+ }
90
+
91
+ // Utility Functions
92
+ function normalizeName(name) {
93
+ return name.toLowerCase().replace(/[^a-z\s]/g, '').trim()
94
+ }
95
+
96
+ function extractHistoricalData(phoneResult, personResult, addressResult) {
97
+ const previousAddresses = []
98
+ const previousPhones = []
99
+ const associatedNames = []
100
+
101
+ // Extract from phone result
102
+ if (phoneResult?.owners) {
103
+ phoneResult.owners.forEach(owner => {
104
+ if (owner.name) associatedNames.push(owner.name)
105
+ if (owner.addresses) {
106
+ owner.addresses.forEach(addr => {
107
+ if (addr.street_line_1 && addr.city && addr.state_code) {
108
+ previousAddresses.push(`${addr.street_line_1}, ${addr.city}, ${addr.state_code}`)
109
+ }
110
+ })
111
+ }
112
+ if (owner.phones) {
113
+ owner.phones.forEach(phone => {
114
+ if (phone.phone_number) previousPhones.push(phone.phone_number)
115
+ })
116
+ }
117
+ })
118
+ }
119
+
120
+ // Extract from person result
121
+ if (personResult?.person) {
122
+ personResult.person.forEach(person => {
123
+ if (person.addresses) {
124
+ person.addresses.forEach(addr => {
125
+ if (addr.street_line_1 && addr.city && addr.state_code) {
126
+ previousAddresses.push(`${addr.street_line_1}, ${addr.city}, ${addr.state_code}`)
127
+ }
128
+ })
129
+ }
130
+ if (person.phones) {
131
+ person.phones.forEach(phone => {
132
+ if (phone.phone_number) previousPhones.push(phone.phone_number)
133
+ })
134
+ }
135
+ })
136
+ }
137
+
138
+ // Extract from address result
139
+ if (addressResult?.current_residents) {
140
+ addressResult.current_residents.forEach(resident => {
141
+ if (resident.name) associatedNames.push(resident.name)
142
+ })
143
+ }
144
+
145
+ return {
146
+ previous_addresses: [...new Set(previousAddresses)].slice(0, 10),
147
+ previous_phones: [...new Set(previousPhones)].slice(0, 5),
148
+ associated_names: [...new Set(associatedNames)].slice(0, 10)
149
+ }
150
+ }
151
+
152
+ function checkNameMatch(personResult, phoneResult, inputName) {
153
+ const normalizedInputName = normalizeName(inputName)
154
+
155
+ // Check person result
156
+ if (personResult?.person) {
157
+ for (const person of personResult.person) {
158
+ if (person.name && normalizeName(person.name) === normalizedInputName) {
159
+ return true
160
+ }
161
+ }
162
+ }
163
+
164
+ // Check phone owners
165
+ if (phoneResult?.owners) {
166
+ for (const owner of phoneResult.owners) {
167
+ if (owner.name && normalizeName(owner.name) === normalizedInputName) {
168
+ return true
169
+ }
170
+ }
171
+ }
172
+
173
+ return false
174
+ }
175
+
176
+ function checkPartialNameMatch(personResult, phoneResult, inputName) {
177
+ const inputNameParts = normalizeName(inputName).split(' ')
178
+
179
+ const checkNameParts = (name) => {
180
+ const nameParts = normalizeName(name).split(' ')
181
+ return inputNameParts.some(part => nameParts.includes(part) && part.length > 2)
182
+ }
183
+
184
+ // Check person result
185
+ if (personResult?.person) {
186
+ for (const person of personResult.person) {
187
+ if (person.name && checkNameParts(person.name)) {
188
+ return true
189
+ }
190
+ }
191
+ }
192
+
193
+ // Check phone owners
194
+ if (phoneResult?.owners) {
195
+ for (const owner of phoneResult.owners) {
196
+ if (owner.name && checkNameParts(owner.name)) {
197
+ return true
198
+ }
199
+ }
200
+ }
201
+
202
+ return false
203
+ }
204
+
205
+ function calculateVerificationScore(phoneResult, personResult, addressResult, input) {
206
+ let score = 0
207
+
208
+ // Phone validation (30 points max)
209
+ if (phoneResult?.is_valid) {
210
+ score += 15
211
+ if (phoneResult.line_type === 'Mobile' || phoneResult.line_type === 'Landline') {
212
+ score += 10
213
+ }
214
+ if (!phoneResult.is_commercial) {
215
+ score += 5
216
+ }
217
+ }
218
+
219
+ // Name matching (40 points max)
220
+ if (checkNameMatch(personResult, phoneResult, input.legal_name)) {
221
+ score += 40
222
+ } else if (checkPartialNameMatch(personResult, phoneResult, input.legal_name)) {
223
+ score += 20
224
+ }
225
+
226
+ // Address validation (30 points max)
227
+ if (addressResult?.is_valid) {
228
+ score += 15
229
+ if (addressResult.is_active) {
230
+ score += 10
231
+ }
232
+ if (!addressResult.is_commercial) {
233
+ score += 5
234
+ }
235
+ }
236
+
237
+ return Math.min(score, 100)
238
+ }
239
+
240
+ function generateFakeAddresses(count) {
241
+ const fakeAddresses = [
242
+ '123 Fake St, Nowhere, CA',
243
+ '456 Made Up Ave, Fictional, TX',
244
+ '789 Pretend Dr, Imaginary, FL',
245
+ '101 False Blvd, Bogus, NY',
246
+ '202 Phony Way, Unreal, WA'
247
+ ]
248
+ return fakeAddresses.sort(() => Math.random() - 0.5).slice(0, count)
249
+ }
250
+
251
+ function generateFakePhones(count) {
252
+ const fakePhones = []
253
+ for (let i = 0; i < count; i++) {
254
+ const areaCode = Math.floor(Math.random() * 900) + 100
255
+ const exchange = Math.floor(Math.random() * 900) + 100
256
+ const number = Math.floor(Math.random() * 9000) + 1000
257
+ fakePhones.push(`${areaCode}${exchange}${number}`)
258
+ }
259
+ return fakePhones
260
+ }
261
+
262
+ function generateVerificationQuestions(historicalData, inputName) {
263
+ const questions = []
264
+
265
+ // Address history questions
266
+ if (historicalData.previous_addresses.length > 0) {
267
+ const shuffledAddresses = [...historicalData.previous_addresses].sort(() => Math.random() - 0.5)
268
+ const realAddresses = shuffledAddresses.slice(0, 3)
269
+ const fakeAddresses = generateFakeAddresses(2)
270
+ const allOptions = [...realAddresses, ...fakeAddresses].sort(() => Math.random() - 0.5)
271
+
272
+ questions.push({
273
+ id: `addr_${Date.now()}`,
274
+ question: `Which of the following addresses have you lived at in the past?`,
275
+ type: 'address_history',
276
+ options: allOptions
277
+ })
278
+ }
279
+
280
+ // Phone history questions
281
+ if (historicalData.previous_phones.length > 1) {
282
+ const shuffledPhones = [...historicalData.previous_phones].sort(() => Math.random() - 0.5)
283
+ const realPhones = shuffledPhones.slice(0, 2)
284
+ const fakePhones = generateFakePhones(2)
285
+ const allOptions = [...realPhones, ...fakePhones, 'None of the above'].sort(() => Math.random() - 0.5)
286
+
287
+ questions.push({
288
+ id: `phone_${Date.now()}`,
289
+ question: `Which of the following phone numbers have you previously used?`,
290
+ type: 'phone_history',
291
+ options: allOptions
292
+ })
293
+ }
294
+
295
+ // Name verification questions
296
+ if (historicalData.associated_names.length > 0) {
297
+ const otherNames = historicalData.associated_names.filter(
298
+ name => normalizeName(name) !== normalizeName(inputName)
299
+ )
300
+
301
+ if (otherNames.length > 0) {
302
+ questions.push({
303
+ id: `name_${Date.now()}`,
304
+ question: `Are any of the following names associated with you (maiden name, nickname, etc.)?`,
305
+ type: 'name_verification',
306
+ options: [...otherNames.slice(0, 4), 'None of the above']
307
+ })
308
+ }
309
+ }
310
+
311
+ return questions
312
+ }
313
+
314
+ function generateRecommendations(score, phoneResult, personResult, addressResult) {
315
+ const recommendations = []
316
+
317
+ if (score < 30) {
318
+ recommendations.push('Consider requesting additional identification documents')
319
+ recommendations.push('Verify identity through alternative methods (government ID, utility bills)')
320
+ } else if (score < 60) {
321
+ recommendations.push('Request additional verification questions')
322
+ recommendations.push('Consider manual review of provided information')
323
+ } else if (score < 80) {
324
+ recommendations.push('Proceed with standard verification process')
325
+ } else {
326
+ recommendations.push('High confidence verification - proceed with confidence')
327
+ }
328
+
329
+ if (!phoneResult?.is_valid) {
330
+ recommendations.push('Request alternative contact phone number')
331
+ }
332
+
333
+ if (!addressResult?.is_valid) {
334
+ recommendations.push('Verify current address with utility bill or bank statement')
335
+ }
336
+
337
+ if (phoneResult?.line_type === 'NonFixedVOIP') {
338
+ recommendations.push('VOIP number detected - consider additional verification')
339
+ }
340
+
341
+ return recommendations
342
+ }
343
+
344
+ // Main Verification Function
345
+ async function verifyIdentity(input, apiKey) {
346
+ const { phone_number, legal_name, current_address } = input
347
+
348
+ // Run all API calls in parallel
349
+ const [phoneData, personData, addressData] = await Promise.allSettled([
350
+ reversePhone(phone_number, apiKey, {
351
+ name: legal_name,
352
+ postalCode: current_address.postal_code
353
+ }),
354
+ findPerson(legal_name, current_address, apiKey),
355
+ reverseAddress(current_address, apiKey)
356
+ ])
357
+
358
+ // Process results
359
+ const phoneResult = phoneData.status === 'fulfilled' ? phoneData.value : null
360
+ const personResult = personData.status === 'fulfilled' ? personData.value : null
361
+ const addressResult = addressData.status === 'fulfilled' ? addressData.value : null
362
+
363
+ // Extract historical data
364
+ const historicalData = extractHistoricalData(phoneResult, personResult, addressResult)
365
+
366
+ // Calculate verification score
367
+ const verificationScore = calculateVerificationScore(
368
+ phoneResult,
369
+ personResult,
370
+ addressResult,
371
+ input
372
+ )
373
+
374
+ // Generate verification questions
375
+ const questions = generateVerificationQuestions(historicalData, legal_name)
376
+
377
+ // Generate recommendations
378
+ const recommendations = generateRecommendations(
379
+ verificationScore,
380
+ phoneResult,
381
+ personResult,
382
+ addressResult
383
+ )
384
+
385
+ return {
386
+ verification_score: verificationScore,
387
+ name_match_found: checkNameMatch(personResult, phoneResult, legal_name),
388
+ phone_validated: phoneResult?.is_valid || false,
389
+ address_validated: addressResult?.is_valid || false,
390
+ questions,
391
+ historical_data: historicalData,
392
+ recommendations
393
+ }
394
+ }
395
+
396
+ // Create Hono app
397
+ const app = new OpenAPIHono()
398
+
399
+ // Middleware
400
+ app.use('*', cors())
401
+ app.use('*', logger())
402
+
403
+ // Get API key from environment
404
+ const getApiKey = () => {
405
+ const apiKey = process.env.TRESTLE_API_KEY
406
+ if (!apiKey) {
407
+ throw new Error('TRESTLE_API_KEY environment variable is required')
408
+ }
409
+ return apiKey
410
+ }
411
+
412
+ // Main API route
413
+ const verifyIdentityRoute = createRoute({
414
+ method: 'post',
415
+ path: '/verify-identity',
416
+ tags: ['Identity Verification'],
417
+ summary: 'Verify person identity',
418
+ description: 'Verify a person\'s identity using phone number, legal name, and current address',
419
+ request: {
420
+ body: {
421
+ content: {
422
+ 'application/json': {
423
+ schema: PersonInputSchema
424
+ }
425
+ }
426
+ }
427
+ },
428
+ responses: {
429
+ 200: {
430
+ content: {
431
+ 'application/json': {
432
+ schema: VerificationResponseSchema
433
+ }
434
+ },
435
+ description: 'Identity verification completed successfully'
436
+ },
437
+ 400: {
438
+ description: 'Bad request - invalid input data'
439
+ },
440
+ 500: {
441
+ description: 'Internal server error'
442
+ }
443
+ }
444
+ })
445
+
446
+ app.openapi(verifyIdentityRoute, async (c) => {
447
+ try {
448
+ const body = c.req.valid('json')
449
+ const apiKey = getApiKey()
450
+ const result = await verifyIdentity(body, apiKey)
451
+
452
+ return c.json(result, 200)
453
+ } catch (error) {
454
+ console.error('Identity verification error:', error)
455
+ return c.json({
456
+ error: 'Failed to verify identity',
457
+ message: error instanceof Error ? error.message : 'Unknown error'
458
+ }, 500)
459
+ }
460
+ })
461
+
462
+ // Health check endpoint
463
+ app.get('/health', (c) => {
464
+ return c.json({ status: 'healthy', timestamp: new Date().toISOString() })
465
+ })
466
+
467
+ // OpenAPI documentation
468
+ app.doc('/doc', {
469
+ openapi: '3.0.0',
470
+ info: {
471
+ version: '1.0.0',
472
+ title: 'Identity Verification API',
473
+ description: 'API for verifying person identity using TrestleIQ data services'
474
+ },
475
+ servers: [
476
+ {
477
+ url: 'http://localhost:3000',
478
+ description: 'Development server'
479
+ }
480
+ ]
481
+ })
482
+
483
+ // Serve Swagger UI
484
+ app.get('/swagger', async (c) => {
485
+ return c.html(`
486
+ <!DOCTYPE html>
487
+ <html>
488
+ <head>
489
+ <title>Identity Verification API</title>
490
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3.52.5/swagger-ui.css" />
491
+ </head>
492
+ <body>
493
+ <div id="swagger-ui"></div>
494
+ <script src="https://unpkg.com/swagger-ui-dist@3.52.5/swagger-ui-bundle.js"></script>
495
+ <script>
496
+ SwaggerUIBundle({
497
+ url: '/doc',
498
+ dom_id: '#swagger-ui',
499
+ presets: [SwaggerUIBundle.presets.standalone]
500
+ })
501
+ </script>
502
+ </body>
503
+ </html>
504
+ `)
505
+ })
506
+
507
+ // Demo endpoint with sample data
508
+ app.get('/demo', async (c) => {
509
+ const sampleRequest = {
510
+ phone_number: "2069735100",
511
+ legal_name: "John Smith",
512
+ current_address: {
513
+ street_line_1: "123 Main St",
514
+ city: "Seattle",
515
+ state_code: "WA",
516
+ postal_code: "98101",
517
+ country_code: "US"
518
+ }
519
+ }
520
+
521
+ try {
522
+ const apiKey = getApiKey()
523
+ const result = await verifyIdentity(sampleRequest, apiKey)
524
+
525
+ return c.json({
526
+ demo_request: sampleRequest,
527
+ demo_response: result,
528
+ note: "This is a demo using sample data. Use POST /verify-identity for real verification."
529
+ })
530
+ } catch (error) {
531
+ return c.json({
532
+ demo_request: sampleRequest,
533
+ error: "Demo failed - check your TRESTLE_API_KEY",
534
+ note: "This demo requires a valid TrestleIQ API key"
535
+ })
536
+ }
537
+ })
538
+
539
+ export default app
540
+
541
+ // Start server if not imported as module
542
+ if (import.meta.main) {
543
+ const port = parseInt(process.env.PORT || '3000')
544
+ console.log(`🚀 Server running at http://localhost:${port}`)
545
+ console.log(`📚 API Documentation: http://localhost:${port}/swagger`)
546
+ console.log(`🔍 OpenAPI Spec: http://localhost:${port}/doc`)
547
+ console.log(`🎮 Demo Endpoint: http://localhost:${port}/demo`)
548
+
549
+ Bun.serve({
550
+ port,
551
+ fetch: app.fetch,
552
+ })
553
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Main entry point for the SMS Verification API server.
3
+ * This file is used by Cloudflare Workers and Wrangler.
4
+ */
5
+
6
+ import app from "./verify-phone-server.js";
7
+
8
+ export default app;