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.
- package/.env.example +20 -0
- package/DEPLOYMENT.md +151 -0
- package/README.md +475 -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/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.js +119 -0
- package/examples/demo.html +325 -0
- package/examples/libphonenumber-example.js +120 -0
- package/openapi.json +329 -0
- package/package.json +71 -0
- package/scripts/deploy.sh +63 -0
- package/src/identity-verification-server.ts +553 -0
- package/src/index.js +8 -0
- package/src/sns.js +236 -0
- package/src/verify-phone-server.js +448 -0
- package/src/verify-phone.ts +551 -0
- package/test/api.test.js +201 -0
- package/test/integration.test.js +152 -0
- package/test/metadata-test.js +73 -0
- package/test/server.test.js +143 -0
- package/test/setup.js +32 -0
- package/test/utils.test.js +186 -0
- package/test/verify.test.js +23 -0
- package/test/voip.test.js +112 -0
- package/vitest.config.js +10 -0
- 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;
|