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