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.
Files changed (50) hide show
  1. package/.env.example +20 -0
  2. package/DEPLOYMENT.md +151 -0
  3. package/README.md +475 -0
  4. package/docs/app/(home)/layout.tsx +7 -0
  5. package/docs/app/(home)/page.tsx +38 -0
  6. package/docs/app/docs/[[...slug]]/page.tsx +59 -0
  7. package/docs/app/docs/layout.tsx +12 -0
  8. package/docs/app/docs-og/[...slug]/route.ts +24 -0
  9. package/docs/app/globals.css +587 -0
  10. package/docs/app/layout.config.tsx +13 -0
  11. package/docs/app/layout.tsx +27 -0
  12. package/docs/app/logo.tsx +35 -0
  13. package/docs/content/docs/API_AUTHENTICATION.md +91 -0
  14. package/docs/content/docs/DEPLOYMENT.md +181 -0
  15. package/docs/content/docs/api/post.mdx +35 -0
  16. package/docs/content/docs/api/verify.mdx +34 -0
  17. package/docs/content/docs/meta.json +8 -0
  18. package/docs/content/docs/verify-legal-name.md +339 -0
  19. package/docs/lib/source.ts +14 -0
  20. package/docs/mdx-components.tsx +12 -0
  21. package/docs/next.config.mjs +51 -0
  22. package/docs/openapi.json +329 -0
  23. package/docs/package.json +37 -0
  24. package/docs/postcss.config.mjs +5 -0
  25. package/docs/scripts/generate-docs.mjs +23 -0
  26. package/docs/source.config.ts +5 -0
  27. package/docs/tsconfig.json +29 -0
  28. package/docs/worker.js +35 -0
  29. package/docs/wrangler.toml +26 -0
  30. package/examples/client.js +119 -0
  31. package/examples/demo.html +325 -0
  32. package/examples/libphonenumber-example.js +120 -0
  33. package/openapi.json +329 -0
  34. package/package.json +71 -0
  35. package/scripts/deploy.sh +63 -0
  36. package/src/identity-verification-server.ts +553 -0
  37. package/src/index.js +8 -0
  38. package/src/sns.js +236 -0
  39. package/src/verify-phone-server.js +448 -0
  40. package/src/verify-phone.ts +551 -0
  41. package/test/api.test.js +201 -0
  42. package/test/integration.test.js +152 -0
  43. package/test/metadata-test.js +73 -0
  44. package/test/server.test.js +143 -0
  45. package/test/setup.js +32 -0
  46. package/test/utils.test.js +186 -0
  47. package/test/verify.test.js +23 -0
  48. package/test/voip.test.js +112 -0
  49. package/vitest.config.js +10 -0
  50. package/wrangler.toml +27 -0
package/src/sns.js ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * AWS SNS HTTP API Client for Cloudflare Workers
3
+ * @class SNSClient
4
+ */
5
+ class SNSClient {
6
+ /**
7
+ * Create a new SNS client instance
8
+ * @param {Object} options - Configuration options
9
+ * @param {string} options.accessKeyId - AWS access key ID
10
+ * @param {string} options.secretAccessKey - AWS secret access key
11
+ * @param {string} [options.region='us-east-1'] - AWS region
12
+ */
13
+ constructor(options = {}) {
14
+ this.accessKeyId = options.accessKeyId;
15
+ this.secretAccessKey = options.secretAccessKey;
16
+ this.region = options.region || 'us-east-1';
17
+ this.endpoint = `https://sns.${this.region}.amazonaws.com`;
18
+ }
19
+
20
+ // Convert string to Uint8Array
21
+ stringToUint8Array(str) {
22
+ return new TextEncoder().encode(str);
23
+ }
24
+
25
+ // Convert Uint8Array to hex string
26
+ arrayBufferToHex(buffer) {
27
+ return Array.from(new Uint8Array(buffer))
28
+ .map(b => b.toString(16).padStart(2, '0'))
29
+ .join('');
30
+ }
31
+
32
+ // AWS Signature Version 4 signing using Web Crypto API
33
+ async sign(method, url, headers, payload) {
34
+ const now = new Date();
35
+ const amzDate = now.toISOString().replace(/[:\-]|\.\d{3}/g, '');
36
+ const dateStamp = amzDate.substr(0, 8);
37
+
38
+ // Create canonical request
39
+ const canonicalUri = '/';
40
+ const canonicalQuerystring = url.split('?')[1] || '';
41
+
42
+ // Add required headers
43
+ headers['host'] = `sns.${this.region}.amazonaws.com`;
44
+ headers['x-amz-date'] = amzDate;
45
+
46
+ // Calculate payload hash
47
+ const payloadHash = await crypto.subtle.digest('SHA-256', this.stringToUint8Array(payload))
48
+ .then(buffer => this.arrayBufferToHex(buffer));
49
+ headers['x-amz-content-sha256'] = payloadHash;
50
+
51
+ // Create canonical headers
52
+ const sortedHeaders = Object.keys(headers).sort().map(key =>
53
+ `${key.toLowerCase()}:${headers[key]}`
54
+ ).join('\n');
55
+
56
+ const signedHeaders = Object.keys(headers).sort().map(key =>
57
+ key.toLowerCase()
58
+ ).join(';');
59
+
60
+ const canonicalRequest = [
61
+ method,
62
+ canonicalUri,
63
+ canonicalQuerystring,
64
+ sortedHeaders,
65
+ '',
66
+ signedHeaders,
67
+ payloadHash
68
+ ].join('\n');
69
+
70
+ // Create string to sign
71
+ const algorithm = 'AWS4-HMAC-SHA256';
72
+ const credentialScope = `${dateStamp}/${this.region}/sns/aws4_request`;
73
+
74
+ const canonicalRequestHash = await crypto.subtle.digest('SHA-256', this.stringToUint8Array(canonicalRequest))
75
+ .then(buffer => this.arrayBufferToHex(buffer));
76
+
77
+ const stringToSign = [
78
+ algorithm,
79
+ amzDate,
80
+ credentialScope,
81
+ canonicalRequestHash
82
+ ].join('\n');
83
+
84
+ // Calculate signature using Web Crypto API
85
+ const kDate = await crypto.subtle.importKey(
86
+ 'raw',
87
+ this.stringToUint8Array(`AWS4${this.secretAccessKey}`),
88
+ { name: 'HMAC', hash: 'SHA-256' },
89
+ false,
90
+ ['sign']
91
+ ).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array(dateStamp)));
92
+
93
+ const kRegion = await crypto.subtle.importKey(
94
+ 'raw',
95
+ kDate,
96
+ { name: 'HMAC', hash: 'SHA-256' },
97
+ false,
98
+ ['sign']
99
+ ).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array(this.region)));
100
+
101
+ const kService = await crypto.subtle.importKey(
102
+ 'raw',
103
+ kRegion,
104
+ { name: 'HMAC', hash: 'SHA-256' },
105
+ false,
106
+ ['sign']
107
+ ).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array('sns')));
108
+
109
+ const kSigning = await crypto.subtle.importKey(
110
+ 'raw',
111
+ kService,
112
+ { name: 'HMAC', hash: 'SHA-256' },
113
+ false,
114
+ ['sign']
115
+ ).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array('aws4_request')));
116
+
117
+ const signature = await crypto.subtle.importKey(
118
+ 'raw',
119
+ kSigning,
120
+ { name: 'HMAC', hash: 'SHA-256' },
121
+ false,
122
+ ['sign']
123
+ ).then(key => crypto.subtle.sign('HMAC', key, this.stringToUint8Array(stringToSign)))
124
+ .then(buffer => this.arrayBufferToHex(buffer));
125
+
126
+ // Create authorization header
127
+ headers['authorization'] = `${algorithm} Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
128
+
129
+ return headers;
130
+ }
131
+
132
+ // Make HTTP request to SNS
133
+ async makeRequest(action, params = {}) {
134
+ const queryParams = new URLSearchParams({
135
+ Action: action,
136
+ Version: '2010-03-31',
137
+ ...params
138
+ });
139
+
140
+ const url = `${this.endpoint}/?${queryParams.toString()}`;
141
+ const payload = '';
142
+ const headers = {
143
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
144
+ };
145
+
146
+ // Sign the request
147
+ const signedHeaders = await this.sign('GET', url, headers, payload);
148
+
149
+ try {
150
+ const response = await fetch(url, {
151
+ method: 'GET',
152
+ headers: signedHeaders
153
+ });
154
+
155
+ const text = await response.text();
156
+
157
+ if (!response.ok) {
158
+ throw new Error(`SNS API Error: ${response.status} - ${text}`);
159
+ }
160
+
161
+ return this.parseXMLResponse(text);
162
+ } catch (error) {
163
+ throw new Error(`SNS Request failed: ${error.message}`);
164
+ }
165
+ }
166
+
167
+ // Parse XML response (simplified for Cloudflare Workers)
168
+ parseXMLResponse(xmlText) {
169
+ // Simple XML parsing for common SNS responses
170
+ // Note: DOMParser is not available in Cloudflare Workers, so we'll use regex parsing
171
+ // This is a simplified parser for SNS responses
172
+
173
+ // Handle common SNS responses using regex parsing
174
+ const messageIdMatch = xmlText.match(/<MessageId>([^<]+)<\/MessageId>/);
175
+ const topicArnMatch = xmlText.match(/<TopicArn>([^<]+)<\/TopicArn>/);
176
+ const subscriptionArnMatch = xmlText.match(/<SubscriptionArn>([^<]+)<\/SubscriptionArn>/);
177
+
178
+ if (messageIdMatch) {
179
+ return { MessageId: messageIdMatch[1] };
180
+ }
181
+
182
+ if (topicArnMatch) {
183
+ return { TopicArn: topicArnMatch[1] };
184
+ }
185
+
186
+ if (subscriptionArnMatch) {
187
+ return { SubscriptionArn: subscriptionArnMatch[1] };
188
+ }
189
+
190
+ // Fallback: return raw text for unknown responses
191
+ return { raw: xmlText };
192
+ }
193
+ }
194
+
195
+ // Global SNS client instance
196
+ let defaultClient = null;
197
+
198
+ // Create SNS client
199
+ export function createClient(options = {}) {
200
+ defaultClient = new SNSClient(options);
201
+ return defaultClient;
202
+ }
203
+
204
+ // Send SMS function
205
+ export function sendSMS(textmessage, phone, senderid, SMSType, callback, client = null) {
206
+ const snsClient = client || defaultClient;
207
+
208
+ if (!snsClient) {
209
+ return callback({ err: new Error('SNS client not initialized. Call createClient() first.') });
210
+ }
211
+
212
+ const params = {
213
+ Message: textmessage,
214
+ PhoneNumber: phone,
215
+ 'MessageAttributes.entry.1.Name': 'AWS.SNS.SMS.SenderID',
216
+ 'MessageAttributes.entry.1.Value.DataType': 'String',
217
+ 'MessageAttributes.entry.1.Value.StringValue': senderid,
218
+ 'MessageAttributes.entry.2.Name': 'AWS.SNS.SMS.SMSType',
219
+ 'MessageAttributes.entry.2.Value.DataType': 'String',
220
+ 'MessageAttributes.entry.2.Value.StringValue': SMSType
221
+ };
222
+
223
+ snsClient.makeRequest('Publish', params)
224
+ .then(response => {
225
+ callback(undefined, response.MessageId);
226
+ })
227
+ .catch(error => {
228
+ callback({ err: error, 'err.stack': error.stack });
229
+ });
230
+ }
231
+
232
+ // Export for compatibility
233
+ export default {
234
+ createClient,
235
+ sendSMS
236
+ };
@@ -0,0 +1,448 @@
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 { Hono } from "hono";
14
+ import { cors } from "hono/cors";
15
+ import { logger } from "hono/logger";
16
+ import { secureHeaders } from "hono/secure-headers";
17
+ import { rateLimiter } from "hono-rate-limiter";
18
+ import { swaggerUI } from "@hono/swagger-ui";
19
+ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
20
+ import verifyPhone from "./verify-phone.ts";
21
+
22
+ // Create the main app
23
+ const app = new OpenAPIHono();
24
+
25
+ // Middleware
26
+ app.use("*", logger());
27
+ 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
+ }));
34
+
35
+ // 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
+ });
43
+
44
+ // Apply rate limiting only when the middleware is actually used
45
+ app.use("*", async (c, next) => {
46
+ const limiter = createRateLimiter();
47
+ return limiter(c, next);
48
+ });
49
+
50
+ // 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 ", "");
53
+ const expectedApiKey = c.env?.API_KEY;
54
+
55
+ if (!apiKey || apiKey !== expectedApiKey) {
56
+ return c.json({
57
+ success: false,
58
+ error: "Unauthorized",
59
+ message: "Invalid or missing API key"
60
+ }, 401);
61
+ }
62
+
63
+ await next();
64
+ };
65
+
66
+ // Health check endpoint
67
+ app.get("/", (c) => {
68
+ return c.json({
69
+ success: true,
70
+ message: "SMS Verification API",
71
+ version: "1.0.0",
72
+ endpoints: {
73
+ health: "/health",
74
+ send: "/api/send",
75
+ verify: "/api/verify",
76
+ docs: "/docs"
77
+ }
78
+ });
79
+ });
80
+
81
+ // Health check
82
+ app.get("/health", (c) => {
83
+ return c.json({
84
+ success: true,
85
+ status: "healthy",
86
+ timestamp: new Date().toISOString(),
87
+ uptime: "N/A" // process.uptime() not available in Cloudflare Workers
88
+ });
89
+ });
90
+
91
+ // Generate verification code
92
+ function generateCode(length = 6) {
93
+ const chars = "0123456789";
94
+ let result = "";
95
+ for (let i = 0; i < length; i++) {
96
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
97
+ }
98
+ return result;
99
+ }
100
+
101
+ // Send SMS verification code
102
+ const sendRoute = createRoute({
103
+ method: "post",
104
+ path: "/api/send",
105
+ security: [{ apiKey: [] }],
106
+ request: {
107
+ body: {
108
+ content: {
109
+ "application/json": {
110
+ schema: z.object({
111
+ phoneNumber: z.string().min(1, "Phone number is required"),
112
+ code: z.string().optional(),
113
+ blockVoip: z.boolean().optional().default(false),
114
+ senderId: z.string().optional().default("Verify"),
115
+ messageTemplate: z.string().optional(),
116
+ smsType: z.enum(["Transactional", "Promotional"]).optional().default("Transactional")
117
+ })
118
+ }
119
+ }
120
+ }
121
+ },
122
+ responses: {
123
+ 200: {
124
+ content: {
125
+ "application/json": {
126
+ schema: z.object({
127
+ success: z.boolean(),
128
+ message: z.string().optional(),
129
+ messageId: z.string().optional(),
130
+ code: z.string().optional(),
131
+ phoneNumber: z.string().optional(),
132
+ expiresIn: z.number().optional(),
133
+ error: z.string().optional(),
134
+ details: z.string().optional(),
135
+ isVoip: z.boolean().optional()
136
+ })
137
+ }
138
+ },
139
+ description: "SMS sent successfully"
140
+ },
141
+ 400: {
142
+ content: {
143
+ "application/json": {
144
+ schema: z.object({
145
+ success: z.boolean(),
146
+ error: z.string(),
147
+ details: z.string().optional()
148
+ })
149
+ }
150
+ },
151
+ description: "Bad request"
152
+ },
153
+ 401: {
154
+ content: {
155
+ "application/json": {
156
+ schema: z.object({
157
+ success: z.boolean(),
158
+ error: z.string(),
159
+ message: z.string()
160
+ })
161
+ }
162
+ },
163
+ description: "Unauthorized"
164
+ }
165
+ }
166
+ });
167
+
168
+ app.openapi(sendRoute, async (c) => {
169
+ try {
170
+ const body = await c.req.json();
171
+ const { phoneNumber, code, blockVoip, senderId, messageTemplate, smsType } = body;
172
+
173
+ // Generate code if not provided
174
+ const verificationCode = code || generateCode();
175
+
176
+ // Get AWS credentials from environment
177
+ const awsCredentials = {
178
+ accessKeyId: c.env?.AWS_ACCESS_KEY_ID,
179
+ secretAccessKey: c.env?.AWS_SECRET_ACCESS_KEY,
180
+ awsRegion: c.env?.AWS_REGION || "us-east-1"
181
+ };
182
+
183
+ // Validate AWS credentials
184
+ 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);
190
+ }
191
+
192
+ // Send verification SMS
193
+ const result = await verifyPhone({
194
+ phoneNumber,
195
+ code: verificationCode,
196
+ ...awsCredentials,
197
+ blockVoip,
198
+ senderId: senderId || c.env?.SMS_SENDER_ID || "Verify",
199
+ messageTemplate,
200
+ smsType
201
+ });
202
+
203
+ if (result.success) {
204
+ return c.json({
205
+ success: true,
206
+ message: result.message,
207
+ messageId: result.messageId,
208
+ code: result.code,
209
+ phoneNumber: result.phoneNumber,
210
+ expiresIn: result.expiresIn
211
+ });
212
+ } else {
213
+ return c.json({
214
+ success: false,
215
+ error: result.error,
216
+ details: result.details,
217
+ isVoip: result.isVoip
218
+ }, 400);
219
+ }
220
+
221
+ } catch (error) {
222
+ return c.json({
223
+ success: false,
224
+ error: "Internal server error",
225
+ details: error.message
226
+ }, 500);
227
+ }
228
+ });
229
+
230
+ // Verify SMS code (mock endpoint for demonstration)
231
+ const verifyRoute = createRoute({
232
+ method: "post",
233
+ path: "/api/verify",
234
+ security: [{ apiKey: [] }],
235
+ request: {
236
+ body: {
237
+ content: {
238
+ "application/json": {
239
+ schema: z.object({
240
+ phoneNumber: z.string().min(1, "Phone number is required"),
241
+ code: z.string().min(1, "Verification code is required")
242
+ })
243
+ }
244
+ }
245
+ }
246
+ },
247
+ responses: {
248
+ 200: {
249
+ content: {
250
+ "application/json": {
251
+ schema: z.object({
252
+ success: z.boolean(),
253
+ message: z.string().optional(),
254
+ verified: z.boolean().optional(),
255
+ error: z.string().optional()
256
+ })
257
+ }
258
+ },
259
+ description: "Code verified successfully"
260
+ },
261
+ 400: {
262
+ content: {
263
+ "application/json": {
264
+ schema: z.object({
265
+ success: z.boolean(),
266
+ error: z.string()
267
+ })
268
+ }
269
+ },
270
+ description: "Bad request"
271
+ }
272
+ }
273
+ });
274
+
275
+ app.openapi(verifyRoute, async (c) => {
276
+ try {
277
+ const body = await c.req.json();
278
+ const { phoneNumber, code } = body;
279
+
280
+ // This is a mock verification - in a real app, you'd store codes in a database
281
+ // and verify them against stored values with proper expiration handling
282
+
283
+ // For demo purposes, we'll just return success
284
+ // In production, implement proper code storage and verification
285
+ return c.json({
286
+ success: true,
287
+ message: "Code verified successfully",
288
+ verified: true
289
+ });
290
+
291
+ } catch (error) {
292
+ return c.json({
293
+ success: false,
294
+ error: "Internal server error",
295
+ details: error.message
296
+ }, 500);
297
+ }
298
+ });
299
+
300
+ // General SMS sending endpoint
301
+ const generalSmsRoute = createRoute({
302
+ method: "post",
303
+ path: "/api/sms",
304
+ security: [{ apiKey: [] }],
305
+ request: {
306
+ body: {
307
+ content: {
308
+ "application/json": {
309
+ schema: z.object({
310
+ phoneNumber: z.string().min(1, "Phone number is required"),
311
+ message: z.string().min(1, "Message is required"),
312
+ senderId: z.string().optional().default("Verify"),
313
+ smsType: z.enum(["Transactional", "Promotional"]).optional().default("Transactional")
314
+ })
315
+ }
316
+ }
317
+ }
318
+ },
319
+ responses: {
320
+ 200: {
321
+ content: {
322
+ "application/json": {
323
+ schema: z.object({
324
+ success: z.boolean(),
325
+ message: z.string().optional(),
326
+ messageId: z.string().optional(),
327
+ phoneNumber: z.string().optional(),
328
+ error: z.string().optional(),
329
+ details: z.string().optional()
330
+ })
331
+ }
332
+ },
333
+ description: "SMS sent successfully"
334
+ }
335
+ }
336
+ });
337
+
338
+ app.openapi(generalSmsRoute, async (c) => {
339
+ try {
340
+ const body = await c.req.json();
341
+ const { phoneNumber, message, senderId, smsType } = body;
342
+
343
+ // Get AWS credentials from environment
344
+ const awsCredentials = {
345
+ accessKeyId: c.env?.AWS_ACCESS_KEY_ID,
346
+ secretAccessKey: c.env?.AWS_SECRET_ACCESS_KEY,
347
+ awsRegion: c.env?.AWS_REGION || "us-east-1"
348
+ };
349
+
350
+ // Validate AWS credentials
351
+ 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);
357
+ }
358
+
359
+ // Send general SMS
360
+ const result = await verifyPhone({
361
+ phoneNumber,
362
+ code: "GENERAL", // Use a placeholder code for general SMS
363
+ ...awsCredentials,
364
+ blockVoip: false,
365
+ senderId: senderId || c.env?.SMS_SENDER_ID || "Verify",
366
+ messageTemplate: message,
367
+ smsType
368
+ });
369
+
370
+ if (result.success) {
371
+ return c.json({
372
+ success: true,
373
+ message: "SMS sent successfully",
374
+ messageId: result.messageId,
375
+ phoneNumber: result.phoneNumber
376
+ });
377
+ } else {
378
+ return c.json({
379
+ success: false,
380
+ error: result.error,
381
+ details: result.details
382
+ }, 400);
383
+ }
384
+
385
+ } catch (error) {
386
+ return c.json({
387
+ success: false,
388
+ error: "Internal server error",
389
+ details: error.message
390
+ }, 500);
391
+ }
392
+ });
393
+
394
+ // OpenAPI documentation
395
+ app.doc("/docs", {
396
+ openapi: "3.0.0",
397
+ info: {
398
+ title: "SMS Verification API",
399
+ version: "1.0.0",
400
+ description: "API for sending SMS verification codes using AWS SNS"
401
+ },
402
+ servers: [
403
+ {
404
+ url: "https://sms-verification-api.your-subdomain.workers.dev",
405
+ description: "Production server"
406
+ },
407
+ {
408
+ url: "http://localhost:8787",
409
+ description: "Development server"
410
+ }
411
+ ],
412
+ components: {
413
+ securitySchemes: {
414
+ apiKey: {
415
+ type: "apiKey",
416
+ name: "X-API-Key",
417
+ in: "header"
418
+ }
419
+ }
420
+ }
421
+ });
422
+
423
+ // Swagger UI
424
+ app.get("/docs", swaggerUI({ url: "/docs" }));
425
+
426
+ // Apply authentication to all API routes
427
+ app.use("/api/*", authenticateApiKey);
428
+
429
+ // Error handling
430
+ app.onError((err, c) => {
431
+ console.error("Server error:", err);
432
+ return c.json({
433
+ success: false,
434
+ error: "Internal server error",
435
+ details: err.message
436
+ }, 500);
437
+ });
438
+
439
+ // 404 handler
440
+ app.notFound((c) => {
441
+ return c.json({
442
+ success: false,
443
+ error: "Not found",
444
+ message: "The requested endpoint does not exist"
445
+ }, 404);
446
+ });
447
+
448
+ export default app;