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.
@@ -3,6 +3,6 @@
3
3
  * This file is used by Cloudflare Workers and Wrangler.
4
4
  */
5
5
 
6
- import app from "./verify-phone-server.js";
6
+ import app from "./verify-phone-server";
7
7
 
8
8
  export default app;
package/src/sns.ts ADDED
@@ -0,0 +1,265 @@
1
+ /**
2
+ * AWS SNS HTTP API Client for Cloudflare Workers
3
+ */
4
+
5
+ interface SNSClientOptions {
6
+ accessKeyId?: string;
7
+ secretAccessKey?: string;
8
+ region?: string;
9
+ }
10
+
11
+ interface SNSResponse {
12
+ MessageId?: string;
13
+ TopicArn?: string;
14
+ SubscriptionArn?: string;
15
+ raw?: string;
16
+ }
17
+
18
+ type SendSMSCallback = (
19
+ err: { err: Error; "err.stack"?: string } | undefined,
20
+ messageId?: string,
21
+ ) => void;
22
+
23
+ class SNSClient {
24
+ accessKeyId?: string;
25
+ secretAccessKey?: string;
26
+ region: string;
27
+ endpoint: string;
28
+
29
+ constructor(options: SNSClientOptions = {}) {
30
+ this.accessKeyId = options.accessKeyId;
31
+ this.secretAccessKey = options.secretAccessKey;
32
+ this.region = options.region || "us-east-1";
33
+ this.endpoint = `https://sns.${this.region}.amazonaws.com`;
34
+ }
35
+
36
+ stringToUint8Array(str: string): Uint8Array {
37
+ return new TextEncoder().encode(str);
38
+ }
39
+
40
+ arrayBufferToHex(buffer: ArrayBuffer | Uint8Array): string {
41
+ return Array.from(new Uint8Array(buffer as ArrayBuffer))
42
+ .map((b) => b.toString(16).padStart(2, "0"))
43
+ .join("");
44
+ }
45
+
46
+ async sign(
47
+ method: string,
48
+ url: string,
49
+ headers: Record<string, string>,
50
+ payload: string,
51
+ ): Promise<Record<string, string>> {
52
+ const now = new Date();
53
+ const amzDate = now.toISOString().replace(/[:\-]|\.\d{3}/g, "");
54
+ const dateStamp = amzDate.substring(0, 8);
55
+
56
+ const canonicalUri = "/";
57
+ const canonicalQuerystring = url.split("?")[1] || "";
58
+
59
+ headers["host"] = `sns.${this.region}.amazonaws.com`;
60
+ headers["x-amz-date"] = amzDate;
61
+
62
+ const payloadHash = await crypto.subtle
63
+ .digest("SHA-256", this.stringToUint8Array(payload))
64
+ .then((buffer) => this.arrayBufferToHex(buffer));
65
+ headers["x-amz-content-sha256"] = payloadHash;
66
+
67
+ const sortedHeaders = Object.keys(headers)
68
+ .sort()
69
+ .map((key) => `${key.toLowerCase()}:${headers[key]}`)
70
+ .join("\n");
71
+
72
+ const signedHeaders = Object.keys(headers)
73
+ .sort()
74
+ .map((key) => key.toLowerCase())
75
+ .join(";");
76
+
77
+ const canonicalRequest = [
78
+ method,
79
+ canonicalUri,
80
+ canonicalQuerystring,
81
+ sortedHeaders,
82
+ "",
83
+ signedHeaders,
84
+ payloadHash,
85
+ ].join("\n");
86
+
87
+ const algorithm = "AWS4-HMAC-SHA256";
88
+ const credentialScope = `${dateStamp}/${this.region}/sns/aws4_request`;
89
+
90
+ const canonicalRequestHash = await crypto.subtle
91
+ .digest("SHA-256", this.stringToUint8Array(canonicalRequest))
92
+ .then((buffer) => this.arrayBufferToHex(buffer));
93
+
94
+ const stringToSign = [
95
+ algorithm,
96
+ amzDate,
97
+ credentialScope,
98
+ canonicalRequestHash,
99
+ ].join("\n");
100
+
101
+ const kDate = await crypto.subtle
102
+ .importKey(
103
+ "raw",
104
+ this.stringToUint8Array(`AWS4${this.secretAccessKey}`),
105
+ { name: "HMAC", hash: "SHA-256" },
106
+ false,
107
+ ["sign"],
108
+ )
109
+ .then((key) =>
110
+ crypto.subtle.sign("HMAC", key, this.stringToUint8Array(dateStamp)),
111
+ );
112
+
113
+ const kRegion = await crypto.subtle
114
+ .importKey("raw", kDate, { name: "HMAC", hash: "SHA-256" }, false, [
115
+ "sign",
116
+ ])
117
+ .then((key) =>
118
+ crypto.subtle.sign("HMAC", key, this.stringToUint8Array(this.region)),
119
+ );
120
+
121
+ const kService = await crypto.subtle
122
+ .importKey("raw", kRegion, { name: "HMAC", hash: "SHA-256" }, false, [
123
+ "sign",
124
+ ])
125
+ .then((key) =>
126
+ crypto.subtle.sign("HMAC", key, this.stringToUint8Array("sns")),
127
+ );
128
+
129
+ const kSigning = await crypto.subtle
130
+ .importKey("raw", kService, { name: "HMAC", hash: "SHA-256" }, false, [
131
+ "sign",
132
+ ])
133
+ .then((key) =>
134
+ crypto.subtle.sign(
135
+ "HMAC",
136
+ key,
137
+ this.stringToUint8Array("aws4_request"),
138
+ ),
139
+ );
140
+
141
+ const signature = await crypto.subtle
142
+ .importKey("raw", kSigning, { name: "HMAC", hash: "SHA-256" }, false, [
143
+ "sign",
144
+ ])
145
+ .then((key) =>
146
+ crypto.subtle.sign("HMAC", key, this.stringToUint8Array(stringToSign)),
147
+ )
148
+ .then((buffer) => this.arrayBufferToHex(buffer));
149
+
150
+ headers["authorization"] =
151
+ `${algorithm} Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
152
+
153
+ return headers;
154
+ }
155
+
156
+ async makeRequest(
157
+ action: string,
158
+ params: Record<string, string> = {},
159
+ ): Promise<SNSResponse> {
160
+ const queryParams = new URLSearchParams({
161
+ Action: action,
162
+ Version: "2010-03-31",
163
+ ...params,
164
+ });
165
+
166
+ const url = `${this.endpoint}/?${queryParams.toString()}`;
167
+ const payload = "";
168
+ const headers: Record<string, string> = {
169
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
170
+ };
171
+
172
+ const signedHeaders = await this.sign("GET", url, headers, payload);
173
+
174
+ try {
175
+ const response = await fetch(url, {
176
+ method: "GET",
177
+ headers: signedHeaders,
178
+ });
179
+
180
+ const text = await response.text();
181
+
182
+ if (!response.ok) {
183
+ throw new Error(`SNS API Error: ${response.status} - ${text}`);
184
+ }
185
+
186
+ return this.parseXMLResponse(text);
187
+ } catch (error) {
188
+ const message = error instanceof Error ? error.message : String(error);
189
+ throw new Error(`SNS Request failed: ${message}`);
190
+ }
191
+ }
192
+
193
+ parseXMLResponse(xmlText: string): SNSResponse {
194
+ const messageIdMatch = xmlText.match(/<MessageId>([^<]+)<\/MessageId>/);
195
+ const topicArnMatch = xmlText.match(/<TopicArn>([^<]+)<\/TopicArn>/);
196
+ const subscriptionArnMatch = xmlText.match(
197
+ /<SubscriptionArn>([^<]+)<\/SubscriptionArn>/,
198
+ );
199
+
200
+ if (messageIdMatch) {
201
+ return { MessageId: messageIdMatch[1] };
202
+ }
203
+
204
+ if (topicArnMatch) {
205
+ return { TopicArn: topicArnMatch[1] };
206
+ }
207
+
208
+ if (subscriptionArnMatch) {
209
+ return { SubscriptionArn: subscriptionArnMatch[1] };
210
+ }
211
+
212
+ return { raw: xmlText };
213
+ }
214
+ }
215
+
216
+ let defaultClient: SNSClient | null = null;
217
+
218
+ export function createClient(options: SNSClientOptions = {}): SNSClient {
219
+ defaultClient = new SNSClient(options);
220
+ return defaultClient;
221
+ }
222
+
223
+ export function sendSMS(
224
+ textmessage: string,
225
+ phone: string,
226
+ senderid: string,
227
+ SMSType: string,
228
+ callback: SendSMSCallback,
229
+ client: SNSClient | null = null,
230
+ ): void {
231
+ const snsClient = client || defaultClient;
232
+
233
+ if (!snsClient) {
234
+ return callback({
235
+ err: new Error("SNS client not initialized. Call createClient() first."),
236
+ });
237
+ }
238
+
239
+ const params: Record<string, string> = {
240
+ Message: textmessage,
241
+ PhoneNumber: phone,
242
+ "MessageAttributes.entry.1.Name": "AWS.SNS.SMS.SenderID",
243
+ "MessageAttributes.entry.1.Value.DataType": "String",
244
+ "MessageAttributes.entry.1.Value.StringValue": senderid,
245
+ "MessageAttributes.entry.2.Name": "AWS.SNS.SMS.SMSType",
246
+ "MessageAttributes.entry.2.Value.DataType": "String",
247
+ "MessageAttributes.entry.2.Value.StringValue": SMSType,
248
+ };
249
+
250
+ snsClient
251
+ .makeRequest("Publish", params)
252
+ .then((response) => {
253
+ callback(undefined, response.MessageId);
254
+ })
255
+ .catch((error: Error) => {
256
+ callback({ err: error, "err.stack": error.stack });
257
+ });
258
+ }
259
+
260
+ export { SNSClient };
261
+
262
+ export default {
263
+ createClient,
264
+ sendSMS,
265
+ };