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