auth-verify 1.1.0 → 1.2.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/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const JWTManager = require("./src/jwt");
2
2
  const OTPManager = require("./src/otp");
3
3
  const SessionManager = require("./src/session");
4
+ const OAuthManager = require("./src/oauth")
4
5
 
5
6
  class AuthVerify {
6
7
  constructor(options = {}) {
@@ -39,6 +40,8 @@ class AuthVerify {
39
40
  // }
40
41
  // }
41
42
  // ✅ No getters — directly reference otp.dev (it's a plain object)
43
+
44
+ this.oauth = new OAuthManager();
42
45
  }
43
46
 
44
47
  // Session helpers
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "uuid": "^13.0.0"
13
13
  },
14
14
  "name": "auth-verify",
15
- "version": "1.1.0",
15
+ "version": "1.2.1",
16
16
  "description": "A simple Node.js library for sending and verifying OTP via email",
17
17
  "main": "index.js",
18
18
  "scripts": {
@@ -37,7 +37,8 @@
37
37
  "two-factor",
38
38
  "2fa",
39
39
  "email",
40
- "jwt"
40
+ "jwt",
41
+ "oauth"
41
42
  ],
42
43
  "author": "Jahongir Sobirov",
43
44
  "license": "MIT",
package/readme.md CHANGED
@@ -1,17 +1,15 @@
1
1
  # auth-verify
2
2
 
3
3
  **auth-verify** is a Node.js authentication utility that provides:
4
- - Secure OTP (one-time password) generation and verification
5
- - Sending OTPs via Email, SMS (pluggable helpers), and Telegram bot
6
- - JWT creation, verification and optional token revocation with memory/Redis storage
7
- - Session management (in-memory or Redis)
8
- - Developer extensibility: custom senders and `auth.register.sender()` / `auth.use(name).send(...)`
9
-
10
- > This README documents the code structure and APIs found in the library files you provided (OTPManager, JWTManager, SessionManager, AuthVerify).
11
-
4
+ - Secure OTP (one-time password) generation and verification
5
+ - Sending OTPs via Email, SMS (pluggable helpers), and Telegram bot
6
+ - JWT creation, verification and optional token revocation with memory/Redis storage
7
+ - Session management (in-memory or Redis)
8
+ - New: OAuth 2.0 integration for Google, Facebook, GitHub, and X (Twitter)
9
+ - ⚙️ Developer extensibility: custom senders and `auth.register.sender()` / `auth.use(name).send(...)`
12
10
  ---
13
11
 
14
- ## Installation
12
+ ## 🧩 Installation
15
13
 
16
14
  ```bash
17
15
  # from npm (when published)
@@ -23,16 +21,16 @@ npm install auth-verify
23
21
 
24
22
  ---
25
23
 
26
- ## Quick overview
24
+ ## ⚙️ Quick overview
27
25
 
28
26
  - `AuthVerify` (entry): constructs and exposes `.jwt`, `.otp`, and (optionally) `.session` managers.
29
27
  - `JWTManager`: sign, verify, decode, revoke tokens. Supports `storeTokens: "memory" | "redis" | "none"`.
30
28
  - `OTPManager`: generate, store, send, verify, resend OTPs. Supports `storeTokens: "memory" | "redis" | "none"`. Supports email, SMS helper, Telegram bot, and custom dev senders.
31
29
  - `SessionManager`: simple session creation/verification/destroy with memory or Redis backend.
32
-
30
+ - `OAuthManager`: Handle OAuth 2.0 logins for Google, Facebook, GitHub, X
33
31
  ---
34
32
 
35
- ## Example: Initialize library (CommonJS)
33
+ ## 🚀 Example: Initialize library (CommonJS)
36
34
 
37
35
  ```js
38
36
  const AuthVerify = require('auth-verify');
@@ -48,11 +46,11 @@ const auth = new AuthVerify({
48
46
 
49
47
  ---
50
48
 
51
- ## JWT usage
49
+ ## 🔐 JWT Usage
52
50
 
53
51
  ```js
54
52
  // create JWT
55
- const token = await auth.jwt.sign({ userId: 123 }, '1h'); // expiry string or number (ms)
53
+ const token = await auth.jwt.sign({ userId: 123 }, '1h'); // expiry string or number (ms) (and also you can add '1m' (minute), '5s' (second) and '7d' (day))
56
54
  console.log('token', token);
57
55
 
58
56
  // verify
@@ -71,7 +69,7 @@ await auth.jwt.revokeUntil(token, '10m');
71
69
  // check if token is revoked (returns boolean)
72
70
  const isRevoked = await auth.jwt.isRevoked(token);
73
71
  ```
74
- ## 🍪 Automatic Cookie Handling (New in v1.1.0)
72
+ ### 🍪 Automatic Cookie Handling (v1.1.0+)
75
73
 
76
74
  You can now automatically store and verify JWTs via HTTP cookies — no need to manually send them!
77
75
  ```js
@@ -111,9 +109,9 @@ Notes:
111
109
 
112
110
  ---
113
111
 
114
- ## OTP (email / sms / telegram / custom sender)
112
+ ## 🔢 OTP (email / sms / telegram / custom sender)
115
113
 
116
- ### Configure sender
114
+ ### 🤝 Configure sender
117
115
 
118
116
  You can set the default sender (email/sms/telegram):
119
117
 
@@ -145,7 +143,7 @@ auth.otp.setSender({
145
143
  });
146
144
  ```
147
145
 
148
- ### Generate → Save → Send (chainable)
146
+ ### ⛓️ Generate → Save → Send (chainable)
149
147
 
150
148
  OTP generation is chainable: `generate()` returns the OTP manager instance.
151
149
 
@@ -176,7 +174,7 @@ await auth.otp.message({
176
174
  });
177
175
  ```
178
176
 
179
- ### Verify
177
+ ### ✔️ Verify
180
178
 
181
179
  ```js
182
180
  // Promise style
@@ -188,6 +186,11 @@ try {
188
186
  }
189
187
 
190
188
  // Callback style also supported: auth.otp.verify({check, code}, callback)
189
+ auth.otp.verify({ check: 'user@example.com', code: '123456' }, (err, isValid)=>{
190
+ if(err) console.log(err);
191
+ if(isValid) console.log('Correct code!');
192
+ else console.log('Incorrect code!');
193
+ });
191
194
  ```
192
195
 
193
196
  ### Resend and cooldown / max attempts
@@ -198,6 +201,148 @@ try {
198
201
 
199
202
  `resend` returns the new code (promise style) or calls callback.
200
203
 
204
+ ---
205
+ ## 🌍 OAuth 2.0 Integration (New in v1.2.0)
206
+ `auth.oauth` supports login via Google, Facebook, GitHub, X (Twitter) and Linkedin.
207
+ ### Example (Google Login with Express)
208
+ ```js
209
+ const express = require('express');
210
+ const AuthVerify = require("auth-verify");
211
+ const app = express();
212
+ app.use(express.json());
213
+ const auth = new AuthVerify({ jwtSecret: 's', storeTokens: 'memory'});
214
+
215
+ const google = auth.oauth.google({clientId: 'YOUR_CLIENT_ID', clientSecret: 'YOUR_CLIENT_SECRET', redirectUri: 'http://localhost:3000/auth/google/callback'});
216
+ app.get('/', async (req, res) => {
217
+ res.send(`
218
+ <h1>Login with Google</h1>
219
+ <a href="/auth/google">Login</a>
220
+ `);
221
+ });
222
+
223
+
224
+ app.get('/auth/google', (req, res) => google.redirect(res));
225
+
226
+ app.get('/auth/google/callback', async (req, res)=>{
227
+ const code = req.query.code;
228
+ try {
229
+ const user = await google.callback(code);
230
+ res.send(`
231
+ <h2>Welcome, ${user.name}!</h2>
232
+ <img src="${user.picture}" width="100" style="border-radius:50%">
233
+ <p>Email: ${user.email}</p>
234
+ <p>Access Token: ${user.access_token.slice(0, 20)}...</p>
235
+ `);
236
+ } catch(err){
237
+ res.status(500).send("Error: " + err.message);
238
+ }
239
+ });
240
+
241
+ app.listen(3000, ()=>{
242
+ console.log('Server is running...');
243
+ });
244
+ ```
245
+ ---
246
+ ### API documentation for OAuth
247
+ - `auth.oauth.google({...})` for making connection to your Google cloud app.
248
+ - `google.redirect(res)` for sending user/client to the Google OAuth page for verifying and selecting his accaount
249
+ - `google.callback(code)` for exchanging server code to the user/client token.
250
+
251
+ ### Other examples with other platforms
252
+ ```js
253
+ const express = require('express');
254
+ const AuthVerify = require("auth-verify");
255
+ const app = express();
256
+ app.use(express.json());
257
+ const auth = new AuthVerify({ jwtSecret: 's', storeTokens: 'memory'});
258
+
259
+ // --- Example: FACEBOOK LOGIN ---
260
+ const facebook = auth.oauth.facebook({
261
+ clientId: "YOUR_FB_APP_ID",
262
+ clientSecret: "YOUR_FB_APP_SECRET",
263
+ redirectUri: "http://localhost:3000/auth/facebook/callback",
264
+ });
265
+
266
+ // --- Example: GITHUB LOGIN ---
267
+ const github = auth.oauth.github({
268
+ clientId: "YOUR_GITHUB_CLIENT_ID",
269
+ clientSecret: "YOUR_GITHUB_CLIENT_SECRET",
270
+ redirectUri: "http://localhost:3000/auth/github/callback",
271
+ });
272
+
273
+ // --- Example: X (Twitter) LOGIN ---
274
+ const twitter = auth.oauth.x({
275
+ clientId: "YOUR_TWITTER_CLIENT_ID",
276
+ clientSecret: "YOUR_TWITTER_CLIENT_SECRET",
277
+ redirectUri: "http://localhost:3000/auth/x/callback",
278
+ });
279
+
280
+ // --- Example: Linkedin LOGIN ---
281
+ const linkedin = auth.oauth.linkedin({
282
+ clientId: "YOUR_LINKEDIN_CLIENT_ID",
283
+ clientSecret: "YOUR_LINKEDIN_CLIENT_SECRET",
284
+ redirectUri: "http://localhost:3000/auth/linkedin/callback"
285
+ });
286
+
287
+
288
+ // ===== FACEBOOK ROUTES =====
289
+ app.get("/auth/facebook", (req, res) => facebook.redirect(res));
290
+
291
+ app.get("/auth/facebook/callback", async (req, res) => {
292
+ try {
293
+ const { code } = req.query;
294
+ const user = await facebook.callback(code);
295
+ res.json({ success: true, provider: "facebook", user });
296
+ } catch (err) {
297
+ res.status(400).json({ error: err.message });
298
+ }
299
+ });
300
+
301
+
302
+ // ===== GITHUB ROUTES =====
303
+ app.get("/auth/github", (req, res) => github.redirect(res));
304
+
305
+ app.get("/auth/github/callback", async (req, res) => {
306
+ try {
307
+ const { code } = req.query;
308
+ const user = await github.callback(code);
309
+ res.json({ success: true, provider: "github", user });
310
+ } catch (err) {
311
+ res.status(400).json({ error: err.message });
312
+ }
313
+ });
314
+
315
+
316
+ // ===== X (TWITTER) ROUTES =====
317
+ app.get("/auth/x", (req, res) => twitter.redirect(res));
318
+
319
+ app.get("/auth/x/callback", async (req, res) => {
320
+ try {
321
+ const { code } = req.query;
322
+ const user = await twitter.callback(code);
323
+ res.json({ success: true, provider: "x", user });
324
+ } catch (err) {
325
+ res.status(400).json({ error: err.message });
326
+ }
327
+ });
328
+
329
+ // ==== LINKEDIN ROUTES ====
330
+ app.get("/auth/linkedin", (req, res) => linkedin.redirect(res));
331
+
332
+ app.get("/auth/linkedin/callback", async (req, res)=>{
333
+ try{
334
+ const { code } = req.query;
335
+ const user = await linkedin.callback(code);
336
+ res.json({ success: true, provider: "x", user });
337
+ }catch(err){
338
+ res.status(400).json({ error: err.message });
339
+ }
340
+ });
341
+
342
+
343
+ app.listen(PORT, () => console.log(`🚀 Server running at http://localhost:${PORT}`));
344
+
345
+ ```
201
346
  ---
202
347
 
203
348
  ## Telegram integration
@@ -281,9 +426,12 @@ auth-verify/
281
426
  ├─ package.json
282
427
  ├─ src/
283
428
  │ ├─ index.js // exports AuthVerify
284
- │ ├─ jwt.js
285
- ├─ otp.js
286
- ├─ session.js
429
+ │ ├─ jwt/
430
+ | | ├─ index.js
431
+ | | ├─ cookie/index.js
432
+ │ ├─ /otp/index.js
433
+ │ ├─ /session/index.js
434
+ | ├─ /oauth/index.js
287
435
  │ └─ helpers/helper.js
288
436
  ```
289
437
 
package/src/jwt/index.js CHANGED
@@ -265,6 +265,7 @@ class JWTManager {
265
265
  if (typeof expiry === "number") return expiry;
266
266
  if (typeof expiry !== "string") return 3600000; // default 1h
267
267
  const num = parseInt(expiry);
268
+ if (expiry.endsWith("d")) return num * 3600000 * 24;
268
269
  if (expiry.endsWith("h")) return num * 3600000;
269
270
  if (expiry.endsWith("m")) return num * 60000;
270
271
  if (expiry.endsWith("s")) return num * 1000;
@@ -0,0 +1,240 @@
1
+ // src/managers/oauth.js
2
+
3
+ class OAuthManager {
4
+ constructor(config = {}) {
5
+ this.providers = config.providers || {};
6
+ }
7
+
8
+ // --- GOOGLE LOGIN ---
9
+ google({ clientId, clientSecret, redirectUri }) {
10
+ return {
11
+ redirect(res) {
12
+ const googleURL =
13
+ "https://accounts.google.com/o/oauth2/v2/auth?" +
14
+ new URLSearchParams({
15
+ client_id: clientId,
16
+ redirect_uri: redirectUri,
17
+ response_type: "code",
18
+ scope: "openid email profile",
19
+ access_type: "offline",
20
+ prompt: "consent",
21
+ });
22
+ res.redirect(googleURL);
23
+ },
24
+
25
+ async callback(code) {
26
+ // Step 1: Exchange code for access token
27
+ const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
30
+ body: new URLSearchParams({
31
+ client_id: clientId,
32
+ client_secret: clientSecret,
33
+ code,
34
+ redirect_uri: redirectUri,
35
+ grant_type: "authorization_code",
36
+ }),
37
+ });
38
+
39
+ const tokenData = await tokenRes.json();
40
+ if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error);
41
+
42
+ // Step 2: Get user info
43
+ const userRes = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
44
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
45
+ });
46
+ const user = await userRes.json();
47
+
48
+ return { ...user, access_token: tokenData.access_token };
49
+ },
50
+ };
51
+ }
52
+
53
+ // --- FACEBOOK LOGIN ---
54
+ facebook({ clientId, clientSecret, redirectUri }) {
55
+ return {
56
+ redirect(res) {
57
+ const fbURL =
58
+ "https://www.facebook.com/v19.0/dialog/oauth?" +
59
+ new URLSearchParams({
60
+ client_id: clientId,
61
+ redirect_uri: redirectUri,
62
+ scope: "email,public_profile",
63
+ response_type: "code",
64
+ });
65
+ res.redirect(fbURL);
66
+ },
67
+
68
+ async callback(code) {
69
+ const tokenRes = await fetch(
70
+ `https://graph.facebook.com/v19.0/oauth/access_token?` +
71
+ new URLSearchParams({
72
+ client_id: clientId,
73
+ client_secret: clientSecret,
74
+ redirect_uri: redirectUri,
75
+ code,
76
+ })
77
+ );
78
+
79
+ const tokenData = await tokenRes.json();
80
+ if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error.message);
81
+
82
+ const userRes = await fetch(
83
+ `https://graph.facebook.com/me?fields=id,name,email,picture&access_token=${tokenData.access_token}`
84
+ );
85
+ const user = await userRes.json();
86
+
87
+ return { ...user, access_token: tokenData.access_token };
88
+ },
89
+ };
90
+ }
91
+
92
+ // --- GITHUB LOGIN ---
93
+ github({ clientId, clientSecret, redirectUri }) {
94
+ return {
95
+ redirect(res) {
96
+ const githubURL =
97
+ "https://github.com/login/oauth/authorize?" +
98
+ new URLSearchParams({
99
+ client_id: clientId,
100
+ redirect_uri: redirectUri,
101
+ scope: "user:email",
102
+ allow_signup: "true",
103
+ });
104
+ res.redirect(githubURL);
105
+ },
106
+
107
+ async callback(code) {
108
+ const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
111
+ body: JSON.stringify({
112
+ client_id: clientId,
113
+ client_secret: clientSecret,
114
+ code,
115
+ redirect_uri: redirectUri,
116
+ }),
117
+ });
118
+
119
+ const tokenData = await tokenRes.json();
120
+ if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error);
121
+
122
+ const userRes = await fetch("https://api.github.com/user", {
123
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
124
+ });
125
+ const user = await userRes.json();
126
+
127
+ return { ...user, access_token: tokenData.access_token };
128
+ },
129
+ };
130
+ }
131
+
132
+ // --- X (TWITTER) LOGIN ---
133
+ x({ clientId, clientSecret, redirectUri }) {
134
+ return {
135
+ redirect(res) {
136
+ const twitterURL =
137
+ "https://twitter.com/i/oauth2/authorize?" +
138
+ new URLSearchParams({
139
+ response_type: "code",
140
+ client_id: clientId,
141
+ redirect_uri: redirectUri,
142
+ scope: "tweet.read users.read offline.access",
143
+ state: "state123",
144
+ code_challenge: "challenge",
145
+ code_challenge_method: "plain",
146
+ });
147
+ res.redirect(twitterURL);
148
+ },
149
+
150
+ async callback(code) {
151
+ const tokenRes = await fetch("https://api.twitter.com/2/oauth2/token", {
152
+ method: "POST",
153
+ headers: {
154
+ "Content-Type": "application/x-www-form-urlencoded",
155
+ Authorization:
156
+ "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64"),
157
+ },
158
+ body: new URLSearchParams({
159
+ code,
160
+ grant_type: "authorization_code",
161
+ redirect_uri: redirectUri,
162
+ code_verifier: "challenge",
163
+ }),
164
+ });
165
+
166
+ const tokenData = await tokenRes.json();
167
+ if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error);
168
+
169
+ const userRes = await fetch("https://api.twitter.com/2/users/me", {
170
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
171
+ });
172
+ const user = await userRes.json();
173
+
174
+ return { ...user, access_token: tokenData.access_token };
175
+ },
176
+ };
177
+ }
178
+
179
+ // --- LINKEDIN LOGIN ---
180
+ linkedin({ clientId, clientSecret, redirectUri }) {
181
+ return {
182
+ // Step 1: Redirect user to LinkedIn's authorization page
183
+ redirect(res) {
184
+ const linkedinURL =
185
+ "https://www.linkedin.com/oauth/v2/authorization?" +
186
+ new URLSearchParams({
187
+ response_type: "code",
188
+ client_id: clientId,
189
+ redirect_uri: redirectUri,
190
+ scope: "r_liteprofile r_emailaddress",
191
+ state: "secure123", // optional: you can randomize this
192
+ });
193
+ res.redirect(linkedinURL);
194
+ },
195
+
196
+ // Step 2: Handle callback, exchange code for token, then get user data
197
+ async callback(code) {
198
+ // Exchange authorization code for access token
199
+ const tokenRes = await fetch("https://www.linkedin.com/oauth/v2/accessToken", {
200
+ method: "POST",
201
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
202
+ body: new URLSearchParams({
203
+ grant_type: "authorization_code",
204
+ code,
205
+ redirect_uri: redirectUri,
206
+ client_id: clientId,
207
+ client_secret: clientSecret,
208
+ }),
209
+ });
210
+
211
+ const tokenData = await tokenRes.json();
212
+ if (tokenData.error)
213
+ throw new Error("OAuth Error: " + tokenData.error_description);
214
+
215
+ // Fetch basic profile info
216
+ const profileRes = await fetch("https://api.linkedin.com/v2/me", {
217
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
218
+ });
219
+ const profile = await profileRes.json();
220
+
221
+ // Fetch user's primary email
222
+ const emailRes = await fetch(
223
+ "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))",
224
+ { headers: { Authorization: `Bearer ${tokenData.access_token}` } }
225
+ );
226
+ const emailData = await emailRes.json();
227
+
228
+ return {
229
+ id: profile.id,
230
+ name: profile.localizedFirstName + " " + profile.localizedLastName,
231
+ email: emailData.elements?.[0]?.["handle~"]?.emailAddress || null,
232
+ access_token: tokenData.access_token,
233
+ };
234
+ },
235
+ };
236
+ }
237
+
238
+ }
239
+
240
+ module.exports = OAuthManager;
package/test.js CHANGED
@@ -153,23 +153,47 @@ app.use(express.json());
153
153
 
154
154
  const auth = new AuthVerify({ jwtSecret: 's', storeTokens: 'memory'});
155
155
 
156
+ const google = auth.oauth.google({clientId: '145870939941-qgeskqo8qlaqqm1osed6f5r8bhl866qk.apps.googleusercontent.com', clientSecret: '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', redirectUri: 'http://localhost:3000/auth/google/callback'});
156
157
  app.get('/', async (req, res) => {
157
- const token = await auth.jwt.sign({ id: 1, role: 'user' }, '1h', { res });
158
- res.send(`JWT saved in cookie!`);
158
+ res.send(`
159
+ <h1>Login with Google</h1>
160
+ <a href="/auth/google">Login</a>
161
+ `);
159
162
  });
160
163
 
161
- app.get('/verify', async (req, res) => {
164
+
165
+ app.get('/auth/google', (req, res) => google.redirect(res));
166
+
167
+ app.get('/auth/google/callback', async (req, res)=>{
168
+ const code = req.query.code;
162
169
  try {
163
- const data = await auth.jwt.verify({ headers: req.headers });
164
- res.json({ valid: true, data });
165
- } catch (err) {
166
- res.status(401).json({ valid: false, error: err.message });
170
+ const user = await google.callback(code);
171
+ res.send(`
172
+ <h2>Welcome, ${user.name}!</h2>
173
+ <img src="${user.picture}" width="100" style="border-radius:50%">
174
+ <p>Email: ${user.email}</p>
175
+ <p>Access Token: ${user.access_token.slice(0, 20)}...</p>
176
+ `);
177
+ } catch(err){
178
+ res.status(500).send("Error: " + err.message);
167
179
  }
168
180
  });
169
181
 
170
182
  app.listen(3000, ()=>{
171
- console.log('App is running')
183
+ console.log('Server is running...');
172
184
  });
185
+ // app.get('/verify', async (req, res) => {
186
+ // try {
187
+ // const data = await auth.jwt.verify({ headers: req.headers });
188
+ // res.json({ valid: true, data });
189
+ // } catch (err) {
190
+ // res.status(401).json({ valid: false, error: err.message });
191
+ // }
192
+ // });
193
+
194
+ // app.listen(3000, ()=>{
195
+ // console.log('App is running')
196
+ // });
173
197
 
174
198
  // auth.register.sender("consoleOtp", async ({ to, code }) => {
175
199
  // console.log(`🔑 Sending OTP ${code} to ${to}`);