verify-phone-sms 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.env.example +20 -0
  2. package/CHANGELOG.md +71 -0
  3. package/DEPLOYMENT.md +151 -0
  4. package/README.md +475 -0
  5. package/bun.lock +1110 -0
  6. package/docs/.source/index.ts +11 -0
  7. package/docs/.source/source.config.mjs +9 -0
  8. package/docs/app/(home)/layout.tsx +7 -0
  9. package/docs/app/(home)/page.tsx +38 -0
  10. package/docs/app/docs/[[...slug]]/page.tsx +59 -0
  11. package/docs/app/docs/layout.tsx +12 -0
  12. package/docs/app/docs-og/[...slug]/route.ts +24 -0
  13. package/docs/app/globals.css +587 -0
  14. package/docs/app/layout.config.tsx +13 -0
  15. package/docs/app/layout.tsx +27 -0
  16. package/docs/app/logo.tsx +35 -0
  17. package/docs/bun.lock +923 -0
  18. package/docs/content/docs/API_AUTHENTICATION.md +91 -0
  19. package/docs/content/docs/DEPLOYMENT.md +181 -0
  20. package/docs/content/docs/api/post.mdx +35 -0
  21. package/docs/content/docs/api/verify.mdx +34 -0
  22. package/docs/content/docs/meta.json +8 -0
  23. package/docs/content/docs/verify-legal-name.md +339 -0
  24. package/docs/lib/source.ts +14 -0
  25. package/docs/mdx-components.tsx +12 -0
  26. package/docs/next.config.mjs +51 -0
  27. package/docs/openapi.json +329 -0
  28. package/docs/package.json +37 -0
  29. package/docs/postcss.config.mjs +5 -0
  30. package/docs/scripts/generate-docs.mjs +23 -0
  31. package/docs/source.config.ts +5 -0
  32. package/docs/tsconfig.json +29 -0
  33. package/docs/worker.js +35 -0
  34. package/docs/wrangler.toml +26 -0
  35. package/examples/client.ts +105 -0
  36. package/examples/demo.html +325 -0
  37. package/examples/libphonenumber-example.ts +120 -0
  38. package/openapi.json +329 -0
  39. package/package.json +75 -0
  40. package/scripts/deploy.sh +63 -0
  41. package/src/identity-verification-server.ts +677 -0
  42. package/src/index.ts +8 -0
  43. package/src/sns.ts +265 -0
  44. package/src/verify-phone-server.ts +503 -0
  45. package/src/verify-phone.ts +577 -0
  46. package/test/api.test.ts +205 -0
  47. package/test/integration.test.ts +152 -0
  48. package/test/metadata-test.ts +73 -0
  49. package/test/server.test.ts +143 -0
  50. package/test/setup.ts +32 -0
  51. package/test/utils.test.ts +186 -0
  52. package/test/verify.test.ts +20 -0
  53. package/test/voip.test.ts +113 -0
  54. package/tsconfig.json +24 -0
  55. package/vitest.config.ts +10 -0
  56. package/wrangler.toml +24 -0
@@ -0,0 +1,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;