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.
- package/.env.example +20 -0
- package/CHANGELOG.md +71 -0
- package/DEPLOYMENT.md +151 -0
- package/README.md +475 -0
- package/bun.lock +1110 -0
- package/docs/.source/index.ts +11 -0
- package/docs/.source/source.config.mjs +9 -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/bun.lock +923 -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.ts +105 -0
- package/examples/demo.html +325 -0
- package/examples/libphonenumber-example.ts +120 -0
- package/openapi.json +329 -0
- package/package.json +75 -0
- package/scripts/deploy.sh +63 -0
- package/src/identity-verification-server.ts +677 -0
- package/src/index.ts +8 -0
- package/src/sns.ts +265 -0
- package/src/verify-phone-server.ts +503 -0
- package/src/verify-phone.ts +577 -0
- package/test/api.test.ts +205 -0
- package/test/integration.test.ts +152 -0
- package/test/metadata-test.ts +73 -0
- package/test/server.test.ts +143 -0
- package/test/setup.ts +32 -0
- package/test/utils.test.ts +186 -0
- package/test/verify.test.ts +20 -0
- package/test/voip.test.ts +113 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +10 -0
- package/wrangler.toml +24 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMS API Server using Hono and AWS SNS.
|
|
3
|
+
*
|
|
4
|
+
* - Provides endpoints for sending and verifying SMS codes.
|
|
5
|
+
* - Supports general SMS messaging with custom text.
|
|
6
|
+
* - Supports API key authentication.
|
|
7
|
+
* - Optionally blocks VoIP numbers using a phone lookup API.
|
|
8
|
+
* - Designed for Cloudflare Workers, but testable locally.
|
|
9
|
+
*
|
|
10
|
+
* @module verify-phone-server
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { cors } from "hono/cors";
|
|
14
|
+
import { logger } from "hono/logger";
|
|
15
|
+
import { secureHeaders } from "hono/secure-headers";
|
|
16
|
+
import { rateLimiter } from "hono-rate-limiter";
|
|
17
|
+
import { swaggerUI } from "@hono/swagger-ui";
|
|
18
|
+
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
|
19
|
+
import verifyPhone from "./verify-phone";
|
|
20
|
+
|
|
21
|
+
interface Env {
|
|
22
|
+
Bindings: {
|
|
23
|
+
API_KEY?: string;
|
|
24
|
+
AWS_ACCESS_KEY_ID?: string;
|
|
25
|
+
AWS_SECRET_ACCESS_KEY?: string;
|
|
26
|
+
AWS_REGION?: string;
|
|
27
|
+
SMS_SENDER_ID?: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create the main app
|
|
32
|
+
const app = new OpenAPIHono<Env>();
|
|
33
|
+
|
|
34
|
+
// Middleware
|
|
35
|
+
app.use("*", logger());
|
|
36
|
+
app.use("*", secureHeaders());
|
|
37
|
+
app.use(
|
|
38
|
+
"*",
|
|
39
|
+
cors({
|
|
40
|
+
origin: ["*"],
|
|
41
|
+
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
42
|
+
allowHeaders: ["Content-Type", "Authorization", "X-API-Key"],
|
|
43
|
+
maxAge: 86400,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Rate limiting - lazy loaded to avoid global scope issues
|
|
48
|
+
const createRateLimiter = () =>
|
|
49
|
+
rateLimiter({
|
|
50
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
51
|
+
limit: 100,
|
|
52
|
+
message: "Too many requests from this IP, please try again later.",
|
|
53
|
+
standardHeaders: true,
|
|
54
|
+
keyGenerator: (c) =>
|
|
55
|
+
c.req.header("CF-Connecting-IP") ||
|
|
56
|
+
c.req.header("X-Forwarded-For") ||
|
|
57
|
+
"unknown",
|
|
58
|
+
} as any);
|
|
59
|
+
|
|
60
|
+
// Apply rate limiting only when the middleware is actually used
|
|
61
|
+
app.use("*", async (c, next) => {
|
|
62
|
+
const limiter = createRateLimiter();
|
|
63
|
+
return limiter(c as any, next);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// API Key authentication middleware
|
|
67
|
+
const authenticateApiKey = async (c: any, next: () => Promise<void>) => {
|
|
68
|
+
const apiKey =
|
|
69
|
+
c.req.header("X-API-Key") ||
|
|
70
|
+
c.req.header("Authorization")?.replace("Bearer ", "");
|
|
71
|
+
const expectedApiKey = c.env?.API_KEY;
|
|
72
|
+
|
|
73
|
+
if (!apiKey || apiKey !== expectedApiKey) {
|
|
74
|
+
return c.json(
|
|
75
|
+
{
|
|
76
|
+
success: false,
|
|
77
|
+
error: "Unauthorized",
|
|
78
|
+
message: "Invalid or missing API key",
|
|
79
|
+
},
|
|
80
|
+
401,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await next();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Health check endpoint
|
|
88
|
+
app.get("/", (c) => {
|
|
89
|
+
return c.json({
|
|
90
|
+
success: true,
|
|
91
|
+
message: "SMS Verification API",
|
|
92
|
+
version: "1.0.0",
|
|
93
|
+
endpoints: {
|
|
94
|
+
health: "/health",
|
|
95
|
+
send: "/api/send",
|
|
96
|
+
verify: "/api/verify",
|
|
97
|
+
docs: "/docs",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Health check
|
|
103
|
+
app.get("/health", (c) => {
|
|
104
|
+
return c.json({
|
|
105
|
+
success: true,
|
|
106
|
+
status: "healthy",
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
uptime: "N/A", // process.uptime() not available in Cloudflare Workers
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Generate verification code
|
|
113
|
+
function generateCode(length = 6) {
|
|
114
|
+
const chars = "0123456789";
|
|
115
|
+
let result = "";
|
|
116
|
+
for (let i = 0; i < length; i++) {
|
|
117
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Send SMS verification code
|
|
123
|
+
const sendRoute = createRoute({
|
|
124
|
+
method: "post",
|
|
125
|
+
path: "/api/send",
|
|
126
|
+
security: [{ apiKey: [] }],
|
|
127
|
+
request: {
|
|
128
|
+
body: {
|
|
129
|
+
content: {
|
|
130
|
+
"application/json": {
|
|
131
|
+
schema: z.object({
|
|
132
|
+
phoneNumber: z.string().min(1, "Phone number is required"),
|
|
133
|
+
code: z.string().optional(),
|
|
134
|
+
blockVoip: z.boolean().optional().default(false),
|
|
135
|
+
senderId: z.string().optional().default("Verify"),
|
|
136
|
+
messageTemplate: z.string().optional(),
|
|
137
|
+
smsType: z
|
|
138
|
+
.enum(["Transactional", "Promotional"])
|
|
139
|
+
.optional()
|
|
140
|
+
.default("Transactional"),
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
responses: {
|
|
147
|
+
200: {
|
|
148
|
+
content: {
|
|
149
|
+
"application/json": {
|
|
150
|
+
schema: z.object({
|
|
151
|
+
success: z.boolean(),
|
|
152
|
+
message: z.string().optional(),
|
|
153
|
+
messageId: z.string().optional(),
|
|
154
|
+
code: z.string().optional(),
|
|
155
|
+
phoneNumber: z.string().optional(),
|
|
156
|
+
expiresIn: z.number().optional(),
|
|
157
|
+
error: z.string().optional(),
|
|
158
|
+
details: z.string().optional(),
|
|
159
|
+
isVoip: z.boolean().optional(),
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
description: "SMS sent successfully",
|
|
164
|
+
},
|
|
165
|
+
400: {
|
|
166
|
+
content: {
|
|
167
|
+
"application/json": {
|
|
168
|
+
schema: z.object({
|
|
169
|
+
success: z.boolean(),
|
|
170
|
+
error: z.string(),
|
|
171
|
+
details: z.string().optional(),
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
description: "Bad request",
|
|
176
|
+
},
|
|
177
|
+
401: {
|
|
178
|
+
content: {
|
|
179
|
+
"application/json": {
|
|
180
|
+
schema: z.object({
|
|
181
|
+
success: z.boolean(),
|
|
182
|
+
error: z.string(),
|
|
183
|
+
message: z.string(),
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
description: "Unauthorized",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
app.openapi(sendRoute, (async (c: any) => {
|
|
193
|
+
try {
|
|
194
|
+
const body = await c.req.json();
|
|
195
|
+
const { phoneNumber, code, blockVoip, senderId, messageTemplate, smsType } =
|
|
196
|
+
body;
|
|
197
|
+
|
|
198
|
+
// Generate code if not provided
|
|
199
|
+
const verificationCode = code || generateCode();
|
|
200
|
+
|
|
201
|
+
// Get AWS credentials from environment
|
|
202
|
+
const awsCredentials = {
|
|
203
|
+
accessKeyId: c.env?.AWS_ACCESS_KEY_ID,
|
|
204
|
+
secretAccessKey: c.env?.AWS_SECRET_ACCESS_KEY,
|
|
205
|
+
awsRegion: c.env?.AWS_REGION || "us-east-1",
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Validate AWS credentials
|
|
209
|
+
if (!awsCredentials.accessKeyId || !awsCredentials.secretAccessKey) {
|
|
210
|
+
return c.json(
|
|
211
|
+
{
|
|
212
|
+
success: false,
|
|
213
|
+
error: "AWS credentials not configured",
|
|
214
|
+
details:
|
|
215
|
+
"Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables",
|
|
216
|
+
},
|
|
217
|
+
500,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Send verification SMS
|
|
222
|
+
const result = await verifyPhone({
|
|
223
|
+
phoneNumber,
|
|
224
|
+
code: verificationCode,
|
|
225
|
+
...awsCredentials,
|
|
226
|
+
blockVoip,
|
|
227
|
+
senderId: senderId || c.env?.SMS_SENDER_ID || "Verify",
|
|
228
|
+
messageTemplate,
|
|
229
|
+
smsType,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (result.success) {
|
|
233
|
+
return c.json({
|
|
234
|
+
success: true,
|
|
235
|
+
message: result.message,
|
|
236
|
+
messageId: result.messageId,
|
|
237
|
+
code: result.code,
|
|
238
|
+
phoneNumber: result.phoneNumber,
|
|
239
|
+
expiresIn: result.expiresIn,
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
return c.json(
|
|
243
|
+
{
|
|
244
|
+
success: false,
|
|
245
|
+
error: result.error,
|
|
246
|
+
details: result.details,
|
|
247
|
+
isVoip: result.isVoip,
|
|
248
|
+
},
|
|
249
|
+
400,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return c.json(
|
|
254
|
+
{
|
|
255
|
+
success: false,
|
|
256
|
+
error: "Internal server error",
|
|
257
|
+
details: error instanceof Error ? error.message : String(error),
|
|
258
|
+
},
|
|
259
|
+
500,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}) as any);
|
|
263
|
+
|
|
264
|
+
// Verify SMS code (mock endpoint for demonstration)
|
|
265
|
+
const verifyRoute = createRoute({
|
|
266
|
+
method: "post",
|
|
267
|
+
path: "/api/verify",
|
|
268
|
+
security: [{ apiKey: [] }],
|
|
269
|
+
request: {
|
|
270
|
+
body: {
|
|
271
|
+
content: {
|
|
272
|
+
"application/json": {
|
|
273
|
+
schema: z.object({
|
|
274
|
+
phoneNumber: z.string().min(1, "Phone number is required"),
|
|
275
|
+
code: z.string().min(1, "Verification code is required"),
|
|
276
|
+
}),
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
responses: {
|
|
282
|
+
200: {
|
|
283
|
+
content: {
|
|
284
|
+
"application/json": {
|
|
285
|
+
schema: z.object({
|
|
286
|
+
success: z.boolean(),
|
|
287
|
+
message: z.string().optional(),
|
|
288
|
+
verified: z.boolean().optional(),
|
|
289
|
+
error: z.string().optional(),
|
|
290
|
+
}),
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
description: "Code verified successfully",
|
|
294
|
+
},
|
|
295
|
+
400: {
|
|
296
|
+
content: {
|
|
297
|
+
"application/json": {
|
|
298
|
+
schema: z.object({
|
|
299
|
+
success: z.boolean(),
|
|
300
|
+
error: z.string(),
|
|
301
|
+
}),
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
description: "Bad request",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
app.openapi(verifyRoute, (async (c: any) => {
|
|
310
|
+
try {
|
|
311
|
+
const body = await c.req.json();
|
|
312
|
+
const { phoneNumber, code } = body;
|
|
313
|
+
|
|
314
|
+
// This is a mock verification - in a real app, you'd store codes in a database
|
|
315
|
+
// and verify them against stored values with proper expiration handling
|
|
316
|
+
|
|
317
|
+
// For demo purposes, we'll just return success
|
|
318
|
+
// In production, implement proper code storage and verification
|
|
319
|
+
return c.json({
|
|
320
|
+
success: true,
|
|
321
|
+
message: "Code verified successfully",
|
|
322
|
+
verified: true,
|
|
323
|
+
});
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return c.json(
|
|
326
|
+
{
|
|
327
|
+
success: false,
|
|
328
|
+
error: "Internal server error",
|
|
329
|
+
details: error instanceof Error ? error.message : String(error),
|
|
330
|
+
},
|
|
331
|
+
500,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}) as any);
|
|
335
|
+
|
|
336
|
+
// General SMS sending endpoint
|
|
337
|
+
const generalSmsRoute = createRoute({
|
|
338
|
+
method: "post",
|
|
339
|
+
path: "/api/sms",
|
|
340
|
+
security: [{ apiKey: [] }],
|
|
341
|
+
request: {
|
|
342
|
+
body: {
|
|
343
|
+
content: {
|
|
344
|
+
"application/json": {
|
|
345
|
+
schema: z.object({
|
|
346
|
+
phoneNumber: z.string().min(1, "Phone number is required"),
|
|
347
|
+
message: z.string().min(1, "Message is required"),
|
|
348
|
+
senderId: z.string().optional().default("Verify"),
|
|
349
|
+
smsType: z
|
|
350
|
+
.enum(["Transactional", "Promotional"])
|
|
351
|
+
.optional()
|
|
352
|
+
.default("Transactional"),
|
|
353
|
+
}),
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
responses: {
|
|
359
|
+
200: {
|
|
360
|
+
content: {
|
|
361
|
+
"application/json": {
|
|
362
|
+
schema: z.object({
|
|
363
|
+
success: z.boolean(),
|
|
364
|
+
message: z.string().optional(),
|
|
365
|
+
messageId: z.string().optional(),
|
|
366
|
+
phoneNumber: z.string().optional(),
|
|
367
|
+
error: z.string().optional(),
|
|
368
|
+
details: z.string().optional(),
|
|
369
|
+
}),
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
description: "SMS sent successfully",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
app.openapi(generalSmsRoute, (async (c: any) => {
|
|
378
|
+
try {
|
|
379
|
+
const body = await c.req.json();
|
|
380
|
+
const { phoneNumber, message, senderId, smsType } = body;
|
|
381
|
+
|
|
382
|
+
// Get AWS credentials from environment
|
|
383
|
+
const awsCredentials = {
|
|
384
|
+
accessKeyId: c.env?.AWS_ACCESS_KEY_ID,
|
|
385
|
+
secretAccessKey: c.env?.AWS_SECRET_ACCESS_KEY,
|
|
386
|
+
awsRegion: c.env?.AWS_REGION || "us-east-1",
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Validate AWS credentials
|
|
390
|
+
if (!awsCredentials.accessKeyId || !awsCredentials.secretAccessKey) {
|
|
391
|
+
return c.json(
|
|
392
|
+
{
|
|
393
|
+
success: false,
|
|
394
|
+
error: "AWS credentials not configured",
|
|
395
|
+
details:
|
|
396
|
+
"Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables",
|
|
397
|
+
},
|
|
398
|
+
500,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Send general SMS
|
|
403
|
+
const result = await verifyPhone({
|
|
404
|
+
phoneNumber,
|
|
405
|
+
code: "GENERAL", // Use a placeholder code for general SMS
|
|
406
|
+
...awsCredentials,
|
|
407
|
+
blockVoip: false,
|
|
408
|
+
senderId: senderId || c.env?.SMS_SENDER_ID || "Verify",
|
|
409
|
+
messageTemplate: message,
|
|
410
|
+
smsType,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (result.success) {
|
|
414
|
+
return c.json({
|
|
415
|
+
success: true,
|
|
416
|
+
message: "SMS sent successfully",
|
|
417
|
+
messageId: result.messageId,
|
|
418
|
+
phoneNumber: result.phoneNumber,
|
|
419
|
+
});
|
|
420
|
+
} else {
|
|
421
|
+
return c.json(
|
|
422
|
+
{
|
|
423
|
+
success: false,
|
|
424
|
+
error: result.error,
|
|
425
|
+
details: result.details,
|
|
426
|
+
},
|
|
427
|
+
400,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return c.json(
|
|
432
|
+
{
|
|
433
|
+
success: false,
|
|
434
|
+
error: "Internal server error",
|
|
435
|
+
details: error instanceof Error ? error.message : String(error),
|
|
436
|
+
},
|
|
437
|
+
500,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}) as any);
|
|
441
|
+
|
|
442
|
+
// OpenAPI documentation
|
|
443
|
+
app.openAPIRegistry.registerComponent("securitySchemes", "apiKey", {
|
|
444
|
+
type: "apiKey",
|
|
445
|
+
name: "X-API-Key",
|
|
446
|
+
in: "header",
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
app.doc("/docs", {
|
|
450
|
+
openapi: "3.0.0",
|
|
451
|
+
info: {
|
|
452
|
+
title: "SMS Verification API",
|
|
453
|
+
version: "1.0.0",
|
|
454
|
+
description: "API for sending SMS verification codes using AWS SNS",
|
|
455
|
+
},
|
|
456
|
+
servers: [
|
|
457
|
+
{
|
|
458
|
+
url: "https://sms-verification-api.your-subdomain.workers.dev",
|
|
459
|
+
description: "Production server",
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
url: "http://localhost:8787",
|
|
463
|
+
description: "Development server",
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Swagger UI
|
|
469
|
+
app.get("/docs", swaggerUI({ url: "/docs" }));
|
|
470
|
+
|
|
471
|
+
// Apply authentication to all API routes
|
|
472
|
+
app.use("/api/*", authenticateApiKey);
|
|
473
|
+
|
|
474
|
+
// Error handling
|
|
475
|
+
app.onError((err, c) => {
|
|
476
|
+
console.error("Server error:", err);
|
|
477
|
+
return c.json(
|
|
478
|
+
{
|
|
479
|
+
success: false,
|
|
480
|
+
error: "Internal server error",
|
|
481
|
+
details: err.message,
|
|
482
|
+
},
|
|
483
|
+
500,
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// 404 handler
|
|
488
|
+
app.notFound((c) => {
|
|
489
|
+
return c.json(
|
|
490
|
+
{
|
|
491
|
+
success: false,
|
|
492
|
+
error: "Not found",
|
|
493
|
+
message: "The requested endpoint does not exist",
|
|
494
|
+
},
|
|
495
|
+
404,
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
export { isPhoneNumberVoip } from "./verify-phone";
|
|
500
|
+
|
|
501
|
+
export const createApp = (_env?: Env["Bindings"]) => app;
|
|
502
|
+
|
|
503
|
+
export default app;
|