auth-verify 0.0.2 → 1.1.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/index.js CHANGED
@@ -1,182 +1,86 @@
1
- const sqlite3 = require("sqlite3").verbose();
2
- const nodemailer = require('nodemailer')
3
- const crypto = require('crypto');
4
- const { request } = require("http");
5
-
6
- class Verifier {
7
- constructor(sender = {}){
8
- this.otpLen = sender.otp?.leng || 6;
9
- this.expMin = sender.otp?.expMin || 3;
10
- this.limit = sender.otp?.limit || 5;
11
- this.cooldown = sender.otp?.cooldown || 60;
12
- // Setup SMTP
13
- this.transport = nodemailer.createTransport({
14
- service: `${sender.serv}`,
15
- auth: {
16
- user: `${sender.sender}`,
17
- pass: sender.pass
1
+ const JWTManager = require("./src/jwt");
2
+ const OTPManager = require("./src/otp");
3
+ const SessionManager = require("./src/session");
4
+
5
+ class AuthVerify {
6
+ constructor(options = {}) {
7
+ const {
8
+ jwtSecret,
9
+ otpExpiry = 300,
10
+ storeTokens = "none",
11
+ otpHash = "sha256",
12
+ redisUrl,
13
+ } = options;
14
+
15
+ if (!jwtSecret) throw new Error("jwtSecret is required in AuthVerify options");
16
+
17
+ this.senderName;
18
+ this.jwt = new JWTManager(jwtSecret, { storeTokens });
19
+ this.otp = new OTPManager({
20
+ storeTokens,
21
+ otpExpiry,
22
+ otpHash,
23
+ redisUrl,
24
+ });
25
+ this.session = new SessionManager({ storeTokens, redisUrl });
26
+
27
+ this.senders = new Map();
28
+ // this.register = {
29
+ // sender: (name, fn)=>{
30
+ // if (!name || typeof fn !== "function") {
31
+ // throw new Error("Sender registration requires a name and a function");
32
+ // }else{
33
+ // try{
34
+ // this.senders.set(name, fn);
35
+ // }catch(err){
36
+ // throw new Error(err);
37
+ // }
38
+ // }
39
+ // }
40
+ // }
41
+ // ✅ No getters — directly reference otp.dev (it's a plain object)
42
+ }
43
+
44
+ // Session helpers
45
+ async createSession(userId, options = {}) {
46
+ return this.session.create(userId, options);
47
+ }
48
+
49
+ async verifySession(sessionId) {
50
+ return this.session.verify(sessionId);
51
+ }
52
+
53
+ async destroySession(sessionId) {
54
+ return this.session.destroy(sessionId);
55
+ }
56
+
57
+ async use(name){
58
+ const senderFn = this.senders.get(name);
59
+ if(!senderFn) throw new Error(`Sender "${name}" not found`);
60
+ this.senderName = senderFn;
61
+ }
62
+
63
+ register = {
64
+ sender: (name, fn) => {
65
+ if (!name || typeof fn !== "function") {
66
+ throw new Error("Sender registration requires a name and a function");
18
67
  }
19
- });
20
-
21
- this.sender = `${sender.smtp?.sender}`
22
-
23
- //making a db for saving otp codes
24
- this.db = new sqlite3.Database('./authverify.db');
25
- this.db.serialize(()=>{
26
- // creating a table
27
- this.db.run(`
28
- CREATE TABLE IF NOT EXISTS verifications (
29
- email TEXT PRIMARY KEY,
30
- code TEXT,
31
- expiresAt TEXT,
32
- requests INTEGER DEFAULT 0,
33
- lastRequest TEXT
34
- )
35
- `);
36
- });
37
-
38
- }
39
-
40
- // Generate OTP securely
41
- generateOTP() {
42
- const buffer = crypto.randomBytes(this.otpLen);
43
- const number = parseInt(buffer.toString('hex'), 16)
44
- .toString()
45
- .slice(0, this.otpLen);
46
- return number.padStart(this.otpLen, '0');
47
- }
48
-
49
- html(html){
50
- this.htmlContent = html;
51
- return this;
52
- }
53
-
54
- subject(subject){
55
- this.mailSubject = subject;
56
- return this;
57
- }
58
-
59
- text(text){
60
- this.mailText = text;
61
- return this;
62
- }
63
-
64
- sendTo(email, callback){
65
-
66
- this.recieverEmail = email;
67
- const code = this.generateOTP();
68
- const expAt = new Date(Date.now() + this.expMin * 60 * 1000).toISOString();
69
-
70
- this.db.get(`SELECT * FROM verifications WHERE email = ?`, [email], (err, row)=>{
71
- if(err) return callback(err);
72
-
73
- const now = new Date();
74
- let requests = 1;
75
-
76
- if(row){
77
- const lastRequest = row.lastRequest ? new Date(row.lastRequest) : null;
78
-
79
- if(lastRequest && now.toDateString() === lastRequest.toDateString()){
80
- requests = row.requests + 1;
81
- if(requests > this.limit){
82
- return callback(new Error("Too many OTP requests today"));
83
- }
84
-
85
- const diff = (now - lastRequest) / 1000;
86
- if(diff < this.cooldown){
87
- return callback(new Error(`Please wait ${Math.ceil(this.cooldown - diff)} seconds before requesting a new OTP.`));
88
- }
89
- }
90
- }
91
-
92
- this.db.run(`INSERT INTO verifications (email, code, expiresAt, requests, lastRequest) VALUES (?, ?, ?, ?, ?)
93
- ON CONFLICT(email) DO UPDATE SET
94
- code = excluded.code,
95
- expiresAt = excluded.expiresAt,
96
- requests = excluded.requests,
97
- lastRequest = excluded.lastRequest`, [email, code, expAt, requests, now.toISOString()], (DBerr)=>{
98
- if(DBerr) return callback(DBerr);
99
-
100
- this.otpOptions = {
101
- from: this.sender,
102
- to: `${email}`,
103
- subject: this.mailSubject ? this.mailSubject.replace("{otp}", code) : undefined,
104
- text: this.mailText ? this.mailText.replace("{otp}", code) : undefined,
105
- html: this.htmlContent ? this.htmlContent.replace("{otp}", code) : undefined
106
- }
107
-
108
- this.transport.sendMail(this.otpOptions, (emailErr, info)=>{
109
- if(emailErr) return callback(emailErr);
110
-
111
- // console.log('📨 Email sent:', info.response);
112
- callback(null, true);
113
- });
114
-
115
- // console.log('💾 Code saved to DB');
116
- });
117
- });
118
-
119
- return this;
120
-
121
- }
122
-
123
- code(userCode){
124
- this.userCode = userCode
125
- return this;
126
- }
127
-
128
- verifyFor(email, callback){
129
- const now = new Date().toISOString();
130
-
131
- this.db.get(`SELECT * FROM verifications WHERE email = ? AND expiresAt > ?`, [email, now], (DBerr, row)=>{
132
- if(DBerr) return callback(DBerr);
68
+ this.senders.set(name, fn);
69
+ // console.log(`✅ Sender registered: ${name}`);
70
+ }
71
+ };
133
72
 
134
- // console.log(`Code was got`);
73
+ // use a sender by name
74
+ use(name) {
75
+ const senderFn = this.senders.get(name);
76
+ if (!senderFn) throw new Error(`Sender "${name}" not found`);
135
77
 
136
- if(!row){
137
- // console.log(this.recieverEmail);
138
- callback(null, false)
139
- }else{
140
- if(row.code === this.userCode){
141
- // console.log('✅ User verified');
142
- callback(null, true);
143
- }else{
144
- // console.log("❌ Code isn't correct")
145
- callback(null, false);
146
- }
78
+ return {
79
+ send: async (options) => {
80
+ return await senderFn(options); // call user function
147
81
  }
148
- });
149
- }
150
-
151
- makeOTP(length){
152
- const buffer = crypto.randomBytes(length);
153
- const number = parseInt(buffer.toString('hex'), 16)
154
- .toString()
155
- .slice(0, length);
156
- return number.padStart(length, '0');
157
- }
158
-
159
- getOTP(email, callback){
160
- this.db.get(`SELECT * FROM verifications WHERE email = ?`, [email], (err, row)=>{
161
- if(err) return callback(err);
162
- if(!row) return callback(null, null);
163
-
164
- callback(null, { code: row.code, expiresAt: row.expiresAt });
165
- });
82
+ };
166
83
  }
167
-
168
- cleanExpired() {
169
- const now = new Date().toISOString();
170
- this.db.run(
171
- `DELETE FROM verifications WHERE expiresAt <= ?`,
172
- [now],
173
- function(err) {
174
- if (err) console.error("Error cleaning expired OTPs:", err.message);
175
- // Optional: console.log(`${this.changes} expired OTPs removed`);
176
- }
177
- );
178
- }
179
-
180
84
  }
181
85
 
182
- module.exports = Verifier;
86
+ module.exports = AuthVerify;
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "dependencies": {
3
+ "axios": "^1.12.2",
3
4
  "crypto": "^1.0.1",
4
5
  "express": "^5.1.0",
6
+ "ioredis": "^5.8.1",
7
+ "jsonwebtoken": "^9.0.2",
8
+ "node-telegram-bot-api": "^0.66.0",
5
9
  "nodemailer": "^7.0.6",
6
- "sqlite3": "^5.1.7"
10
+ "redis": "^5.8.3",
11
+ "twilio": "^5.10.3",
12
+ "uuid": "^13.0.0"
7
13
  },
8
14
  "name": "auth-verify",
9
- "version": "0.0.2",
15
+ "version": "1.1.0",
10
16
  "description": "A simple Node.js library for sending and verifying OTP via email",
11
17
  "main": "index.js",
12
- "devDependencies": {},
13
18
  "scripts": {
14
19
  "test": "node test.js"
15
20
  },
@@ -31,7 +36,8 @@
31
36
  "login",
32
37
  "two-factor",
33
38
  "2fa",
34
- "email"
39
+ "email",
40
+ "jwt"
35
41
  ],
36
42
  "author": "Jahongir Sobirov",
37
43
  "license": "MIT",
package/readme.md CHANGED
@@ -1,119 +1,296 @@
1
1
  # auth-verify
2
2
 
3
- `auth-verify` is a **Node.js library** for handling **OTP (One-Time Password) generation, email verification, and expiration tracking**. It provides secure OTP generation, rate-limiting, cooldowns, and integration with **Nodemailer** for sending verification emails.
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(...)`
4
9
 
5
- ---
6
-
7
- ## Features
8
-
9
- - Generate secure OTP codes
10
- - Send OTP via **email** using Nodemailer
11
- - Verify OTP codes with expiration checks
12
- - Limit OTP requests per day and enforce cooldowns
13
- - Store OTPs in a **SQLite database**
14
- - Fully asynchronous with callback support
10
+ > This README documents the code structure and APIs found in the library files you provided (OTPManager, JWTManager, SessionManager, AuthVerify).
15
11
 
16
12
  ---
17
13
 
18
14
  ## Installation
19
15
 
20
16
  ```bash
17
+ # from npm (when published)
21
18
  npm install auth-verify
19
+
20
+ # or locally during development
21
+ # copy the package into your project and `require` it`
22
22
  ```
23
- ### 🚀 Quick Start
24
23
 
25
- ##### 1. Initialize Verifier
24
+ ---
25
+
26
+ ## Quick overview
27
+
28
+ - `AuthVerify` (entry): constructs and exposes `.jwt`, `.otp`, and (optionally) `.session` managers.
29
+ - `JWTManager`: sign, verify, decode, revoke tokens. Supports `storeTokens: "memory" | "redis" | "none"`.
30
+ - `OTPManager`: generate, store, send, verify, resend OTPs. Supports `storeTokens: "memory" | "redis" | "none"`. Supports email, SMS helper, Telegram bot, and custom dev senders.
31
+ - `SessionManager`: simple session creation/verification/destroy with memory or Redis backend.
32
+
33
+ ---
34
+
35
+ ## Example: Initialize library (CommonJS)
36
+
26
37
  ```js
27
- const Verifier = require('auth-verify');
28
-
29
- const verifier = new Verifier({
30
- sender: 'your_email@example.com',
31
- pass: 'your_email_password',
32
- serv: 'gmail', // SMTP service name
33
- otp: {
34
- leng: 6, // OTP length (default: 6)
35
- expMin: 3, // OTP expiration in minutes (default: 3)
36
- limit: 5, // Max requests per day (default: 5)
37
- cooldown: 60 // Cooldown between requests in seconds (default: 60)
38
- }
38
+ const AuthVerify = require('auth-verify');
39
+
40
+ const auth = new AuthVerify({
41
+ jwtSecret: "super_secret_value",
42
+ storeTokens: "memory", // or "redis" or "none"
43
+ otpExpiry: "5m", // supports number (seconds) OR string like '30s', '5m', '1h'
44
+ otpHash: "sha256", // optional OTP hashing algorithm
45
+ // you may pass redisUrl inside managers' own options when using redis
39
46
  });
40
47
  ```
41
- ##### 2. Send OTP
48
+
49
+ ---
50
+
51
+ ## JWT usage
52
+
42
53
  ```js
43
- verifier
44
- .html("<h1>Your OTP is {otp}</h1>") // optional HTML template
45
- .subject("Verify your account: {otp}") // optional subject
46
- .text("Your OTP is {otp}") // optional plain text
47
- .sendTo('user@example.com', (err, success) => {
48
- if (err) return console.error(err);
49
- console.log("OTP sent successfully!");
50
- });
54
+ // create JWT
55
+ const token = await auth.jwt.sign({ userId: 123 }, '1h'); // expiry string or number (ms)
56
+ console.log('token', token);
57
+
58
+ // verify
59
+ const decoded = await auth.jwt.verify(token);
60
+ console.log('decoded', decoded);
61
+
62
+ // decode without verification
63
+ const payload = await auth.jwt.decode(token);
64
+
65
+ // revoke a token (immediately)
66
+ await auth.jwt.revoke(token, 0);
67
+
68
+ // revoke token until a time in the future (e.g. '10m' or number ms)
69
+ await auth.jwt.revokeUntil(token, '10m');
70
+
71
+ // check if token is revoked (returns boolean)
72
+ const isRevoked = await auth.jwt.isRevoked(token);
51
73
  ```
52
- ##### 3. Verify OTP
74
+ ## 🍪 Automatic Cookie Handling (New in v1.1.0)
75
+
76
+ You can now automatically store and verify JWTs via HTTP cookies — no need to manually send them!
53
77
  ```js
54
- verifier.code('123456').verifyFor('user@example.com', (err, isValid) => {
55
- if (err) return console.error(err);
56
- console.log(isValid ? "✅ OTP verified" : "❌ Invalid or expired OTP");
78
+ const AuthVerify = require("auth-verify");
79
+ const express = require("express");
80
+ const app = express();
81
+
82
+ const auth = new AuthVerify({
83
+ jwtSecret: "supersecret", storeTokens: "memory"
84
+ });
85
+
86
+ app.post("/login", async (req, res) => {
87
+ const token = await auth.jwt.sign({ userId: 1 }, "5s", { res });
88
+ res.json({ token }); // token is also set as cookie automatically
89
+ });
90
+
91
+ app.get("/verify", async (req, res) => {
92
+ try {
93
+ const data = await auth.jwt.verify(req); // auto reads from cookie
94
+ res.json({ valid: true, data });
95
+ } catch (err) {
96
+ res.json({ valid: false, error: err.message });
97
+ }
57
98
  });
99
+
100
+ app.listen(3000, () => console.log("🚀 Server running at http://localhost:3000"));
58
101
  ```
59
- ##### 4. Get OTP Details (for testing/debugging)
102
+
103
+ What it does automatically:
104
+
105
+ - Saves token in a secure HTTP-only cookie
106
+ - Reads and verifies token from cookies
107
+ - Supports both async/await and callback styles
108
+
109
+ Notes:
110
+ - `sign` and `verify` support callback and promise styles in the implementation. When `storeTokens` is `"redis"` you should use the promise/async style (callback mode returns an error for redis in the current implementation).
111
+
112
+ ---
113
+
114
+ ## OTP (email / sms / telegram / custom sender)
115
+
116
+ ### Configure sender
117
+
118
+ You can set the default sender (email/sms/telegram):
119
+
60
120
  ```js
61
- verifier.getOTP('user@example.com', (err, data) => {
62
- if (err) return console.error(err);
63
- console.log(data); // { code: '123456', expiresAt: '2025-09-28T...' }
121
+ // email example
122
+ auth.otp.setSender({
123
+ via: 'email',
124
+ sender: 'your@address.com',
125
+ pass: 'app-password-or-smtp-pass',
126
+ service: 'gmail' // or 'smtp'
127
+ // if smtp service: host, port, secure (boolean)
128
+ });
129
+
130
+ // sms example (the internal helper expects provider/apiKey or mock flag)
131
+ auth.otp.setSender({
132
+ via: 'sms',
133
+ provider: 'infobip',
134
+ apiKey: 'xxx',
135
+ apiSecret: 'yyy',
136
+ sender: 'SENDER_NAME',
137
+ mock: true // in dev prints message instead of sending
138
+ });
139
+
140
+ // telegram example
141
+ auth.otp.setSender({
142
+ via: 'telegram',
143
+ token: '123:ABC', // bot token
144
+ // call auth.otp.setupTelegramBot(token) to start the bot
64
145
  });
65
146
  ```
66
- ##### 5. Clean Expired OTPs
147
+
148
+ ### Generate → Save → Send (chainable)
149
+
150
+ OTP generation is chainable: `generate()` returns the OTP manager instance.
151
+
152
+ ```js
153
+ // chainable + callback style example
154
+ auth.otp.generate(6).set('user@example.com', (err) => {
155
+ if (err) throw err;
156
+ auth.otp.message({
157
+ to: 'user@example.com',
158
+ subject: 'Your OTP',
159
+ html: `Your code: <b>${auth.otp.code}</b>`
160
+ }, (err, info) => {
161
+ if (err) console.error('send error', err);
162
+ else console.log('sent', info && info.messageId);
163
+ });
164
+ });
165
+ ```
166
+
167
+ Async/await style:
168
+
169
+ ```js
170
+ await auth.otp.generate(6); // generates and stores `auth.otp.code`
171
+ await auth.otp.set('user@example.com'); // saves OTP into memory/redis
172
+ await auth.otp.message({
173
+ to: 'user@example.com',
174
+ subject: 'Verify',
175
+ html: `Your code: <b>${auth.otp.code}</b>`
176
+ });
177
+ ```
178
+
179
+ ### Verify
180
+
67
181
  ```js
68
- verifier.cleanExpired(); // Deletes expired OTPs from the database
182
+ // Promise style
183
+ try {
184
+ const ok = await auth.otp.verify({ check: 'user@example.com', code: '123456' });
185
+ console.log('verified', ok);
186
+ } catch (err) {
187
+ console.error('verify failed', err.message);
188
+ }
189
+
190
+ // Callback style also supported: auth.otp.verify({check, code}, callback)
69
191
  ```
70
- #### API Reference
71
- `new Verifier(options)`
72
192
 
73
- `sender` sender email for Nodemailer
193
+ ### Resend and cooldown / max attempts
194
+
195
+ - `auth.otp.cooldown('30s')` or `auth.otp.cooldown(30000)` — set cooldown duration.
196
+ - `auth.otp.maxAttempt(5)` — set maximum attempts allowed.
197
+ - `auth.otp.resend(identifier)` — regenerate and resend OTP, observing cooldown and expiry rules.
198
+
199
+ `resend` returns the new code (promise style) or calls callback.
200
+
201
+ ---
202
+
203
+ ## Telegram integration
204
+
205
+ There are two ways to use Telegram flow:
206
+
207
+ 1. Use the built-in `senderConfig.via = 'telegram'` and call `auth.otp.setupTelegramBot(botToken)` — this starts a polling bot that asks users to share their phone via `/start`, and then matches the phone to in-memory/Redis OTP records and replies with the code.
208
+
209
+ 2. Developer-supplied custom sender (see below) — you can create your own bot and call it from `auth.use(...).send(...)` or register via `auth.register.sender()`.
74
210
 
75
- `pass` email password
211
+ **Important**: Only one bot using long polling must be running per bot token — if you get `409 Conflict` it's because another process or instance is already polling that bot token.
76
212
 
77
- `serv` – SMTP service name
213
+ ---
214
+
215
+ ## Developer extensibility (custom senders)
216
+
217
+ You can register custom senders and use them:
218
+
219
+ ```js
220
+ // register a named sender function
221
+ auth.register.sender('consoleOtp', async ({ to, code }) => {
222
+ console.log(`[DEV SEND] send to ${to}: ${code}`);
223
+ });
224
+
225
+ // use it later (chainable)
226
+ await auth.use('consoleOtp').send({ to: '+998901234567', code: await auth.otp.generate(5) });
227
+ ```
228
+
229
+ ---
78
230
 
79
- `otp` object:
231
+ When a custom sender is registered, `auth.otp.message()` will first attempt the `customSender` before falling back to built-in providers.
80
232
 
81
- `leng` (number) – OTP length (default: 6)
233
+ ---
82
234
 
83
- `expMin` (number) – OTP expiration in minutes (default: 3)
235
+ ## SessionManager
84
236
 
85
- `limit` (number) – Max requests per day (default: 5)
237
+ ```js
238
+ const SessionManager = require('./src/session'); // or auth.session after export
239
+ const sessions = new SessionManager({ storeTokens: 'redis' });
86
240
 
87
- `cooldown` (number) – Cooldown in seconds between requests (default: 60)
241
+ // create
242
+ const sessionId = await sessions.create('user123', { expiresIn: '2h' });
88
243
 
89
- #### Methods
244
+ // verify
245
+ const userId = await sessions.verify(sessionId);
90
246
 
91
- `html(content)` – Sets optional HTML content with {otp} placeholder
247
+ // destroy
248
+ await sessions.destroy(sessionId);
249
+ ```
92
250
 
93
- `subject(content)` – Sets optional email subject with {otp} placeholder
251
+ Notes:
252
+ - `expiresIn` accepts numeric seconds or strings like `'30s'`, `'5m'`, `'1h'`, `'1d'`.
253
+
254
+ ---
94
255
 
95
- `text(content)` – Sets optional plain text with {otp} placeholder
256
+ ## Helpers
96
257
 
97
- `sendTo(email, callback)` Sends OTP to an email
258
+ `helpers/helper.js` exposes utility functions used by managers:
98
259
 
99
- `code(otp)` Set user-provided OTP for verification
260
+ - `generateSecureOTP(length, hashAlgorithm)` returns secure numeric OTP string
261
+ - `parseTime(strOrNumber)` — converts `'1h' | '30s' | number` into milliseconds
262
+ - `resendGeneratedOTP(params)` — helper to send email via nodemailer (used by resend)
263
+ - `sendSMS(params)` — helper for sending SMS using supported providers or mock
100
264
 
101
- `verifyFor(email, callback)` – Verify OTP for the given email
265
+ ---
102
266
 
103
- `getOTP(email, callback)` Retrieve OTP and expiration from DB
267
+ ## Error handling and notes
104
268
 
105
- `cleanExpired()` Delete expired OTPs from database
269
+ - Many methods support both **callback** and **Promise (async/await)** styles. When using Redis store, prefer **async/await** (callback variants intentionally return an error when Redis is selected).
270
+ - OTP storage keys are the user identifier you pass (email or phone number). Keep identifiers consistent.
271
+ - Be careful when using Telegram polling: do not run two instances with polling true for the same bot token (use webhooks or a single process).
272
+ - When configuring SMTP (non-Gmail), provide `host`, `port` and `secure` in `setSender()`.
106
273
 
107
- #### Database
274
+ ---
108
275
 
109
- Uses SQLite (authverify.db) to store:
276
+ ## Suggested folder structure
110
277
 
111
- Email
278
+ ```
279
+ auth-verify/
280
+ ├─ README.md
281
+ ├─ package.json
282
+ ├─ src/
283
+ │ ├─ index.js // exports AuthVerify
284
+ │ ├─ jwt.js
285
+ │ ├─ otp.js
286
+ │ ├─ session.js
287
+ │ └─ helpers/helper.js
288
+ ```
112
289
 
113
- OTP code
290
+ ---
114
291
 
115
- Expiration timestamp
292
+ ## Contributing & License
116
293
 
117
- Request count
294
+ Contributions welcome! Open issues / PRs for bugs, improvements, or API suggestions.
118
295
 
119
- Last request timestamp
296
+ MIT © 2025 — Jahongir Sobirov