angelos 1.0.0

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Angelos
2
+
3
+ This is an npm package.
package/dist/index.cjs ADDED
@@ -0,0 +1,201 @@
1
+ 'use strict';
2
+
3
+ var QRCode = require('qrcode');
4
+ var crypto2 = require('crypto');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var QRCode__default = /*#__PURE__*/_interopDefault(QRCode);
9
+ var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
10
+
11
+ // src/qr/qrGenerator.ts
12
+ var generateQrFromData = async (phoneNumber, token) => {
13
+ const encodedText = encodeURIComponent(token);
14
+ const dataLink = `https://wa.me/${phoneNumber}?text=${encodedText}`;
15
+ const deepLink = `whatsapp://send?phone=${phoneNumber}&text=${encodedText}`;
16
+ const qrLink = await QRCode__default.default.toDataURL(dataLink, {
17
+ width: 300,
18
+ margin: 2,
19
+ errorCorrectionLevel: "M"
20
+ });
21
+ const qrSvg = await QRCode__default.default.toString(dataLink, {
22
+ type: "svg",
23
+ margin: 2,
24
+ errorCorrectionLevel: "M"
25
+ });
26
+ return {
27
+ dataLink,
28
+ deepLink,
29
+ qrLink,
30
+ qrSvg
31
+ };
32
+ };
33
+ var verifyMetaSignature = (rawPayload, signatureHeader, secretKey) => {
34
+ if (!rawPayload || !signatureHeader || !secretKey) return false;
35
+ const [algorithm, signature] = signatureHeader.split("=");
36
+ const expectedSignature = crypto2__default.default.createHmac("sha256", secretKey).update(rawPayload).digest("hex");
37
+ const signatureBuffer = Buffer.from(signature, "utf8");
38
+ const expectedBuffer = Buffer.from(expectedSignature, "utf8");
39
+ if (signatureBuffer.length !== expectedBuffer.length) {
40
+ return false;
41
+ }
42
+ return crypto2__default.default.timingSafeEqual(signatureBuffer, expectedBuffer);
43
+ };
44
+
45
+ // src/webhooks/parser.ts
46
+ var parser = (jsonData) => {
47
+ try {
48
+ const entry = jsonData?.entry?.[0];
49
+ const change = entry?.changes?.[0];
50
+ const value = change?.value;
51
+ const message = value?.messages?.[0];
52
+ if (!message || message.type !== "text") return null;
53
+ return {
54
+ sendersPhoneNumber: message.from,
55
+ text: message.text.body,
56
+ messageId: message.id,
57
+ timestamp: message.timestamp
58
+ };
59
+ } catch (error) {
60
+ return null;
61
+ }
62
+ };
63
+
64
+ // src/keyGenerator/keyGenerator.ts
65
+ function generateKey(phoneNumber) {
66
+ return `wa_verify:${phoneNumber}`;
67
+ }
68
+
69
+ // src/whatsappverifier.ts
70
+ var MIN_TTL_SECONDS = 30;
71
+ var MAX_TTL_SECONDS = 3600;
72
+ var whatsappVerifier = class {
73
+ storage;
74
+ businessPhoneNumber;
75
+ waVerificationKey;
76
+ defaultTtl;
77
+ constructor(config) {
78
+ this.storage = config.storage;
79
+ this.businessPhoneNumber = config.businessPhoneNumber;
80
+ this.waVerificationKey = config.waVerificationKey;
81
+ this.defaultTtl = this.validateTtl(config.defaultTtl, "defaultTtl");
82
+ }
83
+ validateTtl(ttl, label) {
84
+ if (!Number.isFinite(ttl) || ttl < MIN_TTL_SECONDS || ttl > MAX_TTL_SECONDS) {
85
+ throw new RangeError(
86
+ `${label} must be between ${MIN_TTL_SECONDS} and ${MAX_TTL_SECONDS} seconds, got ${ttl}`
87
+ );
88
+ }
89
+ return ttl;
90
+ }
91
+ async createSession(params) {
92
+ const id = crypto2__default.default.randomUUID();
93
+ const cleanPhone = params.phoneNumber.replace(/\D/g, "");
94
+ const ttl = params.ttl ? this.validateTtl(params.ttl, "ttl") : this.defaultTtl;
95
+ const token = crypto2__default.default.randomInt(1e5, 999999).toString();
96
+ const expiresAt = ttl * 1e3 + Date.now();
97
+ const sessionData = {
98
+ sessionId: id,
99
+ expectedPhoneNumber: cleanPhone,
100
+ token,
101
+ expiresAt
102
+ };
103
+ const key = generateKey(cleanPhone);
104
+ try {
105
+ await this.storage.set(key, JSON.stringify(sessionData), expiresAt);
106
+ } catch (error) {
107
+ throw new Error(
108
+ `Failed to store session data: ${error instanceof Error ? error.message : error}`
109
+ );
110
+ }
111
+ const message = `VERIFY ${id} ${token} ${expiresAt}`;
112
+ const { dataLink, deepLink, qrLink, qrSvg } = await generateQrFromData(
113
+ this.businessPhoneNumber,
114
+ message
115
+ );
116
+ return {
117
+ id,
118
+ token,
119
+ dataLink,
120
+ deepLink,
121
+ qrLink,
122
+ qrSvg
123
+ };
124
+ }
125
+ async verifySession(params) {
126
+ const rawBody = params.rawBody;
127
+ const jsonData = params.jsonData;
128
+ const signature = params.signature;
129
+ const isSignatureValid = verifyMetaSignature(rawBody, signature, this.waVerificationKey);
130
+ if (!isSignatureValid) {
131
+ return {
132
+ success: false,
133
+ message: "Invalid webhook signature"
134
+ };
135
+ }
136
+ const parsedData = parser(jsonData);
137
+ if (!parsedData) {
138
+ return {
139
+ success: false,
140
+ message: "Invalid or unsupported webhook message"
141
+ };
142
+ }
143
+ const key = generateKey(parsedData.sendersPhoneNumber.replace(/\D/g, ""));
144
+ let userData;
145
+ try {
146
+ const data = await this.storage.get(key);
147
+ if (!data) {
148
+ return {
149
+ success: false,
150
+ message: "Session data not found"
151
+ };
152
+ }
153
+ userData = JSON.parse(data);
154
+ await this.storage.delete(key);
155
+ } catch (error) {
156
+ return {
157
+ success: false,
158
+ message: `Failed to get/delete session data: ${error instanceof Error ? error.message : error}`
159
+ };
160
+ }
161
+ const messageKeyword = parsedData.text.trim().split(" ");
162
+ messageKeyword[1];
163
+ const token = messageKeyword[2];
164
+ const expiresAt = messageKeyword[3];
165
+ const tokenBuffer = Buffer.from(token ?? "", "utf8");
166
+ const expectedTokenBuffer = Buffer.from(userData.token, "utf8");
167
+ const senderPhone = parsedData.sendersPhoneNumber.replace(/\D/g, "");
168
+ const phoneBuffer = Buffer.from(senderPhone, "utf8");
169
+ const expectedPhoneBuffer = Buffer.from(userData.expectedPhoneNumber, "utf8");
170
+ const isTokenValid = tokenBuffer.length === expectedTokenBuffer.length && crypto2__default.default.timingSafeEqual(tokenBuffer, expectedTokenBuffer);
171
+ const isPhoneValid = phoneBuffer.length === expectedPhoneBuffer.length && crypto2__default.default.timingSafeEqual(phoneBuffer, expectedPhoneBuffer);
172
+ if (!isTokenValid || !isPhoneValid || Number(expiresAt) < Date.now()) {
173
+ return {
174
+ success: false,
175
+ message: "Token is invalid or expired"
176
+ };
177
+ }
178
+ return {
179
+ success: true,
180
+ message: "Successfully parsed the data",
181
+ parsedData
182
+ };
183
+ }
184
+ handleWebhookVerification(query) {
185
+ const mode = query["hub.mode"];
186
+ const token = query["hub.verify_token"];
187
+ const challenge = query["hub.challenge"];
188
+ if (mode === "subscribe" && token === this.waVerificationKey) {
189
+ return { success: true, challenge };
190
+ }
191
+ return { success: false };
192
+ }
193
+ };
194
+ var whatsappverifier_default = whatsappVerifier;
195
+
196
+ // src/index.ts
197
+ var index_default = whatsappverifier_default;
198
+
199
+ module.exports = index_default;
200
+ //# sourceMappingURL=index.cjs.map
201
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/qr/qrGenerator.ts","../src/webhooks/signature.ts","../src/webhooks/parser.ts","../src/keyGenerator/keyGenerator.ts","../src/whatsappverifier.ts","../src/index.ts"],"names":["QRCode","crypto"],"mappings":"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","file":"index.cjs","sourcesContent":["import QRCode from \"qrcode\";\nimport { GeneratedOrCode } from \"../types/index.js\";\n\nexport const generateQrFromData = async (\n phoneNumber: string,\n token: string,\n): Promise<GeneratedOrCode> => {\n const encodedText = encodeURIComponent(token);\n const dataLink = `https://wa.me/${phoneNumber}?text=${encodedText}`;\n const deepLink = `whatsapp://send?phone=${phoneNumber}&text=${encodedText}`;\n\n const qrLink = await QRCode.toDataURL(dataLink, {\n width: 300,\n margin: 2,\n errorCorrectionLevel: \"M\",\n });\n\n const qrSvg = await QRCode.toString(dataLink, {\n type: \"svg\",\n margin: 2,\n errorCorrectionLevel: \"M\",\n });\n\n return {\n dataLink: dataLink,\n deepLink: deepLink,\n qrLink: qrLink,\n qrSvg: qrSvg,\n } as GeneratedOrCode;\n};\n","import crypto from \"node:crypto\";\n\nexport const verifyMetaSignature = (\n rawPayload: Buffer,\n signatureHeader: string,\n secretKey: string,\n) => {\n if (!rawPayload || !signatureHeader || !secretKey) return false;\n\n const [algorithm, signature] = signatureHeader.split(\"=\");\n\n const expectedSignature = crypto\n .createHmac(\"sha256\", secretKey)\n .update(rawPayload)\n .digest(\"hex\");\n\n const signatureBuffer = Buffer.from(signature, \"utf8\");\n const expectedBuffer = Buffer.from(expectedSignature, \"utf8\");\n if (signatureBuffer.length !== expectedBuffer.length) {\n return false;\n }\n\n return crypto.timingSafeEqual(signatureBuffer, expectedBuffer);\n};\n","import { ParsedData } from \"../types/index.js\";\nimport { MetaWebhookPayload } from \"../types/internal.js\";\n\nexport const parser = (jsonData: MetaWebhookPayload): ParsedData => {\n try {\n const entry = jsonData?.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n const message = value?.messages?.[0];\n\n if (!message || message.type !== \"text\") return null;\n\n return {\n sendersPhoneNumber: message.from,\n text: message.text.body,\n messageId: message.id,\n timestamp: message.timestamp,\n };\n } catch (error) {\n return null;\n }\n};\n","export function generateKey(phoneNumber: string): string {\n return `wa_verify:${phoneNumber}`;\n}\n","import { generateQrFromData } from \"./qr/qrGenerator.js\";\nimport {\n Config,\n CreateSessionConfig,\n CreateSessionResult,\n StorageAdapter,\n StorageData,\n VerifySession,\n VerifyWebhooks,\n WebhookVerificationQuery,\n WebhookVerificationResult,\n} from \"./types/index.js\";\nimport crypto from \"node:crypto\";\nimport { verifyMetaSignature } from \"./webhooks/signature.js\";\nimport { parser } from \"./webhooks/parser.js\";\nimport { generateKey } from \"./keyGenerator/keyGenerator.js\";\n\nconst MIN_TTL_SECONDS = 30;\nconst MAX_TTL_SECONDS = 3600;\n\nclass whatsappVerifier {\n private storage: StorageAdapter;\n private businessPhoneNumber: string;\n private waVerificationKey: string;\n private defaultTtl: number;\n\n constructor(config: Config) {\n this.storage = config.storage;\n this.businessPhoneNumber = config.businessPhoneNumber;\n this.waVerificationKey = config.waVerificationKey;\n this.defaultTtl = this.validateTtl(config.defaultTtl, \"defaultTtl\");\n }\n\n private validateTtl(ttl: number, label: string): number {\n if (!Number.isFinite(ttl) || ttl < MIN_TTL_SECONDS || ttl > MAX_TTL_SECONDS) {\n throw new RangeError(\n `${label} must be between ${MIN_TTL_SECONDS} and ${MAX_TTL_SECONDS} seconds, got ${ttl}`,\n );\n }\n return ttl;\n }\n\n public async createSession(params: CreateSessionConfig): Promise<CreateSessionResult> {\n const id = crypto.randomUUID();\n const cleanPhone = params.phoneNumber.replace(/\\D/g, \"\");\n const ttl = params.ttl ? this.validateTtl(params.ttl, \"ttl\") : this.defaultTtl;\n const token = crypto.randomInt(100000, 999999).toString();\n const expiresAt = ttl * 1000 + Date.now();\n\n const sessionData: StorageData = {\n sessionId: id,\n expectedPhoneNumber: cleanPhone,\n token: token,\n expiresAt: expiresAt,\n };\n\n const key = generateKey(cleanPhone);\n\n try {\n await this.storage.set(key, JSON.stringify(sessionData), expiresAt);\n } catch (error: unknown) {\n throw new Error(\n `Failed to store session data: ${error instanceof Error ? error.message : error}`,\n );\n }\n\n const message = `VERIFY ${id} ${token} ${expiresAt}`;\n const { dataLink, deepLink, qrLink, qrSvg } = await generateQrFromData(\n this.businessPhoneNumber,\n message,\n );\n\n return {\n id,\n token,\n dataLink,\n deepLink,\n qrLink,\n qrSvg,\n };\n }\n\n public async verifySession(params: VerifyWebhooks): Promise<VerifySession> {\n const rawBody = params.rawBody;\n const jsonData = params.jsonData;\n const signature = params.signature;\n\n const isSignatureValid = verifyMetaSignature(rawBody, signature, this.waVerificationKey);\n if (!isSignatureValid) {\n return {\n success: false,\n message: \"Invalid webhook signature\",\n };\n }\n\n const parsedData = parser(jsonData);\n if (!parsedData) {\n return {\n success: false,\n message: \"Invalid or unsupported webhook message\",\n };\n }\n\n const key = generateKey(parsedData.sendersPhoneNumber.replace(/\\D/g, \"\"));\n let userData;\n\n try {\n const data = await this.storage.get(key);\n if (!data) {\n return {\n success: false,\n message: \"Session data not found\",\n };\n }\n\n userData = JSON.parse(data) as StorageData;\n await this.storage.delete(key);\n } catch (error: unknown) {\n return {\n success: false,\n message: `Failed to get/delete session data: ${error instanceof Error ? error.message : error}`,\n };\n }\n\n const messageKeyword = parsedData.text.trim().split(\" \");\n const id = messageKeyword[1];\n const token = messageKeyword[2];\n const expiresAt = messageKeyword[3];\n\n const tokenBuffer = Buffer.from(token ?? \"\", \"utf8\");\n const expectedTokenBuffer = Buffer.from(userData.token, \"utf8\");\n\n const senderPhone = parsedData.sendersPhoneNumber.replace(/\\D/g, \"\");\n const phoneBuffer = Buffer.from(senderPhone, \"utf8\");\n const expectedPhoneBuffer = Buffer.from(userData.expectedPhoneNumber, \"utf8\");\n\n const isTokenValid =\n tokenBuffer.length === expectedTokenBuffer.length &&\n crypto.timingSafeEqual(tokenBuffer, expectedTokenBuffer);\n\n const isPhoneValid =\n phoneBuffer.length === expectedPhoneBuffer.length &&\n crypto.timingSafeEqual(phoneBuffer, expectedPhoneBuffer);\n\n if (!isTokenValid || !isPhoneValid || Number(expiresAt) < Date.now()) {\n return {\n success: false,\n message: \"Token is invalid or expired\",\n };\n }\n\n return {\n success: true,\n message: \"Successfully parsed the data\",\n parsedData: parsedData,\n };\n }\n\n public handleWebhookVerification(query: WebhookVerificationQuery): WebhookVerificationResult {\n const mode = query[\"hub.mode\"];\n const token = query[\"hub.verify_token\"];\n const challenge = query[\"hub.challenge\"];\n\n if (mode === \"subscribe\" && token === this.waVerificationKey) {\n return { success: true, challenge };\n }\n\n return { success: false };\n }\n}\n\nexport default whatsappVerifier;\n","import whatsappVerifier from \"./whatsappverifier.js\";\n\nexport default whatsappVerifier;\nexport * from \"./types/index.js\";\n"]}
@@ -0,0 +1,128 @@
1
+ interface MetaWebhookTextMessage {
2
+ from: string;
3
+ id: string;
4
+ timestamp: string;
5
+ type: "text";
6
+ text: {
7
+ body: string;
8
+ };
9
+ }
10
+ interface MetaWebhookOtherMessage {
11
+ from: string;
12
+ id: string;
13
+ timestamp: string;
14
+ type: "image" | "audio" | "document" | "video" | "sticker" | "button" | "interactive" | "unknown";
15
+ [key: string]: unknown;
16
+ }
17
+ type MetaWebhookMessage = MetaWebhookTextMessage | MetaWebhookOtherMessage;
18
+ interface MetaWebhookStatus {
19
+ id: string;
20
+ status: "sent" | "delivered" | "read" | "failed";
21
+ timestamp: string;
22
+ recipient_id: string;
23
+ }
24
+ interface MetaWebhookValue {
25
+ messaging_product: "whatsapp";
26
+ metadata: {
27
+ display_phone_number: string;
28
+ phone_number_id: string;
29
+ };
30
+ contacts?: {
31
+ profile: {
32
+ name: string;
33
+ };
34
+ wa_id: string;
35
+ }[];
36
+ messages?: MetaWebhookMessage[];
37
+ statuses?: MetaWebhookStatus[];
38
+ }
39
+ interface MetaWebhookChange {
40
+ field: "messages";
41
+ value: MetaWebhookValue;
42
+ }
43
+ interface MetaWebhookEntry {
44
+ id: string;
45
+ changes: MetaWebhookChange[];
46
+ }
47
+ interface MetaWebhookPayload {
48
+ object: "whatsapp_business_account";
49
+ entry?: MetaWebhookEntry[];
50
+ }
51
+
52
+ /**
53
+ * Types for configuration of new class
54
+ */
55
+ interface Config {
56
+ businessPhoneNumber: string;
57
+ waVerificationKey: string;
58
+ storage: any;
59
+ defaultTtl: number;
60
+ }
61
+ /**
62
+ * Data stored in external driver provided by adapter
63
+ */
64
+ interface StorageData {
65
+ sessionId: string;
66
+ expectedPhoneNumber: string;
67
+ token: string;
68
+ expiresAt: number;
69
+ }
70
+ interface StorageAdapter {
71
+ get(key: string): Promise<string> | string;
72
+ set(key: string, value: string, expiresAt: number): Promise<void> | void;
73
+ delete(key: string): Promise<void> | void;
74
+ }
75
+ interface CreateSessionConfig {
76
+ phoneNumber: string;
77
+ ttl?: number;
78
+ }
79
+ interface GeneratedOrCode {
80
+ dataLink: string;
81
+ deepLink: string;
82
+ qrLink: string;
83
+ qrSvg: string;
84
+ }
85
+ interface CreateSessionResult extends GeneratedOrCode {
86
+ id: string;
87
+ token: string;
88
+ }
89
+ interface VerifyWebhooks {
90
+ rawBody: Buffer;
91
+ signature: string;
92
+ jsonData: MetaWebhookPayload;
93
+ }
94
+ interface ParsedDataOutputs {
95
+ sendersPhoneNumber: string;
96
+ text: string;
97
+ messageId: string;
98
+ timestamp: string;
99
+ }
100
+ type ParsedData = ParsedDataOutputs | null;
101
+ interface VerifySession {
102
+ success: boolean;
103
+ message: string;
104
+ parsedData?: ParsedDataOutputs;
105
+ }
106
+ interface WebhookVerificationQuery {
107
+ "hub.mode": string;
108
+ "hub.verify_token": string;
109
+ "hub.challenge": string;
110
+ }
111
+ interface WebhookVerificationResult {
112
+ success: boolean;
113
+ challenge?: string;
114
+ }
115
+
116
+ declare class whatsappVerifier {
117
+ private storage;
118
+ private businessPhoneNumber;
119
+ private waVerificationKey;
120
+ private defaultTtl;
121
+ constructor(config: Config);
122
+ private validateTtl;
123
+ createSession(params: CreateSessionConfig): Promise<CreateSessionResult>;
124
+ verifySession(params: VerifyWebhooks): Promise<VerifySession>;
125
+ handleWebhookVerification(query: WebhookVerificationQuery): WebhookVerificationResult;
126
+ }
127
+
128
+ export { type Config, type CreateSessionConfig, type CreateSessionResult, type GeneratedOrCode, type ParsedData, type ParsedDataOutputs, type StorageAdapter, type StorageData, type VerifySession, type VerifyWebhooks, type WebhookVerificationQuery, type WebhookVerificationResult, whatsappVerifier as default };
@@ -0,0 +1,128 @@
1
+ interface MetaWebhookTextMessage {
2
+ from: string;
3
+ id: string;
4
+ timestamp: string;
5
+ type: "text";
6
+ text: {
7
+ body: string;
8
+ };
9
+ }
10
+ interface MetaWebhookOtherMessage {
11
+ from: string;
12
+ id: string;
13
+ timestamp: string;
14
+ type: "image" | "audio" | "document" | "video" | "sticker" | "button" | "interactive" | "unknown";
15
+ [key: string]: unknown;
16
+ }
17
+ type MetaWebhookMessage = MetaWebhookTextMessage | MetaWebhookOtherMessage;
18
+ interface MetaWebhookStatus {
19
+ id: string;
20
+ status: "sent" | "delivered" | "read" | "failed";
21
+ timestamp: string;
22
+ recipient_id: string;
23
+ }
24
+ interface MetaWebhookValue {
25
+ messaging_product: "whatsapp";
26
+ metadata: {
27
+ display_phone_number: string;
28
+ phone_number_id: string;
29
+ };
30
+ contacts?: {
31
+ profile: {
32
+ name: string;
33
+ };
34
+ wa_id: string;
35
+ }[];
36
+ messages?: MetaWebhookMessage[];
37
+ statuses?: MetaWebhookStatus[];
38
+ }
39
+ interface MetaWebhookChange {
40
+ field: "messages";
41
+ value: MetaWebhookValue;
42
+ }
43
+ interface MetaWebhookEntry {
44
+ id: string;
45
+ changes: MetaWebhookChange[];
46
+ }
47
+ interface MetaWebhookPayload {
48
+ object: "whatsapp_business_account";
49
+ entry?: MetaWebhookEntry[];
50
+ }
51
+
52
+ /**
53
+ * Types for configuration of new class
54
+ */
55
+ interface Config {
56
+ businessPhoneNumber: string;
57
+ waVerificationKey: string;
58
+ storage: any;
59
+ defaultTtl: number;
60
+ }
61
+ /**
62
+ * Data stored in external driver provided by adapter
63
+ */
64
+ interface StorageData {
65
+ sessionId: string;
66
+ expectedPhoneNumber: string;
67
+ token: string;
68
+ expiresAt: number;
69
+ }
70
+ interface StorageAdapter {
71
+ get(key: string): Promise<string> | string;
72
+ set(key: string, value: string, expiresAt: number): Promise<void> | void;
73
+ delete(key: string): Promise<void> | void;
74
+ }
75
+ interface CreateSessionConfig {
76
+ phoneNumber: string;
77
+ ttl?: number;
78
+ }
79
+ interface GeneratedOrCode {
80
+ dataLink: string;
81
+ deepLink: string;
82
+ qrLink: string;
83
+ qrSvg: string;
84
+ }
85
+ interface CreateSessionResult extends GeneratedOrCode {
86
+ id: string;
87
+ token: string;
88
+ }
89
+ interface VerifyWebhooks {
90
+ rawBody: Buffer;
91
+ signature: string;
92
+ jsonData: MetaWebhookPayload;
93
+ }
94
+ interface ParsedDataOutputs {
95
+ sendersPhoneNumber: string;
96
+ text: string;
97
+ messageId: string;
98
+ timestamp: string;
99
+ }
100
+ type ParsedData = ParsedDataOutputs | null;
101
+ interface VerifySession {
102
+ success: boolean;
103
+ message: string;
104
+ parsedData?: ParsedDataOutputs;
105
+ }
106
+ interface WebhookVerificationQuery {
107
+ "hub.mode": string;
108
+ "hub.verify_token": string;
109
+ "hub.challenge": string;
110
+ }
111
+ interface WebhookVerificationResult {
112
+ success: boolean;
113
+ challenge?: string;
114
+ }
115
+
116
+ declare class whatsappVerifier {
117
+ private storage;
118
+ private businessPhoneNumber;
119
+ private waVerificationKey;
120
+ private defaultTtl;
121
+ constructor(config: Config);
122
+ private validateTtl;
123
+ createSession(params: CreateSessionConfig): Promise<CreateSessionResult>;
124
+ verifySession(params: VerifyWebhooks): Promise<VerifySession>;
125
+ handleWebhookVerification(query: WebhookVerificationQuery): WebhookVerificationResult;
126
+ }
127
+
128
+ export { type Config, type CreateSessionConfig, type CreateSessionResult, type GeneratedOrCode, type ParsedData, type ParsedDataOutputs, type StorageAdapter, type StorageData, type VerifySession, type VerifyWebhooks, type WebhookVerificationQuery, type WebhookVerificationResult, whatsappVerifier as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,194 @@
1
+ import QRCode from 'qrcode';
2
+ import crypto2 from 'crypto';
3
+
4
+ // src/qr/qrGenerator.ts
5
+ var generateQrFromData = async (phoneNumber, token) => {
6
+ const encodedText = encodeURIComponent(token);
7
+ const dataLink = `https://wa.me/${phoneNumber}?text=${encodedText}`;
8
+ const deepLink = `whatsapp://send?phone=${phoneNumber}&text=${encodedText}`;
9
+ const qrLink = await QRCode.toDataURL(dataLink, {
10
+ width: 300,
11
+ margin: 2,
12
+ errorCorrectionLevel: "M"
13
+ });
14
+ const qrSvg = await QRCode.toString(dataLink, {
15
+ type: "svg",
16
+ margin: 2,
17
+ errorCorrectionLevel: "M"
18
+ });
19
+ return {
20
+ dataLink,
21
+ deepLink,
22
+ qrLink,
23
+ qrSvg
24
+ };
25
+ };
26
+ var verifyMetaSignature = (rawPayload, signatureHeader, secretKey) => {
27
+ if (!rawPayload || !signatureHeader || !secretKey) return false;
28
+ const [algorithm, signature] = signatureHeader.split("=");
29
+ const expectedSignature = crypto2.createHmac("sha256", secretKey).update(rawPayload).digest("hex");
30
+ const signatureBuffer = Buffer.from(signature, "utf8");
31
+ const expectedBuffer = Buffer.from(expectedSignature, "utf8");
32
+ if (signatureBuffer.length !== expectedBuffer.length) {
33
+ return false;
34
+ }
35
+ return crypto2.timingSafeEqual(signatureBuffer, expectedBuffer);
36
+ };
37
+
38
+ // src/webhooks/parser.ts
39
+ var parser = (jsonData) => {
40
+ try {
41
+ const entry = jsonData?.entry?.[0];
42
+ const change = entry?.changes?.[0];
43
+ const value = change?.value;
44
+ const message = value?.messages?.[0];
45
+ if (!message || message.type !== "text") return null;
46
+ return {
47
+ sendersPhoneNumber: message.from,
48
+ text: message.text.body,
49
+ messageId: message.id,
50
+ timestamp: message.timestamp
51
+ };
52
+ } catch (error) {
53
+ return null;
54
+ }
55
+ };
56
+
57
+ // src/keyGenerator/keyGenerator.ts
58
+ function generateKey(phoneNumber) {
59
+ return `wa_verify:${phoneNumber}`;
60
+ }
61
+
62
+ // src/whatsappverifier.ts
63
+ var MIN_TTL_SECONDS = 30;
64
+ var MAX_TTL_SECONDS = 3600;
65
+ var whatsappVerifier = class {
66
+ storage;
67
+ businessPhoneNumber;
68
+ waVerificationKey;
69
+ defaultTtl;
70
+ constructor(config) {
71
+ this.storage = config.storage;
72
+ this.businessPhoneNumber = config.businessPhoneNumber;
73
+ this.waVerificationKey = config.waVerificationKey;
74
+ this.defaultTtl = this.validateTtl(config.defaultTtl, "defaultTtl");
75
+ }
76
+ validateTtl(ttl, label) {
77
+ if (!Number.isFinite(ttl) || ttl < MIN_TTL_SECONDS || ttl > MAX_TTL_SECONDS) {
78
+ throw new RangeError(
79
+ `${label} must be between ${MIN_TTL_SECONDS} and ${MAX_TTL_SECONDS} seconds, got ${ttl}`
80
+ );
81
+ }
82
+ return ttl;
83
+ }
84
+ async createSession(params) {
85
+ const id = crypto2.randomUUID();
86
+ const cleanPhone = params.phoneNumber.replace(/\D/g, "");
87
+ const ttl = params.ttl ? this.validateTtl(params.ttl, "ttl") : this.defaultTtl;
88
+ const token = crypto2.randomInt(1e5, 999999).toString();
89
+ const expiresAt = ttl * 1e3 + Date.now();
90
+ const sessionData = {
91
+ sessionId: id,
92
+ expectedPhoneNumber: cleanPhone,
93
+ token,
94
+ expiresAt
95
+ };
96
+ const key = generateKey(cleanPhone);
97
+ try {
98
+ await this.storage.set(key, JSON.stringify(sessionData), expiresAt);
99
+ } catch (error) {
100
+ throw new Error(
101
+ `Failed to store session data: ${error instanceof Error ? error.message : error}`
102
+ );
103
+ }
104
+ const message = `VERIFY ${id} ${token} ${expiresAt}`;
105
+ const { dataLink, deepLink, qrLink, qrSvg } = await generateQrFromData(
106
+ this.businessPhoneNumber,
107
+ message
108
+ );
109
+ return {
110
+ id,
111
+ token,
112
+ dataLink,
113
+ deepLink,
114
+ qrLink,
115
+ qrSvg
116
+ };
117
+ }
118
+ async verifySession(params) {
119
+ const rawBody = params.rawBody;
120
+ const jsonData = params.jsonData;
121
+ const signature = params.signature;
122
+ const isSignatureValid = verifyMetaSignature(rawBody, signature, this.waVerificationKey);
123
+ if (!isSignatureValid) {
124
+ return {
125
+ success: false,
126
+ message: "Invalid webhook signature"
127
+ };
128
+ }
129
+ const parsedData = parser(jsonData);
130
+ if (!parsedData) {
131
+ return {
132
+ success: false,
133
+ message: "Invalid or unsupported webhook message"
134
+ };
135
+ }
136
+ const key = generateKey(parsedData.sendersPhoneNumber.replace(/\D/g, ""));
137
+ let userData;
138
+ try {
139
+ const data = await this.storage.get(key);
140
+ if (!data) {
141
+ return {
142
+ success: false,
143
+ message: "Session data not found"
144
+ };
145
+ }
146
+ userData = JSON.parse(data);
147
+ await this.storage.delete(key);
148
+ } catch (error) {
149
+ return {
150
+ success: false,
151
+ message: `Failed to get/delete session data: ${error instanceof Error ? error.message : error}`
152
+ };
153
+ }
154
+ const messageKeyword = parsedData.text.trim().split(" ");
155
+ messageKeyword[1];
156
+ const token = messageKeyword[2];
157
+ const expiresAt = messageKeyword[3];
158
+ const tokenBuffer = Buffer.from(token ?? "", "utf8");
159
+ const expectedTokenBuffer = Buffer.from(userData.token, "utf8");
160
+ const senderPhone = parsedData.sendersPhoneNumber.replace(/\D/g, "");
161
+ const phoneBuffer = Buffer.from(senderPhone, "utf8");
162
+ const expectedPhoneBuffer = Buffer.from(userData.expectedPhoneNumber, "utf8");
163
+ const isTokenValid = tokenBuffer.length === expectedTokenBuffer.length && crypto2.timingSafeEqual(tokenBuffer, expectedTokenBuffer);
164
+ const isPhoneValid = phoneBuffer.length === expectedPhoneBuffer.length && crypto2.timingSafeEqual(phoneBuffer, expectedPhoneBuffer);
165
+ if (!isTokenValid || !isPhoneValid || Number(expiresAt) < Date.now()) {
166
+ return {
167
+ success: false,
168
+ message: "Token is invalid or expired"
169
+ };
170
+ }
171
+ return {
172
+ success: true,
173
+ message: "Successfully parsed the data",
174
+ parsedData
175
+ };
176
+ }
177
+ handleWebhookVerification(query) {
178
+ const mode = query["hub.mode"];
179
+ const token = query["hub.verify_token"];
180
+ const challenge = query["hub.challenge"];
181
+ if (mode === "subscribe" && token === this.waVerificationKey) {
182
+ return { success: true, challenge };
183
+ }
184
+ return { success: false };
185
+ }
186
+ };
187
+ var whatsappverifier_default = whatsappVerifier;
188
+
189
+ // src/index.ts
190
+ var index_default = whatsappverifier_default;
191
+
192
+ export { index_default as default };
193
+ //# sourceMappingURL=index.mjs.map
194
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/qr/qrGenerator.ts","../src/webhooks/signature.ts","../src/webhooks/parser.ts","../src/keyGenerator/keyGenerator.ts","../src/whatsappverifier.ts","../src/index.ts"],"names":["crypto"],"mappings":"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","file":"index.mjs","sourcesContent":["import QRCode from \"qrcode\";\nimport { GeneratedOrCode } from \"../types/index.js\";\n\nexport const generateQrFromData = async (\n phoneNumber: string,\n token: string,\n): Promise<GeneratedOrCode> => {\n const encodedText = encodeURIComponent(token);\n const dataLink = `https://wa.me/${phoneNumber}?text=${encodedText}`;\n const deepLink = `whatsapp://send?phone=${phoneNumber}&text=${encodedText}`;\n\n const qrLink = await QRCode.toDataURL(dataLink, {\n width: 300,\n margin: 2,\n errorCorrectionLevel: \"M\",\n });\n\n const qrSvg = await QRCode.toString(dataLink, {\n type: \"svg\",\n margin: 2,\n errorCorrectionLevel: \"M\",\n });\n\n return {\n dataLink: dataLink,\n deepLink: deepLink,\n qrLink: qrLink,\n qrSvg: qrSvg,\n } as GeneratedOrCode;\n};\n","import crypto from \"node:crypto\";\n\nexport const verifyMetaSignature = (\n rawPayload: Buffer,\n signatureHeader: string,\n secretKey: string,\n) => {\n if (!rawPayload || !signatureHeader || !secretKey) return false;\n\n const [algorithm, signature] = signatureHeader.split(\"=\");\n\n const expectedSignature = crypto\n .createHmac(\"sha256\", secretKey)\n .update(rawPayload)\n .digest(\"hex\");\n\n const signatureBuffer = Buffer.from(signature, \"utf8\");\n const expectedBuffer = Buffer.from(expectedSignature, \"utf8\");\n if (signatureBuffer.length !== expectedBuffer.length) {\n return false;\n }\n\n return crypto.timingSafeEqual(signatureBuffer, expectedBuffer);\n};\n","import { ParsedData } from \"../types/index.js\";\nimport { MetaWebhookPayload } from \"../types/internal.js\";\n\nexport const parser = (jsonData: MetaWebhookPayload): ParsedData => {\n try {\n const entry = jsonData?.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n const message = value?.messages?.[0];\n\n if (!message || message.type !== \"text\") return null;\n\n return {\n sendersPhoneNumber: message.from,\n text: message.text.body,\n messageId: message.id,\n timestamp: message.timestamp,\n };\n } catch (error) {\n return null;\n }\n};\n","export function generateKey(phoneNumber: string): string {\n return `wa_verify:${phoneNumber}`;\n}\n","import { generateQrFromData } from \"./qr/qrGenerator.js\";\nimport {\n Config,\n CreateSessionConfig,\n CreateSessionResult,\n StorageAdapter,\n StorageData,\n VerifySession,\n VerifyWebhooks,\n WebhookVerificationQuery,\n WebhookVerificationResult,\n} from \"./types/index.js\";\nimport crypto from \"node:crypto\";\nimport { verifyMetaSignature } from \"./webhooks/signature.js\";\nimport { parser } from \"./webhooks/parser.js\";\nimport { generateKey } from \"./keyGenerator/keyGenerator.js\";\n\nconst MIN_TTL_SECONDS = 30;\nconst MAX_TTL_SECONDS = 3600;\n\nclass whatsappVerifier {\n private storage: StorageAdapter;\n private businessPhoneNumber: string;\n private waVerificationKey: string;\n private defaultTtl: number;\n\n constructor(config: Config) {\n this.storage = config.storage;\n this.businessPhoneNumber = config.businessPhoneNumber;\n this.waVerificationKey = config.waVerificationKey;\n this.defaultTtl = this.validateTtl(config.defaultTtl, \"defaultTtl\");\n }\n\n private validateTtl(ttl: number, label: string): number {\n if (!Number.isFinite(ttl) || ttl < MIN_TTL_SECONDS || ttl > MAX_TTL_SECONDS) {\n throw new RangeError(\n `${label} must be between ${MIN_TTL_SECONDS} and ${MAX_TTL_SECONDS} seconds, got ${ttl}`,\n );\n }\n return ttl;\n }\n\n public async createSession(params: CreateSessionConfig): Promise<CreateSessionResult> {\n const id = crypto.randomUUID();\n const cleanPhone = params.phoneNumber.replace(/\\D/g, \"\");\n const ttl = params.ttl ? this.validateTtl(params.ttl, \"ttl\") : this.defaultTtl;\n const token = crypto.randomInt(100000, 999999).toString();\n const expiresAt = ttl * 1000 + Date.now();\n\n const sessionData: StorageData = {\n sessionId: id,\n expectedPhoneNumber: cleanPhone,\n token: token,\n expiresAt: expiresAt,\n };\n\n const key = generateKey(cleanPhone);\n\n try {\n await this.storage.set(key, JSON.stringify(sessionData), expiresAt);\n } catch (error: unknown) {\n throw new Error(\n `Failed to store session data: ${error instanceof Error ? error.message : error}`,\n );\n }\n\n const message = `VERIFY ${id} ${token} ${expiresAt}`;\n const { dataLink, deepLink, qrLink, qrSvg } = await generateQrFromData(\n this.businessPhoneNumber,\n message,\n );\n\n return {\n id,\n token,\n dataLink,\n deepLink,\n qrLink,\n qrSvg,\n };\n }\n\n public async verifySession(params: VerifyWebhooks): Promise<VerifySession> {\n const rawBody = params.rawBody;\n const jsonData = params.jsonData;\n const signature = params.signature;\n\n const isSignatureValid = verifyMetaSignature(rawBody, signature, this.waVerificationKey);\n if (!isSignatureValid) {\n return {\n success: false,\n message: \"Invalid webhook signature\",\n };\n }\n\n const parsedData = parser(jsonData);\n if (!parsedData) {\n return {\n success: false,\n message: \"Invalid or unsupported webhook message\",\n };\n }\n\n const key = generateKey(parsedData.sendersPhoneNumber.replace(/\\D/g, \"\"));\n let userData;\n\n try {\n const data = await this.storage.get(key);\n if (!data) {\n return {\n success: false,\n message: \"Session data not found\",\n };\n }\n\n userData = JSON.parse(data) as StorageData;\n await this.storage.delete(key);\n } catch (error: unknown) {\n return {\n success: false,\n message: `Failed to get/delete session data: ${error instanceof Error ? error.message : error}`,\n };\n }\n\n const messageKeyword = parsedData.text.trim().split(\" \");\n const id = messageKeyword[1];\n const token = messageKeyword[2];\n const expiresAt = messageKeyword[3];\n\n const tokenBuffer = Buffer.from(token ?? \"\", \"utf8\");\n const expectedTokenBuffer = Buffer.from(userData.token, \"utf8\");\n\n const senderPhone = parsedData.sendersPhoneNumber.replace(/\\D/g, \"\");\n const phoneBuffer = Buffer.from(senderPhone, \"utf8\");\n const expectedPhoneBuffer = Buffer.from(userData.expectedPhoneNumber, \"utf8\");\n\n const isTokenValid =\n tokenBuffer.length === expectedTokenBuffer.length &&\n crypto.timingSafeEqual(tokenBuffer, expectedTokenBuffer);\n\n const isPhoneValid =\n phoneBuffer.length === expectedPhoneBuffer.length &&\n crypto.timingSafeEqual(phoneBuffer, expectedPhoneBuffer);\n\n if (!isTokenValid || !isPhoneValid || Number(expiresAt) < Date.now()) {\n return {\n success: false,\n message: \"Token is invalid or expired\",\n };\n }\n\n return {\n success: true,\n message: \"Successfully parsed the data\",\n parsedData: parsedData,\n };\n }\n\n public handleWebhookVerification(query: WebhookVerificationQuery): WebhookVerificationResult {\n const mode = query[\"hub.mode\"];\n const token = query[\"hub.verify_token\"];\n const challenge = query[\"hub.challenge\"];\n\n if (mode === \"subscribe\" && token === this.waVerificationKey) {\n return { success: true, challenge };\n }\n\n return { success: false };\n }\n}\n\nexport default whatsappVerifier;\n","import whatsappVerifier from \"./whatsappverifier.js\";\n\nexport default whatsappVerifier;\nexport * from \"./types/index.js\";\n"]}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "angelos",
3
+ "version": "1.0.0",
4
+ "description": "Frictionless WhatsApp QR-based phone verification engine for Node.js",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "dev": "tsup --watch",
23
+ "build": "tsup",
24
+ "typecheck": "tsc --noEmit",
25
+ "prepublishOnly": "npm run typecheck && npm run build"
26
+ },
27
+ "dependencies": {
28
+ "qrcode": "^1.5.4"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.20.5",
32
+ "@types/qrcode": "^1.5.5",
33
+ "tsup": "^8.3.0",
34
+ "typescript": "^5.5.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "author": "Hartejsingh Sandhu",
40
+ "license": "MIT"
41
+ }