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.
- package/CHANGELOG.md +71 -0
- package/examples/{client.js → client.ts} +105 -119
- package/examples/{libphonenumber-example.js → libphonenumber-example.ts} +1 -1
- package/package.json +16 -14
- package/src/identity-verification-server.ts +385 -261
- package/src/{index.js → index.ts} +1 -1
- package/src/sns.ts +265 -0
- package/src/{verify-phone-server.js → verify-phone-server.ts} +206 -151
- package/src/verify-phone.ts +48 -22
- package/test/{api.test.js → api.test.ts} +20 -16
- package/test/{integration.test.js → integration.test.ts} +10 -10
- package/test/{server.test.js → server.test.ts} +3 -3
- package/test/{verify.test.js → verify.test.ts} +20 -23
- package/test/{voip.test.js → voip.test.ts} +13 -12
- package/tsconfig.json +24 -0
- package/{vitest.config.js → vitest.config.ts} +1 -1
- package/wrangler.toml +1 -4
- package/src/sns.js +0 -236
- /package/test/{metadata-test.js → metadata-test.ts} +0 -0
- /package/test/{setup.js → setup.ts} +0 -0
- /package/test/{utils.test.js → utils.test.ts} +0 -0
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
|
+
};
|