auth-verify 1.7.0 → 1.8.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.
@@ -68,4 +68,107 @@ window.AuthVerify = class AuthVerify {
68
68
  async verify(code){
69
69
  return this.data({code});
70
70
  }
71
+
72
+ // -----------------------------
73
+ // Helper: decode Base64URL to Uint8Array
74
+ // -----------------------------
75
+ base64urlToUint8Array(base64url) {
76
+ if (!base64url) throw new Error("Missing Base64URL data");
77
+ let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
78
+ while (base64.length % 4) base64 += '=';
79
+ const str = atob(base64);
80
+ return new Uint8Array([...str].map(c => c.charCodeAt(0)));
81
+ }
82
+
83
+ start(route){
84
+ this.startRegisterApi = route;
85
+ return this;
86
+ }
87
+
88
+ finish(route){
89
+ this.finishRegisterApi = route;
90
+ return this;
91
+ }
92
+
93
+ // -----------------------------
94
+ // REGISTER PASSKEY (full flow)
95
+ // -----------------------------
96
+ async registerPasskey(user) {
97
+ try {
98
+ // 1️⃣ Get registration options from backend
99
+ const publicKey = await this.post(`${this.startRegisterApi}`).data({user});
100
+
101
+ // 2️⃣ Decode challenge & user.id automatically
102
+ publicKey.challenge = this.base64urlToUint8Array(publicKey.challenge);
103
+ publicKey.user.id = this.base64urlToUint8Array(publicKey.user.id);
104
+
105
+ // 3️⃣ Ask browser to create credential
106
+ const credential = await navigator.credentials.create({ publicKey });
107
+
108
+ // 4️⃣ Convert ArrayBuffers to base64
109
+ const data = {
110
+ id: credential.id,
111
+ rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
112
+ type: credential.type,
113
+ response: {
114
+ clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))),
115
+ attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.attestationObject))),
116
+ },
117
+ };
118
+
119
+ // 5️⃣ Send credential to backend to finish registration
120
+ const result = await this.post(`${this.finishRegisterApi}`).data(data);
121
+
122
+ return result;
123
+
124
+ } catch (err) {
125
+ console.error("[AuthVerify registerPasskey]", err);
126
+ return { error: true, message: err.message };
127
+ }
128
+ }
129
+
130
+ // -----------------------------
131
+ // LOGIN / AUTHENTICATE PASSKEY
132
+ // -----------------------------
133
+ async loginPasskey(user) {
134
+ try {
135
+ // 1️⃣ Get assertion options (challenge) from backend
136
+ const publicKey = await this.post(`${this.startRegisterApi}`).data({ user, login: true });
137
+
138
+ // 2️⃣ Decode Base64URL fields
139
+ publicKey.challenge = this.base64urlToUint8Array(publicKey.challenge);
140
+ publicKey.allowCredentials = publicKey.allowCredentials.map(cred => ({
141
+ ...cred,
142
+ id: this.base64urlToUint8Array(cred.id)
143
+ }));
144
+
145
+ // 3️⃣ Ask browser to get credential
146
+ const credential = await navigator.credentials.get({ publicKey });
147
+
148
+ // 4️⃣ Convert ArrayBuffers to Base64
149
+ const data = {
150
+ id: credential.id,
151
+ rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
152
+ type: credential.type,
153
+ response: {
154
+ clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))),
155
+ authenticatorData: btoa(String.fromCharCode(...new Uint8Array(credential.response.authenticatorData))),
156
+ signature: btoa(String.fromCharCode(...new Uint8Array(credential.response.signature))),
157
+ userHandle: credential.response.userHandle
158
+ ? btoa(String.fromCharCode(...new Uint8Array(credential.response.userHandle)))
159
+ : null,
160
+ },
161
+ };
162
+
163
+ // 5️⃣ Send assertion to backend for verification
164
+ const result = await this.post(`${this.finishRegisterApi}`).data(data);
165
+
166
+ return result;
167
+
168
+ } catch (err) {
169
+ console.error("[AuthVerify loginPasskey]", err);
170
+ return { error: true, message: err.message };
171
+ }
172
+ }
173
+
71
174
  }
package/index.js CHANGED
@@ -4,6 +4,7 @@ const SessionManager = require("./src/session");
4
4
  const OAuthManager = require("./src/oauth");
5
5
  const TOTPManager = require("./src/totp");
6
6
  const PasskeyManager = require("./src/passkey");
7
+ const MagicLinkManager = require("./src/magiclink");
7
8
 
8
9
  class AuthVerify {
9
10
  constructor(options = {}) {
@@ -22,7 +23,10 @@ class AuthVerify {
22
23
  },
23
24
  rpName = "auth-verify",
24
25
  saveBy = "id",
25
- passExp = "2m"
26
+ passExp = "2m",
27
+ mlSecret = "ml_secret",
28
+ mlExpiry = "5m",
29
+ appUrl = "https://yourapp.com"
26
30
  } = options;
27
31
 
28
32
  // ✅ Ensure cookieName and secret always exist
@@ -50,6 +54,7 @@ class AuthVerify {
50
54
  this.senders = new Map();
51
55
 
52
56
  this.passkey = new PasskeyManager({rpName, storeTokens, saveBy, passExp});
57
+ this.magic = new MagicLinkManager({mlSecret, mlExpiry, appUrl, storeTokens})
53
58
  }
54
59
 
55
60
  // --- Session helpers ---
package/package.json CHANGED
@@ -12,8 +12,8 @@
12
12
  "uuid": "^9.0.1"
13
13
  },
14
14
  "name": "auth-verify",
15
- "version": "1.7.0",
16
- "description": "A simple Node.js library for sending and verifying OTP via email, SMS and Telegram bot. And generating TOTP codes and QR codes. And handling JWT with Cookies. And also handling passwordless logins with passkeys/webauthn",
15
+ "version": "1.8.0",
16
+ "description": "A simple Node.js library for sending and verifying OTP via email, SMS and Telegram bot. And generating TOTP codes and QR codes. And handling JWT with Cookies. And also handling passwordless logins with passkeys/webauth. And handling magiclink passwordless logins",
17
17
  "main": "index.js",
18
18
  "scripts": {
19
19
  "test": "jest --runInBand"
@@ -45,7 +45,10 @@
45
45
  "jsonwebtoken",
46
46
  "totp",
47
47
  "google-authenticator",
48
- "signin"
48
+ "signin",
49
+ "webauthn",
50
+ "passkey",
51
+ "passwordless"
49
52
  ],
50
53
  "author": "Jahongir Sobirov",
51
54
  "license": "MIT",
package/readme.md CHANGED
@@ -1,18 +1,17 @@
1
1
  # auth-verify
2
2
 
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
- - ✅ TOTP (Time-based One Time Passwords) generation, QR code generation, and verification (Google Authenticator support).
7
- - ✅ JWT creation, verification, optional token revocation with memory/Redis storage, and advanced middleware for protecting routes, custom cookie/header handling, role-based guards, and token extraction from custom sources.
8
- - ✅ Session management (in-memory or Redis).
9
- - ✅ OAuth 2.0 integration for Google, Facebook, GitHub, X (Twitter), Linkedin, and additional providers like Apple, Discord, Slack, Microsoft, Telegram,and WhatsApp.
10
- - ⚙️ Developer extensibility: custom senders via `auth.register.sender()` and chainable sending via `auth.use(name).send(...)`.
11
- - ✅ Frontend client SDK (`authverify.client.js`) for browser usage: QR display, OTP verification, JWT requests, and auth headers; works without modules, just `<script>`.
12
- - ✅ Automatic JWT cookie handling for Express apps, supporting secure, HTTP-only cookies and optional auto-verification.
13
- - Passwordless login and registration with passkeys and webauthn.
14
- - Fully asynchronous/Promise-based API, with callback support where applicable.
15
- - ✅ Chainable OTP workflow with cooldowns, max attempts, and resend functionality.
3
+ **AuthVerify** is a modular authentication library for Node.js, providing JWT, OTP, TOTP, Passkeys (WebAuthn), Magic Links, Sessions, and OAuth helpers. You can easily register custom senders for OTPs or notifications.
4
+ - [Installation](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#-installation)
5
+ - [Initialization](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#-example-initialize-library-commonjs)
6
+ - [JWT](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#-jwt-usage)
7
+ - [OTP](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#-otp-email--sms--telegram--custom-sender)
8
+ - [TOTP](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#-totp-time-based-one-time-passwords--google-authenticator-support-v140)
9
+ - [Passkeys](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#%EF%B8%8F-passkey-webauthn-v161)
10
+ - [Auth-Verify Frontend SDK](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#auth-verify-client)
11
+ - [OAuth](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#-oauth-20-integration-v120)
12
+ - [Magic Links](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#-magiclink-passwordless-login-new-in-v180)
13
+ - [Custom Senders](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#developer-extensibility-custom-senders)
14
+ - [Session Management](https://github.com/Jahongir2007/auth-verify/blob/main/docs/docs.md#sessionmanager)
16
15
  ---
17
16
 
18
17
  ## 🧩 Installation
@@ -44,13 +43,35 @@ npm install auth-verify
44
43
  const AuthVerify = require('auth-verify');
45
44
 
46
45
  const auth = new AuthVerify({
47
- jwtSecret: "super_secret_value",
48
- storeTokens: "memory", // or "redis" or "none"
49
- otpExpiry: "5m", // supports number (seconds) OR string like '30s', '5m', '1h'
50
- otpHash: "sha256", // optional OTP hashing algorithm
51
- // you may pass redisUrl inside managers' own options when using redis
46
+ jwtSecret: 'your_jwt_secret',
47
+ cookieName: 'jwt_token',
48
+ otpExpiry: 300, // in seconds
49
+ storeTokens: 'memory', // or 'redis'
50
+ redisUrl: 'redis://localhost:6379',
51
+ totp: { digits: 6, step: 30, alg: 'SHA1' },
52
+ rpName: 'myApp',
53
+ passExp: '2m',
54
+ mlSecret: 'ml_secret',
55
+ mlExpiry: '5m',
56
+ appUrl: 'https://yourapp.com'
52
57
  });
53
58
  ```
59
+ ### Options explained:
60
+
61
+ | Option | Default | Description |
62
+ | ------------- | -------------------------------------- | ---------------------------------------- |
63
+ | `jwtSecret` | `"jwt_secret"` | Secret key for JWT signing |
64
+ | `cookieName` | `"jwt_token"` | Cookie name for JWT storage |
65
+ | `otpExpiry` | `300` | OTP expiration in seconds |
66
+ | `storeTokens` | `"memory"` | Token storage type (`memory` or `redis`) |
67
+ | `redisUrl` | `undefined` | Redis connection string if using Redis |
68
+ | `totp` | `{ digits: 6, step: 30, alg: 'SHA1' }` | TOTP configuration |
69
+ | `rpName` | `"auth-verify"` | Relying party name for Passkeys |
70
+ | `passExp` | `"2m"` | Passkey expiration duration |
71
+ | `mlSecret` | `"ml_secret"` | Magic link secret |
72
+ | `mlExpiry` | `"5m"` | Magic link expiration duration |
73
+ | `appUrl` | `"https://yourapp.com"` | App base URL for Magic Links |
74
+
54
75
 
55
76
  ---
56
77
 
@@ -389,7 +410,7 @@ auth.otp.verify({ check: 'user@example.com', code: '123456' }, (err, isValid)=>{
389
410
 
390
411
  ---
391
412
 
392
- ## 🗝️ Passkey (WebAuthn) (New in v1.6.1)
413
+ ## 🗝️ Passkey (WebAuthn) (v1.6.1+)
393
414
 
394
415
  `AuthVerify` includes a `PasskeyManager` class to handle passwordless login using WebAuthn / passkeys. You can **register** users, **verify login**, and manage **challenges** safely.
395
416
 
@@ -583,7 +604,7 @@ console.log("TOTP:", token);
583
604
  ```
584
605
  ### verify a code entered by user
585
606
  ```js
586
- const ok = auth.totp.verify({ secret, token });
607
+ const ok = auth.totp.verify(secret, token);
587
608
  console.log(ok); // true or false
588
609
  ```
589
610
  ### example real flow
@@ -599,7 +620,7 @@ const qr = await auth.totp.qrcode(uri);
599
620
  const token = req.body.code;
600
621
 
601
622
  // Verify
602
- if (auth.totp.verify({ secret, token })) {
623
+ if (auth.totp.verify(secret, token )) {
603
624
  // enable 2FA
604
625
  }
605
626
  ```
@@ -614,7 +635,10 @@ It works with your backend APIs to:
614
635
  - Verify user OTP codes
615
636
  - Request JWT tokens from backend
616
637
  - Send authenticated requests easily
617
-
638
+ - **Register a passkey** (create a new credential)
639
+ - **Login with a passkey** (authenticate existing credential)
640
+ - Handle **Base64URL decoding**, **ArrayBuffer conversion**, and **backend communication** automatically
641
+ - Easily integrate with your Node.js backend using `auth-verify`
618
642
  Works like jQuery: just include the script in HTML, no module or bundler needed.
619
643
 
620
644
  ## 2️⃣ Installation
@@ -669,15 +693,63 @@ console.log(profile);
669
693
  - `auth.header()` returns `{ Authorization: "Bearer <jwt>" }`
670
694
  - Easy to attach JWT to any request
671
695
 
696
+ ### Passkey part (new in v1.8.0)
697
+ #### API Methods
698
+ ##### `start(route)`
699
+ Sets the backend endpoint to start a **registration or login flow**.
700
+ ```js
701
+ auth.start('/api/register/start'); // registration start
702
+ auth.start('/api/login/start'); // login start
703
+ ```
704
+
705
+ #### `finish(route)`
706
+ Sets the backend endpoint to **finish the flow** (verify credential/assertion).
707
+ ```js
708
+ auth.finish('/api/register/finish'); // registration finish
709
+ auth.finish('/api/login/finish'); // login finish
710
+ ```
711
+
712
+ #### `registerPasskey(user)`
713
+ Registers a new passkey for the user.
714
+ ##### Parameters:
715
+ | Param | Type | Description |
716
+ | ----- | ------ | ---------------------------------------------------------------------- |
717
+ | user | Object | `{ id: "user1", username: "john_doe" }` — user info to send to backend |
718
+
719
+ ##### Returns:
720
+ `Promise<Object>` — result from backend (`{ success: true/false, message: "..." }`)
721
+ ##### Example:
722
+ ```js
723
+ auth.start('/api/register/start').finish('/api/register/finish');
724
+
725
+ const result = await auth.registerPasskey({ id: 'user1', username: 'john_doe' });
726
+
727
+ if(result.success) alert("Passkey registered!");
728
+ else alert("Error: " + result.message);
729
+ ```
730
+
731
+ ##### What it does internally:
732
+ 1. Calls `/start` endpoint → gets assertion options.
733
+ 2. Decodes `challenge` and `allowCredentials[].id` from Base64URL → Uint8Array.
734
+ 3. Calls `navigator.credentials.get({ publicKey })`.
735
+ 4. Converts ArrayBuffers to Base64.
736
+ 5. Sends assertion to `/finish` endpoint for verification.
737
+ #### `base64urlToUint8Array(base64url)`
738
+ Helper to convert Base64URL string to `Uint8Array`.
739
+ Used internally in registration & login flow. Devs can use it for custom WebAuthn handling if needed.
672
740
  ### 8️⃣ Method Summary
673
- | Method | Description |
674
- | --------------- | ----------------------------------------------- |
675
- | `get(url)` | Set GET endpoint |
676
- | `post(url)` | Set POST endpoint |
677
- | `qr()` | Fetch QR from backend and display |
678
- | `data(payload)` | Send payload to backend; stores JWT if returned |
679
- | `verify(code)` | Send OTP code to backend |
680
- | `header()` | Return JWT auth header object |
741
+ | Method | Description |
742
+ | ----------------------- | ---------------------------------------------------------------------------------------------- |
743
+ | `get(url)` | Set GET endpoint |
744
+ | `post(url)` | Set POST endpoint |
745
+ | `qr()` | Fetch QR from backend and display |
746
+ | `data(payload)` | Send payload to backend; stores JWT if returned |
747
+ | `verify(code)` | Send OTP code to backend |
748
+ | `header()` | Return JWT auth header object |
749
+ | `start(route)` | Set backend endpoint to **start registration or login** |
750
+ | `finish(route)` | Set backend endpoint to **finish registration or login** |
751
+ | `registerPasskey(user)` | Full registration flow: fetch challenge, decode, create credential in browser, send to backend |
752
+ | `loginPasskey(user)` | Full login flow: fetch assertion, decode, get credential from browser, send to backend |
681
753
 
682
754
  ### 9️⃣ Example HTML
683
755
  ```html
@@ -686,7 +758,7 @@ console.log(profile);
686
758
  <button id="getQRBtn">Get QR</button>
687
759
  <button id="sendBtn">Send Data</button>
688
760
 
689
- <script src="authverify.client.js"></script>
761
+ <script src="https://cdn.jsdelivr.net/gh/jahongir2007/auth-verify/authverify.client.js"></script>
690
762
  <script>
691
763
  const qrImage = document.getElementById('qrImage');
692
764
  const responseDiv = document.getElementById('response');
@@ -703,10 +775,50 @@ document.getElementById('sendBtn').addEventListener('click', async () => {
703
775
  </script>
704
776
  ```
705
777
 
778
+ ### Passkey example
779
+ ```html
780
+ <!DOCTYPE html>
781
+ <html>
782
+ <head>
783
+ <title>AuthVerify Demo</title>
784
+ </head>
785
+ <body>
786
+ <h1>AuthVerify Passkey Demo</h1>
787
+ <button id="register">Register Passkey</button>
788
+ <button id="login">Login with Passkey</button>
789
+
790
+ <script src="https://cdn.jsdelivr.net/gh/jahongir2007/auth-verify/authverify.client.js"></script>
791
+ <script>
792
+ const auth = new AuthVerify({ apiBase: "http://localhost:3000" });
793
+
794
+ // Registration setup
795
+ auth.start('/api/register/start').finish('/api/register/finish');
796
+ document.getElementById('register').addEventListener('click', async () => {
797
+ const result = await auth.registerPasskey({ id: 'user1', username: 'john_doe' });
798
+ alert(result.message);
799
+ });
800
+
801
+ // Login setup
802
+ auth.start('/api/login/start').finish('/api/login/finish');
803
+ document.getElementById('login').addEventListener('click', async () => {
804
+ const result = await auth.loginPasskey({ id: 'user1', username: 'john_doe' });
805
+ alert(result.message);
806
+ });
807
+ </script>
808
+ </body>
809
+ </html>
810
+ ```
811
+ ✅ Fully functional frontend passkey demo
812
+ ✅ One line registration / login for devs
813
+ ✅ Automatic Base64URL decoding and ArrayBuffer handling
814
+
706
815
  ### 10️⃣ Tips for Developers
707
816
  - Always call `auth.get('/api/qr').qr()` **after page loads**
708
817
  - Use `auth.header()` for any authenticated request
709
818
  - Backend must provide endpoints for `/api/qr`, `/api/verify-totp`, `/api/sign-jwt`
819
+ - Make sure backend endpoints return **raw WebAuthn options** (`challenge`, `user`, `allowCredentials`) in **Base64URL format**.
820
+ - `user.id` and `challenge` must be **Base64URL encoded** on backend.
821
+ - JWT storage is automatic if backend returns **token**.
710
822
 
711
823
  ---
712
824
 
@@ -956,6 +1068,166 @@ app.listen(PORT, () => console.log(`🚀 Server running at http://localhost:${PO
956
1068
  ```
957
1069
  ---
958
1070
 
1071
+ ## 💌 Magiclink (Passwordless login) (New in v1.8.0)
1072
+ The **Magic Link Manager** allows developers to implement **secure**, **passwordless login** using **email-based links**.
1073
+ Built directly into the AuthVerify SDK, it supports **Gmail**, **custom SMTP**, and token storage via **Memory** or **Redis**.
1074
+
1075
+ ### 🚀 Basic Setup
1076
+ ```js
1077
+ const AuthVerify = require('auth-verify');
1078
+
1079
+ const auth = new AuthVerify({
1080
+ mlSecret: 'super_secret_key',
1081
+ mlExp: '5m',
1082
+ appUrl: 'http://localhost:3000',
1083
+ storeTokens: 'memory'
1084
+ });
1085
+ ```
1086
+
1087
+ ### ⚙️ Configure Magic Link Sender
1088
+ Before sending links, you must set up your email transport.
1089
+ #### Gmail Example
1090
+ ```js
1091
+ await auth.magic.sender({
1092
+ service: 'gmail',
1093
+ sender: 'yourapp@gmail.com',
1094
+ pass: 'your_gmail_app_password'
1095
+ });
1096
+ ```
1097
+
1098
+ #### Custom SMTP Example
1099
+ ```js
1100
+ await auth.magic.sender({
1101
+ host: 'smtp.mailgun.org',
1102
+ port: 587,
1103
+ secure: false,
1104
+ sender: 'noreply@yourdomain.com',
1105
+ pass: 'your_smtp_password'
1106
+ });
1107
+ ```
1108
+ > ✅ Both Gmail and any SMTP provider are supported.
1109
+ > Use app passwords or tokens instead of your real password!
1110
+
1111
+ ### 📩 Send Magic Link
1112
+ Send a secure, expiring link to the user’s email:
1113
+ ```js
1114
+ await auth.magic.send('user@example.com', {
1115
+ subject: 'Your Secure Login Link ✨',
1116
+ html: `<p>Click below to sign in:</p>
1117
+ <a href="{{link}}">Login Now</a>`
1118
+ });
1119
+ ```
1120
+ > The `{{link}}` placeholder will automatically be replaced with the generated magic link.
1121
+
1122
+ ### 🪄 Generate Magic Link Manually
1123
+ If you just want to create a link (not send it yet):
1124
+ ```js
1125
+ const token = await auth.magic.generate('user@example.com');
1126
+ console.log(token);
1127
+ ```
1128
+ Then make your own URL:
1129
+ ```js
1130
+ const link = `http://localhost:3000/auth/verify?token=${token}`;
1131
+ ```
1132
+
1133
+ ### 🔐 Verify Magic Link
1134
+ Typically used in your backend `/auth/verify` route:
1135
+ ```js
1136
+ app.get('/auth/verify', async (req, res) => {
1137
+ const { token } = req.query;
1138
+ try {
1139
+ const user = await auth.magic.verify(token);
1140
+ res.json({ success: true, user });
1141
+ } catch (err) {
1142
+ res.status(400).json({ success: false, message: err.message });
1143
+ }
1144
+ });
1145
+ ```
1146
+
1147
+ ### 🧠 How It Works
1148
+ 1. `auth.magic.generate()` → creates a short-lived JWT with the user’s email.
1149
+ 2. `auth.magic.send()` → sends a secure login link by email.
1150
+ 3. `auth.magic.verify()` → decodes & validates the token, optionally checks store.
1151
+
1152
+ ### 💾 Storage Options
1153
+ | Mode | Description | Best For |
1154
+ | ------------------ | -------------------- | ------------------------------ |
1155
+ | `memory` (default) | Uses in-memory Map() | Single server / small projects |
1156
+ | `redis` | Uses Redis with TTL | Scalable, multi-server apps |
1157
+
1158
+ Example using Redis:
1159
+ ```js
1160
+ const auth = new AuthVerify({
1161
+ storeTokens: 'redis',
1162
+ redisUrl: 'redis://localhost:6379'
1163
+ });
1164
+ ```
1165
+
1166
+ ### 🧰 Callback Support
1167
+ You can also use Node-style callbacks if you prefer:
1168
+ ```js
1169
+ auth.magic.send('user@example.com', (err) => {
1170
+ if (err) console.error('❌ Failed to send link:', err);
1171
+ else console.log('✅ Magic link sent!');
1172
+ });
1173
+ ```
1174
+
1175
+ ### 🌍 Example Express Integration
1176
+ ```js
1177
+ const express = require('express');
1178
+ const bodyParser = require('body-parser');
1179
+ const { AuthVerify } = require('auth-verify');
1180
+
1181
+ const app = express();
1182
+ app.use(bodyParser.json());
1183
+
1184
+ const auth = new AuthVerify({
1185
+ mlSecret: 'supersecretkey',
1186
+ appUrl: 'http://localhost:3000',
1187
+ storeTokens: 'memory'
1188
+ });
1189
+
1190
+ auth.magic.sender({
1191
+ service: 'gmail',
1192
+ sender: 'yourapp@gmail.com',
1193
+ pass: 'your_app_password'
1194
+ });
1195
+
1196
+ // Send link
1197
+ app.post('/auth/send', async (req, res) => {
1198
+ const { email } = req.body;
1199
+ await auth.magic.send(email);
1200
+ res.json({ message: 'Magic link sent!' });
1201
+ });
1202
+
1203
+ // Verify link
1204
+ app.get('/auth/verify', async (req, res) => {
1205
+ try {
1206
+ const user = await auth.magic.verify(req.query.token);
1207
+ res.json({ message: 'Login successful!', user });
1208
+ } catch (err) {
1209
+ res.status(400).json({ message: err.message });
1210
+ }
1211
+ });
1212
+
1213
+ app.listen(3000, () => console.log('🚀 Server running on port 3000'));
1214
+ ```
1215
+
1216
+ ### 🧾 Summary
1217
+ | Feature | Supported |
1218
+ | -------------------------- | --------- |
1219
+ | Gmail & SMTP | ✅ |
1220
+ | Memory & Redis Token Store | ✅ |
1221
+ | Token Expiry | ✅ |
1222
+ | Callback & Promise APIs | ✅ |
1223
+ | HTML Custom Email | ✅ |
1224
+
1225
+ ### ⚡ Future Vision
1226
+
1227
+ `auth.magic` is built for **modern SaaS**, **fintech**, and **crypto** apps that need **passwordless**, **secure**, and **user-friendly** authentication.
1228
+
1229
+ ---
1230
+
959
1231
  ## Telegram integration
960
1232
 
961
1233
  There are two ways to use Telegram flow:
@@ -1041,6 +1313,7 @@ auth-verify/
1041
1313
  | | ├─ index.js
1042
1314
  | | ├─ cookie/index.js
1043
1315
  │ ├─ /otp/index.js
1316
+ │ ├─ /magiclink/index.js
1044
1317
  │ ├─ totp/
1045
1318
  | | ├─ index.js
1046
1319
  | | ├─ base32.js
@@ -0,0 +1,114 @@
1
+ const jwt = require('jsonwebtoken');
2
+ const nodemailer = require('nodemailer');
3
+ const Redis = require('ioredis');
4
+
5
+ class MagicLinkManager {
6
+ constructor(config = {}) {
7
+ this.secret = config.mlSecret || 'authverify_secret';
8
+ this.expiresIn = config.mlExpiry || '5m';
9
+ this.appUrl = config.appUrl || 'https://yourapp.com';
10
+ this.storeType = config.storeTokens || 'memory';
11
+
12
+ if (this.storeType === 'memory') {
13
+ this.tokenStore = new Map();
14
+ } else if (this.storeType === 'redis') {
15
+ this.redis = new Redis(config.redisUrl || 'redis://localhost:6379');
16
+ } else if (this.storeType !== 'none') {
17
+ throw new Error("{storeTokens} must be 'memory' or 'redis'");
18
+ }
19
+ }
20
+
21
+ async generate(email) {
22
+ return jwt.sign({ email }, this.secret, { expiresIn: this.expiresIn });
23
+ }
24
+
25
+ sender(config = {}) {
26
+ this.senderConfig = config;
27
+ }
28
+
29
+ async send(email, mailOption = {}, callback) {
30
+ if (typeof mailOption === 'function') {
31
+ callback = mailOption;
32
+ mailOption = {};
33
+ }
34
+
35
+ const sendProcess = async () => {
36
+ const token = await this.generate(email);
37
+ const link = `${this.appUrl}/auth/verify?token=${token}`;
38
+
39
+ const letterHtml = mailOption.html
40
+ ? mailOption.html.replace(/{{\s*link\s*}}/gi, link)
41
+ : `<p>Click to login: <a href="${link}">${link}</a></p>`;
42
+
43
+ let transporter;
44
+ if (this.senderConfig.service === 'gmail') {
45
+ transporter = nodemailer.createTransport({
46
+ service: 'gmail',
47
+ auth: { user: this.senderConfig.sender, pass: this.senderConfig.pass }
48
+ });
49
+ } else {
50
+ transporter = nodemailer.createTransport({
51
+ host: this.senderConfig.host,
52
+ port: this.senderConfig.port || 587,
53
+ secure: this.senderConfig.secure || false,
54
+ auth: { user: this.senderConfig.sender, pass: this.senderConfig.pass }
55
+ });
56
+ }
57
+
58
+ await transporter.sendMail({
59
+ from: this.senderConfig.sender,
60
+ to: email,
61
+ subject: mailOption.subject || 'Your Magic Login Link ✨',
62
+ html: letterHtml
63
+ });
64
+
65
+ let timeVal = 5 * 60 * 1000;
66
+ if (typeof this.expiresIn === 'string') {
67
+ if (this.expiresIn.endsWith('m')) timeVal = parseInt(this.expiresIn) * 60 * 1000;
68
+ else if (this.expiresIn.endsWith('s')) timeVal = parseInt(this.expiresIn) * 1000;
69
+ } else if (typeof this.expiresIn === 'number') timeVal = this.expiresIn * 1000;
70
+
71
+ if (this.storeType === 'memory') {
72
+ this.tokenStore.set(email, token);
73
+ setTimeout(() => this.tokenStore.delete(email), timeVal);
74
+ } else if (this.storeType === 'redis') {
75
+ await this.redis.set(email, token, 'PX', timeVal);
76
+ }
77
+
78
+ return { token, link };
79
+ };
80
+
81
+ if (callback) sendProcess().then(r => callback(null, r)).catch(e => callback(e));
82
+ else return sendProcess();
83
+ }
84
+
85
+ async verify(token, callback) {
86
+ const verifyProcess = async () => {
87
+ try {
88
+ const decoded = jwt.verify(token, this.secret);
89
+ // console.log(decoded);
90
+ const email = decoded.email;
91
+
92
+ let stored;
93
+ if (this.storeType === 'memory') stored = this.tokenStore.get(email);
94
+ else if (this.storeType === 'redis') stored = await this.redis.get(email);
95
+
96
+ if (!stored) throw new Error('Magic link expired or not found');
97
+ if (stored !== token) throw new Error('Invalid or already used magic link');
98
+
99
+ if (this.storeType === 'memory') this.tokenStore.delete(email);
100
+ if (this.storeType === 'redis') await this.redis.del(email);
101
+
102
+ return { success: true, user: decoded };
103
+ } catch (err) {
104
+ throw new Error('Invalid or expired magic link');
105
+ }
106
+ };
107
+
108
+ if (callback && typeof callback === 'function')
109
+ verifyProcess().then(r => callback(null, r)).catch(e => callback(e));
110
+ else return verifyProcess();
111
+ }
112
+ }
113
+
114
+ module.exports = MagicLinkManager;
package/src/otp/index.js CHANGED
@@ -12,7 +12,7 @@ class OTPManager {
12
12
  }else{
13
13
  this.otpExpiry = (otpOptions.otpExpiry || 300) * 1000;
14
14
  }
15
- this.storeType = otpOptions.storeTokens || 'none';
15
+ this.storeType = otpOptions.storeTokens || 'memory';
16
16
  this.hashAlgorithm = otpOptions.otpHash || 'sha256';
17
17
  this.logger = null;
18
18
  this.customSender = null;
@@ -56,12 +56,12 @@ class OTPManager {
56
56
  // }
57
57
 
58
58
  setSender(config){
59
- if(!config.via) throw new Error("⚠️ Sender type { via } is required. It shouldbe 'email' or 'sms' or 'telegram'");
59
+ if(!config.via) throw new Error("⚠️ Sender type { via } is required. It should be 'email' or 'sms' or 'telegram'");
60
60
  this.senderConfig = config;
61
61
  }
62
62
 
63
63
  sender(config){
64
- if(!config.via) throw new Error("⚠️ Sender type { via } is required. It shouldbe 'email' or 'sms' or 'telegram'");
64
+ if(!config.via) throw new Error("⚠️ Sender type { via } is required. It should be 'email' or 'sms' or 'telegram'");
65
65
  this.senderConfig = config;
66
66
  }
67
67
 
@@ -328,7 +328,7 @@ class OTPManager {
328
328
  // if developer provided their own sender function
329
329
 
330
330
  if (!this.senderConfig)
331
- throw new Error("Sender not configured. Use setSender() before message().");
331
+ throw new Error("Sender not configured. Use setSender() or sender() before message().");
332
332
 
333
333
  // ---- EMAIL PART ----
334
334
  if (this.senderConfig.via === 'email') {
@@ -0,0 +1,67 @@
1
+ const jwt = require('jsonwebtoken');
2
+ const nodemailer = require('nodemailer');
3
+ const AuthVerify = require('../index');
4
+
5
+ // --- Mock nodemailer ---
6
+ jest.mock('nodemailer', () => ({
7
+ createTransport: jest.fn(() => ({
8
+ sendMail: jest.fn().mockResolvedValue(true)
9
+ }))
10
+ }));
11
+
12
+ describe('MagicLinkManager', () => {
13
+ let magic;
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ auth = new AuthVerify({
18
+ mlSecret: 'test_secret',
19
+ appUrl: 'http://localhost:3000',
20
+ storeTokens: 'memory',
21
+ mlExpiry: '1m'
22
+ });
23
+
24
+ auth.magic.sender({
25
+ service: 'gmail',
26
+ sender: 'test@gmail.com',
27
+ pass: 'fakepass'
28
+ });
29
+ });
30
+
31
+ // --- 1️⃣ Generate token ---
32
+ test('should generate valid JWT token', async () => {
33
+ const token = await auth.magic.generate('user@example.com');
34
+ const decoded = jwt.verify(token, 'test_secret');
35
+ // console.log(decoded);
36
+ expect(decoded.email).toBe('user@example.com');
37
+ });
38
+
39
+ // --- 2️⃣ Send magic link (mocked) ---
40
+ test('should send email using nodemailer', async () => {
41
+ const res = await auth.magic.send('user@example.com');
42
+ expect(res.link).toContain('/auth/verify?token=');
43
+ expect(nodemailer.createTransport).toHaveBeenCalled();
44
+ });
45
+
46
+ // --- 3️⃣ Verify valid token ---
47
+ test('should verify a valid token', async () => {
48
+ const { token } = await auth.magic.send('user@example.com');
49
+ const decoded = await auth.magic.verify(token);
50
+ expect(decoded.user.email).toBe('user@example.com');
51
+ });
52
+
53
+ // --- 4️⃣ Reject invalid or expired token ---
54
+ test('should throw error for invalid token', async () => {
55
+ await expect(auth.magic.verify('fake.token.value'))
56
+ .rejects
57
+ .toThrow('Invalid or expired magic link');
58
+ });
59
+
60
+ // --- 5️⃣ Delete token after verification ---
61
+ test('should remove token after successful verification', async () => {
62
+ const { token } = await auth.magic.send('remove@example.com');
63
+ await auth.magic.verify(token);
64
+ // Second time should fail (already used)
65
+ await expect(auth.magic.verify(token)).rejects.toThrow('Invalid or expired magic link');
66
+ });
67
+ });