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,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
|
+
}
|