ace-auth 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 +225 -0
- package/dist/adapters/MemoryStore.d.ts +10 -0
- package/dist/adapters/MemoryStore.js +62 -0
- package/dist/adapters/MongoStore.d.ts +11 -0
- package/dist/adapters/MongoStore.js +41 -0
- package/dist/adapters/PostgressStore.d.ts +25 -0
- package/dist/adapters/PostgressStore.js +72 -0
- package/dist/adapters/RedisStore.d.ts +38 -0
- package/dist/adapters/RedisStore.js +91 -0
- package/dist/core/ZenAuth.d.ts +64 -0
- package/dist/core/ZenAuth.js +140 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +16 -0
- package/dist/interfaces/IStore.d.ts +15 -0
- package/dist/interfaces/IStore.js +3 -0
- package/dist/middleware/gatekeeper.d.ts +8 -0
- package/dist/middleware/gatekeeper.js +43 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# 🛡️ ZenAuth
|
|
2
|
+
|
|
3
|
+
> **Stateful Security, Stateless Speed.**
|
|
4
|
+
> An enterprise-grade identity management library featuring "Graceful Token Rotation," Device Fingerprinting, and Sliding Window sessions.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@namra_ace/zen-auth)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 💡 Why ZenAuth?
|
|
14
|
+
|
|
15
|
+
In modern web development, you typically have to choose between **Security** (short-lived JWTs) and **User Experience** (long-lived sessions).
|
|
16
|
+
|
|
17
|
+
**ZenAuth gives you both.** It uses a **Hybrid Architecture** to maintain security without forcing users to log in repeatedly.
|
|
18
|
+
|
|
19
|
+
| Feature | Standard JWT | ZenAuth |
|
|
20
|
+
|------------------|-----------------------------|-----------------------------|
|
|
21
|
+
| **Revocation** | ❌ Impossible until expiry | ✅ **Instant** (DB Backed) |
|
|
22
|
+
| **Performance** | ✅ High (Stateless) | ✅ **High** (Redis Caching) |
|
|
23
|
+
| **UX** | ❌ Hard Logout on expiry | ✅ **Graceful Auto-Rotation** |
|
|
24
|
+
| **Device Mgmt** | ❌ None | ✅ **Active Sessions View** |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 📦 Installation
|
|
29
|
+
|
|
30
|
+
Install ZenAuth via npm:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @namra_ace/zen-auth
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🚀 Quick Start
|
|
39
|
+
|
|
40
|
+
### 1. Initialize
|
|
41
|
+
|
|
42
|
+
ZenAuth is database-agnostic. Below is a standard production setup using Redis:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { ZenAuth, RedisStore } from '@namra_ace/zen-auth';
|
|
46
|
+
import { createClient } from 'redis';
|
|
47
|
+
|
|
48
|
+
// 1. Connect to Redis
|
|
49
|
+
const redis = createClient();
|
|
50
|
+
await redis.connect();
|
|
51
|
+
|
|
52
|
+
// 2. Initialize Auth Engine
|
|
53
|
+
const auth = new ZenAuth({
|
|
54
|
+
secret: process.env.JWT_SECRET || 'super-secret',
|
|
55
|
+
store: new RedisStore(redis),
|
|
56
|
+
sessionDuration: 30 * 24 * 60 * 60, // 30 Days (Sliding Window)
|
|
57
|
+
tokenDuration: '15m', // Rotate token every 15 mins
|
|
58
|
+
smtp: { // Optional: For Email OTP
|
|
59
|
+
host: 'smtp.example.com',
|
|
60
|
+
auth: { user: '...', pass: '...' }
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### 2. Login (Capture Device Info)
|
|
68
|
+
|
|
69
|
+
Pass the request object (`req`) so ZenAuth can fingerprint the device (IP/User-Agent).
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import express from 'express';
|
|
73
|
+
const app = express();
|
|
74
|
+
|
|
75
|
+
app.post('/api/login', async (req, res) => {
|
|
76
|
+
// ... validate user credentials first ...
|
|
77
|
+
const userId = 'user_123';
|
|
78
|
+
|
|
79
|
+
// Create Session & Token
|
|
80
|
+
const { token, sessionId } = await auth.login({ id: userId, role: 'admin' }, req);
|
|
81
|
+
|
|
82
|
+
res.json({ token });
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### 3. Protect Routes (Middleware)
|
|
89
|
+
|
|
90
|
+
Use the included `gatekeeper` middleware to secure endpoints. It automatically handles Graceful Expiration.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { gatekeeper } from '@namra_ace/zen-auth/middleware';
|
|
94
|
+
|
|
95
|
+
app.get('/api/profile', gatekeeper(auth), (req, res) => {
|
|
96
|
+
// If token was rotated, the new one is in res.headers['x-zen-token']
|
|
97
|
+
res.json({ message: `Hello User ${req.user.id}` });
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🔌 Database Adapters
|
|
104
|
+
|
|
105
|
+
ZenAuth works with any database. Import the specific adapter you need.
|
|
106
|
+
|
|
107
|
+
### Redis (Recommended for Speed)
|
|
108
|
+
|
|
109
|
+
Uses Secondary Indexing (Sets) to map Users ↔ Sessions for O(1) lookups.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { RedisStore } from '@namra_ace/zen-auth/adapters';
|
|
113
|
+
// Requires 'redis' package installed
|
|
114
|
+
const store = new RedisStore(redisClient);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### PostgreSQL (Persistent)
|
|
118
|
+
|
|
119
|
+
Requires a table with columns: `sid` (text), `sess` (json), `expired_at` (timestamp).
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { PostgresStore } from '@namra_ace/zen-auth/adapters';
|
|
123
|
+
// Requires 'pg' pool
|
|
124
|
+
const store = new PostgresStore(pool, 'auth_sessions_table');
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### MongoDB
|
|
128
|
+
|
|
129
|
+
Stores sessions as documents. Good for no-setup environments.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { MongoStore } from '@namra_ace/zen-auth/adapters';
|
|
133
|
+
// Requires 'mongoose' connection
|
|
134
|
+
const store = new MongoStore(mongoose.connection.collection('sessions'));
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🧠 Advanced Features
|
|
140
|
+
|
|
141
|
+
### 📱 Device Management Dashboard
|
|
142
|
+
|
|
143
|
+
Allow users to see all their logged-in devices and remotely log them out (like Netflix/Google).
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// GET /api/devices
|
|
147
|
+
app.get('/api/devices', gatekeeper(auth), async (req, res) => {
|
|
148
|
+
// Returns: [{ device: { ip: '...', userAgent: 'Chrome' }, loginAt: '...' }]
|
|
149
|
+
const sessions = await auth.getActiveSessions(req.user.id);
|
|
150
|
+
res.json(sessions);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// POST /api/devices/logout-all
|
|
154
|
+
app.post('/api/devices/logout-all', gatekeeper(auth), async (req, res) => {
|
|
155
|
+
await auth.logoutAll(req.user.id);
|
|
156
|
+
res.json({ success: true, message: "Logged out of all other devices" });
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### 📧 Passwordless Login (OTP)
|
|
163
|
+
|
|
164
|
+
Built-in support for generating and verifying Email One-Time-Passwords.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// 1. Send Code
|
|
168
|
+
app.post('/auth/send-code', async (req, res) => {
|
|
169
|
+
await auth.sendOTP(req.body.email);
|
|
170
|
+
res.send('Code sent!');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// 2. Verify & Login
|
|
174
|
+
app.post('/auth/verify-code', async (req, res) => {
|
|
175
|
+
const { valid } = await auth.verifyOTP(req.body.email, req.body.code);
|
|
176
|
+
|
|
177
|
+
if (valid) {
|
|
178
|
+
const { token } = await auth.login({ email: req.body.email }, req);
|
|
179
|
+
res.json({ token });
|
|
180
|
+
} else {
|
|
181
|
+
res.status(401).send('Invalid Code');
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 🏗️ Architecture: "Graceful Expiration"
|
|
189
|
+
|
|
190
|
+
This is the core problem ZenAuth solves.
|
|
191
|
+
|
|
192
|
+
**Scenario:** User leaves a tab open for 20 minutes. The 15-minute JWT expires.
|
|
193
|
+
|
|
194
|
+
- **Standard Library:** Request fails (401). User is forced to log in again. 😡
|
|
195
|
+
- **ZenAuth:** Middleware catches the expiry error, checks the database, and issues a new token if the session is still valid.
|
|
196
|
+
|
|
197
|
+
```mermaid
|
|
198
|
+
sequenceDiagram
|
|
199
|
+
participant Client
|
|
200
|
+
participant Middleware
|
|
201
|
+
participant Database
|
|
202
|
+
|
|
203
|
+
Client->>Middleware: Sends Request (Token Expired)
|
|
204
|
+
Middleware->>Middleware: Signature Valid? ✅
|
|
205
|
+
Middleware->>Middleware: Time Check: Expired ❌
|
|
206
|
+
|
|
207
|
+
Note right of Middleware: "Graceful Rescue" Triggered
|
|
208
|
+
|
|
209
|
+
Middleware->>Database: Check Session ID
|
|
210
|
+
Database-->>Middleware: Session Active (30 Days left)
|
|
211
|
+
|
|
212
|
+
Middleware->>Client: 200 OK + New Token (Header)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 🧪 Security & Testing
|
|
218
|
+
|
|
219
|
+
This library is 100% covered by tests using Vitest.
|
|
220
|
+
|
|
221
|
+
- ✅ **Replay Protection:** OTPs are deleted immediately after use.
|
|
222
|
+
- ✅ **Tamper Proofing:** Tokens signed with invalid secrets are rejected immediately.
|
|
223
|
+
- ✅ **Lazy Cleanup:** Expired sessions are automatically cleaned up from the user index during read operations to prevent memory leaks.
|
|
224
|
+
|
|
225
|
+
---
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { IStore } from "../interfaces/IStore";
|
|
2
|
+
export declare class MemoryStore implements IStore {
|
|
3
|
+
private store;
|
|
4
|
+
set(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
5
|
+
get(key: string): Promise<string | null>;
|
|
6
|
+
delete(key: string): Promise<void>;
|
|
7
|
+
touch(key: string, ttlSeconds: number): Promise<void>;
|
|
8
|
+
findAllByUser(userId: string): Promise<string[]>;
|
|
9
|
+
deleteByUser(userId: string): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemoryStore = void 0;
|
|
4
|
+
class MemoryStore {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.store = new Map();
|
|
7
|
+
}
|
|
8
|
+
async set(key, value, ttlSeconds) {
|
|
9
|
+
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
10
|
+
// We try to parse the User ID from the payload to index it
|
|
11
|
+
// Assumption: The payload has an 'id' or '_id' field.
|
|
12
|
+
const parsed = JSON.parse(value);
|
|
13
|
+
const userId = parsed.id || parsed._id || 'unknown';
|
|
14
|
+
this.store.set(key, { value, expiresAt, userId });
|
|
15
|
+
}
|
|
16
|
+
async get(key) {
|
|
17
|
+
const record = this.store.get(key);
|
|
18
|
+
if (!record)
|
|
19
|
+
return null;
|
|
20
|
+
if (Date.now() > record.expiresAt) {
|
|
21
|
+
this.store.delete(key);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return record.value;
|
|
25
|
+
}
|
|
26
|
+
async delete(key) {
|
|
27
|
+
this.store.delete(key);
|
|
28
|
+
}
|
|
29
|
+
async touch(key, ttlSeconds) {
|
|
30
|
+
const record = this.store.get(key);
|
|
31
|
+
if (record) {
|
|
32
|
+
record.expiresAt = Date.now() + ttlSeconds * 1000;
|
|
33
|
+
this.store.set(key, record);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// --- NEW METHODS ---
|
|
37
|
+
async findAllByUser(userId) {
|
|
38
|
+
const sessions = [];
|
|
39
|
+
// In a real database (SQL/Mongo), this is a query.
|
|
40
|
+
// In Map, we have to iterate (Slow, but fine for memory/dev).
|
|
41
|
+
for (const [key, record] of this.store.entries()) {
|
|
42
|
+
if (record.userId === String(userId)) {
|
|
43
|
+
// cleanup expired ones while we are here
|
|
44
|
+
if (Date.now() > record.expiresAt) {
|
|
45
|
+
this.store.delete(key);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
sessions.push(record.value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return sessions;
|
|
53
|
+
}
|
|
54
|
+
async deleteByUser(userId) {
|
|
55
|
+
for (const [key, record] of this.store.entries()) {
|
|
56
|
+
if (record.userId === String(userId)) {
|
|
57
|
+
this.store.delete(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.MemoryStore = MemoryStore;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { IStore } from "../interfaces/IStore";
|
|
2
|
+
export declare class MongoStore implements IStore {
|
|
3
|
+
private model;
|
|
4
|
+
constructor(mongooseModel: any);
|
|
5
|
+
findAllByUser(userId: string): Promise<string[]>;
|
|
6
|
+
deleteByUser(userId: string): Promise<void>;
|
|
7
|
+
set(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
8
|
+
get(key: string): Promise<string | null>;
|
|
9
|
+
delete(key: string): Promise<void>;
|
|
10
|
+
touch(key: string, ttlSeconds: number): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MongoStore = void 0;
|
|
4
|
+
class MongoStore {
|
|
5
|
+
constructor(mongooseModel) {
|
|
6
|
+
this.model = mongooseModel;
|
|
7
|
+
}
|
|
8
|
+
async findAllByUser(userId) {
|
|
9
|
+
const docs = await this.model.findOne({ userId });
|
|
10
|
+
if (!docs)
|
|
11
|
+
return [];
|
|
12
|
+
return docs.map((doc) => doc.data);
|
|
13
|
+
}
|
|
14
|
+
async deleteByUser(userId) {
|
|
15
|
+
await this.model.deleteOne({ userId });
|
|
16
|
+
}
|
|
17
|
+
async set(key, value, ttlSeconds) {
|
|
18
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
|
19
|
+
// Upsert: Update if exists, Insert if new
|
|
20
|
+
await this.model.updateOne({ _id: key }, { _id: key, data: value, expiresAt }, { upsert: true });
|
|
21
|
+
}
|
|
22
|
+
async get(key) {
|
|
23
|
+
const doc = await this.model.findOne({ _id: key });
|
|
24
|
+
if (!doc)
|
|
25
|
+
return null;
|
|
26
|
+
// MongoDB TTL indexes usually handle cleanup, but we double-check here
|
|
27
|
+
if (new Date() > doc.expiresAt) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return doc.data;
|
|
31
|
+
}
|
|
32
|
+
async delete(key) {
|
|
33
|
+
await this.model.deleteOne({ _id: key });
|
|
34
|
+
}
|
|
35
|
+
async touch(key, ttlSeconds) {
|
|
36
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
|
37
|
+
// The "Slide": Just update the date
|
|
38
|
+
await this.model.updateOne({ _id: key }, { $set: { expiresAt } });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.MongoStore = MongoStore;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { IStore } from '../interfaces/IStore';
|
|
2
|
+
/**
|
|
3
|
+
* SQL SCHEMA REQUIREMENT:
|
|
4
|
+
* * CREATE TABLE auth_sessions (
|
|
5
|
+
* sid VARCHAR(255) PRIMARY KEY,
|
|
6
|
+
* sess JSON NOT NULL,
|
|
7
|
+
* expired_at TIMESTAMPTZ NOT NULL
|
|
8
|
+
* );
|
|
9
|
+
* * CREATE INDEX idx_auth_sessions_expired_at ON auth_sessions(expired_at);
|
|
10
|
+
*/
|
|
11
|
+
interface PgPool {
|
|
12
|
+
query(text: string, params?: any[]): Promise<any>;
|
|
13
|
+
}
|
|
14
|
+
export declare class PostgresStore implements IStore {
|
|
15
|
+
private pool;
|
|
16
|
+
private tableName;
|
|
17
|
+
constructor(pool: PgPool, tableName?: string);
|
|
18
|
+
findAllByUser(userId: string): Promise<string[]>;
|
|
19
|
+
deleteByUser(userId: string): Promise<void>;
|
|
20
|
+
set(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
21
|
+
get(key: string): Promise<string | null>;
|
|
22
|
+
delete(key: string): Promise<void>;
|
|
23
|
+
touch(key: string, ttlSeconds: number): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresStore = void 0;
|
|
4
|
+
class PostgresStore {
|
|
5
|
+
// Allow user to customize table name
|
|
6
|
+
constructor(pool, tableName = 'auth_sessions') {
|
|
7
|
+
this.pool = pool;
|
|
8
|
+
this.tableName = tableName;
|
|
9
|
+
}
|
|
10
|
+
async findAllByUser(userId) {
|
|
11
|
+
const query = `
|
|
12
|
+
SELECT sess FROM ${this.tableName}
|
|
13
|
+
WHERE sess->>'userId' = $1 AND expired_at > NOW()
|
|
14
|
+
`;
|
|
15
|
+
const result = await this.pool.query(query, [userId]);
|
|
16
|
+
// Extract sessions and return them as an array of strings
|
|
17
|
+
return result.rows.map((row) => {
|
|
18
|
+
const data = row.sess;
|
|
19
|
+
return typeof data === 'string' ? data : JSON.stringify(data);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async deleteByUser(userId) {
|
|
23
|
+
const query = `
|
|
24
|
+
DELETE FROM ${this.tableName}
|
|
25
|
+
WHERE sess->>'userId' = $1
|
|
26
|
+
`;
|
|
27
|
+
await this.pool.query(query, [userId]);
|
|
28
|
+
}
|
|
29
|
+
async set(key, value, ttlSeconds) {
|
|
30
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
|
31
|
+
// We use ON CONFLICT to handle "Upserts" (Update if exists, Insert if new)
|
|
32
|
+
const query = `
|
|
33
|
+
INSERT INTO ${this.tableName} (sid, sess, expired_at)
|
|
34
|
+
VALUES ($1, $2, $3)
|
|
35
|
+
ON CONFLICT (sid)
|
|
36
|
+
DO UPDATE SET sess = $2, expired_at = $3
|
|
37
|
+
`;
|
|
38
|
+
await this.pool.query(query, [key, value, expiresAt]);
|
|
39
|
+
}
|
|
40
|
+
async get(key) {
|
|
41
|
+
// We perform a "Lazy Delete" check here.
|
|
42
|
+
// Even if the row exists, if it's expired, we treat it as null.
|
|
43
|
+
const query = `
|
|
44
|
+
SELECT sess FROM ${this.tableName}
|
|
45
|
+
WHERE sid = $1 AND expired_at > NOW()
|
|
46
|
+
`;
|
|
47
|
+
const result = await this.pool.query(query, [key]);
|
|
48
|
+
if (result.rows && result.rows.length > 0) {
|
|
49
|
+
// Postgres returns JSON columns as objects, but our interface expects a string
|
|
50
|
+
// so we might need to stringify it back, or just return the data depending on implementation.
|
|
51
|
+
// Since ZenAuth expects a stringified payload:
|
|
52
|
+
const data = result.rows[0].sess;
|
|
53
|
+
return typeof data === 'string' ? data : JSON.stringify(data);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
async delete(key) {
|
|
58
|
+
const query = `DELETE FROM ${this.tableName} WHERE sid = $1`;
|
|
59
|
+
await this.pool.query(query, [key]);
|
|
60
|
+
}
|
|
61
|
+
async touch(key, ttlSeconds) {
|
|
62
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
|
63
|
+
// Just update the timestamp to keep the session alive
|
|
64
|
+
const query = `
|
|
65
|
+
UPDATE ${this.tableName}
|
|
66
|
+
SET expired_at = $1
|
|
67
|
+
WHERE sid = $2
|
|
68
|
+
`;
|
|
69
|
+
await this.pool.query(query, [expiresAt, key]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.PostgresStore = PostgresStore;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { IStore } from '../interfaces/IStore';
|
|
2
|
+
interface RedisClient {
|
|
3
|
+
get(key: string): Promise<string | null>;
|
|
4
|
+
set(key: string, value: string, options?: any): Promise<any>;
|
|
5
|
+
del(key: string): Promise<any>;
|
|
6
|
+
expire(key: string, seconds: number): Promise<any>;
|
|
7
|
+
sAdd(key: string, value: string): Promise<any>;
|
|
8
|
+
sRem(key: string, value: string): Promise<any>;
|
|
9
|
+
sMembers(key: string): Promise<string[]>;
|
|
10
|
+
exists(key: string): Promise<number>;
|
|
11
|
+
}
|
|
12
|
+
export declare class RedisStore implements IStore {
|
|
13
|
+
private client;
|
|
14
|
+
constructor(client: RedisClient);
|
|
15
|
+
/**
|
|
16
|
+
* SAVE SESSION
|
|
17
|
+
* We now do two things:
|
|
18
|
+
* 1. Save the session data (Auto-expires).
|
|
19
|
+
* 2. Add the SessionID to the User's "Index" (A Redis Set).
|
|
20
|
+
*/
|
|
21
|
+
set(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
22
|
+
get(key: string): Promise<string | null>;
|
|
23
|
+
/**
|
|
24
|
+
* DELETE SESSION
|
|
25
|
+
* We must remove the data AND the reference in the index.
|
|
26
|
+
*/
|
|
27
|
+
delete(key: string): Promise<void>;
|
|
28
|
+
touch(key: string, ttlSeconds: number): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* FIND ALL BY USER (The Dashboard Feature)
|
|
31
|
+
* 1. Get all Session IDs from the Index Set.
|
|
32
|
+
* 2. Loop through them and fetch the actual data.
|
|
33
|
+
* 3. (Lazy Cleanup) If a session expired, remove it from the index.
|
|
34
|
+
*/
|
|
35
|
+
findAllByUser(userId: string): Promise<string[]>;
|
|
36
|
+
deleteByUser(userId: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisStore = void 0;
|
|
4
|
+
class RedisStore {
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* SAVE SESSION
|
|
10
|
+
* We now do two things:
|
|
11
|
+
* 1. Save the session data (Auto-expires).
|
|
12
|
+
* 2. Add the SessionID to the User's "Index" (A Redis Set).
|
|
13
|
+
*/
|
|
14
|
+
async set(key, value, ttlSeconds) {
|
|
15
|
+
await this.client.set(key, value, { EX: ttlSeconds });
|
|
16
|
+
// Extract UserID to build the index
|
|
17
|
+
// We assume the value is the JSON payload containing the user ID
|
|
18
|
+
try {
|
|
19
|
+
const payload = JSON.parse(value);
|
|
20
|
+
const userId = payload.id || payload._id || payload.userId;
|
|
21
|
+
if (userId) {
|
|
22
|
+
// Add this session ID to the user's list of active sessions
|
|
23
|
+
await this.client.sAdd(`idx:user:${userId}`, key);
|
|
24
|
+
// Safety: Expire the index too (slightly longer than session) so we don't leak memory
|
|
25
|
+
// if a user vanishes.
|
|
26
|
+
await this.client.expire(`idx:user:${userId}`, ttlSeconds + 3600);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
// If parsing fails, we just don't index it.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async get(key) {
|
|
34
|
+
return await this.client.get(key);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* DELETE SESSION
|
|
38
|
+
* We must remove the data AND the reference in the index.
|
|
39
|
+
*/
|
|
40
|
+
async delete(key) {
|
|
41
|
+
// 1. Get the data first to find the UserID (so we can clean the index)
|
|
42
|
+
const data = await this.client.get(key);
|
|
43
|
+
if (data) {
|
|
44
|
+
const payload = JSON.parse(data);
|
|
45
|
+
const userId = payload.id || payload._id || payload.userId;
|
|
46
|
+
if (userId) {
|
|
47
|
+
await this.client.sRem(`idx:user:${userId}`, key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// 2. Delete the actual session
|
|
51
|
+
await this.client.del(key);
|
|
52
|
+
}
|
|
53
|
+
async touch(key, ttlSeconds) {
|
|
54
|
+
await this.client.expire(key, ttlSeconds);
|
|
55
|
+
// Note: We ideally should update the index expiry too, but strictly not required for MVP
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* FIND ALL BY USER (The Dashboard Feature)
|
|
59
|
+
* 1. Get all Session IDs from the Index Set.
|
|
60
|
+
* 2. Loop through them and fetch the actual data.
|
|
61
|
+
* 3. (Lazy Cleanup) If a session expired, remove it from the index.
|
|
62
|
+
*/
|
|
63
|
+
async findAllByUser(userId) {
|
|
64
|
+
const indexKey = `idx:user:${userId}`;
|
|
65
|
+
const sessionIds = await this.client.sMembers(indexKey);
|
|
66
|
+
const activeSessions = [];
|
|
67
|
+
for (const sid of sessionIds) {
|
|
68
|
+
const sessionData = await this.client.get(sid);
|
|
69
|
+
if (sessionData) {
|
|
70
|
+
activeSessions.push(sessionData);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// LAZY CLEANUP: Redis deleted the session (TTL), but it's still in our Set.
|
|
74
|
+
// We clean it up now.
|
|
75
|
+
await this.client.sRem(indexKey, sid);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return activeSessions;
|
|
79
|
+
}
|
|
80
|
+
async deleteByUser(userId) {
|
|
81
|
+
const indexKey = `idx:user:${userId}`;
|
|
82
|
+
const sessionIds = await this.client.sMembers(indexKey);
|
|
83
|
+
// Delete all session keys
|
|
84
|
+
for (const sid of sessionIds) {
|
|
85
|
+
await this.client.del(sid);
|
|
86
|
+
}
|
|
87
|
+
// Delete the index itself
|
|
88
|
+
await this.client.del(indexKey);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.RedisStore = RedisStore;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { IStore } from '../interfaces/IStore';
|
|
2
|
+
export interface ZenAuthOptions {
|
|
3
|
+
secret: string;
|
|
4
|
+
store: IStore;
|
|
5
|
+
sessionDuration: number;
|
|
6
|
+
tokenDuration: string;
|
|
7
|
+
smtp?: any;
|
|
8
|
+
}
|
|
9
|
+
export declare class ZenAuth {
|
|
10
|
+
private options;
|
|
11
|
+
private mailer;
|
|
12
|
+
constructor(options: ZenAuthOptions);
|
|
13
|
+
/**
|
|
14
|
+
* LOGIN: Creates a session with Device Metadata
|
|
15
|
+
* @param payload - The user data (must include 'id' or '_id')
|
|
16
|
+
* @param req - Optional Express Request object to capture IP/User-Agent
|
|
17
|
+
*/
|
|
18
|
+
login(payload: any, req?: any): Promise<{
|
|
19
|
+
token: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* AUTHORIZE: Validates token AND slides the session window.
|
|
24
|
+
* Handles "Graceful Expiration" (allows expired token if session is valid).
|
|
25
|
+
*/
|
|
26
|
+
authorize(token: string): Promise<{
|
|
27
|
+
valid: boolean;
|
|
28
|
+
sessionId: string;
|
|
29
|
+
user: any;
|
|
30
|
+
error?: undefined;
|
|
31
|
+
} | {
|
|
32
|
+
valid: boolean;
|
|
33
|
+
error: any;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Helper to check DB and Slide the Window
|
|
37
|
+
*/
|
|
38
|
+
private validateSession;
|
|
39
|
+
signToken(sessionId: string, payload: any): string;
|
|
40
|
+
logout(sessionId: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Get all active devices for a user.
|
|
43
|
+
*/
|
|
44
|
+
getActiveSessions(userId: string): Promise<{
|
|
45
|
+
sessionId: string;
|
|
46
|
+
device: any;
|
|
47
|
+
loginAt: any;
|
|
48
|
+
user: any;
|
|
49
|
+
}[]>;
|
|
50
|
+
/**
|
|
51
|
+
* "Log me out of everywhere"
|
|
52
|
+
*/
|
|
53
|
+
logoutAll(userId: string): Promise<void>;
|
|
54
|
+
sendOTP(email: string): Promise<{
|
|
55
|
+
success: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
verifyOTP(email: string, code: string): Promise<{
|
|
58
|
+
valid: boolean;
|
|
59
|
+
error: string;
|
|
60
|
+
} | {
|
|
61
|
+
valid: boolean;
|
|
62
|
+
error?: undefined;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ZenAuth = void 0;
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
const uuid_1 = require("uuid");
|
|
9
|
+
const nodemailer_1 = __importDefault(require("nodemailer"));
|
|
10
|
+
class ZenAuth {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
if (this.options.smtp) {
|
|
14
|
+
this.mailer = nodemailer_1.default.createTransport(this.options.smtp);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// ==========================================
|
|
18
|
+
// CORE AUTHENTICATION LOGIC
|
|
19
|
+
// ==========================================
|
|
20
|
+
/**
|
|
21
|
+
* LOGIN: Creates a session with Device Metadata
|
|
22
|
+
* @param payload - The user data (must include 'id' or '_id')
|
|
23
|
+
* @param req - Optional Express Request object to capture IP/User-Agent
|
|
24
|
+
*/
|
|
25
|
+
async login(payload, req) {
|
|
26
|
+
const sessionId = (0, uuid_1.v4)();
|
|
27
|
+
// 1. Capture Device Info (The "System Design" Feature)
|
|
28
|
+
const deviceInfo = {
|
|
29
|
+
ip: req?.ip || req?.socket?.remoteAddress || 'unknown',
|
|
30
|
+
userAgent: req?.headers?.['user-agent'] || 'unknown',
|
|
31
|
+
loginAt: new Date().toISOString()
|
|
32
|
+
};
|
|
33
|
+
// 2. Merge Metadata with User Payload
|
|
34
|
+
// We store metadata in a reserved field '_meta'
|
|
35
|
+
const fullPayload = {
|
|
36
|
+
...payload,
|
|
37
|
+
_meta: deviceInfo
|
|
38
|
+
};
|
|
39
|
+
// 3. Save to Store
|
|
40
|
+
await this.options.store.set(sessionId, JSON.stringify(fullPayload), this.options.sessionDuration);
|
|
41
|
+
// 4. Generate Token
|
|
42
|
+
const token = this.signToken(sessionId, fullPayload);
|
|
43
|
+
return { token, sessionId };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* AUTHORIZE: Validates token AND slides the session window.
|
|
47
|
+
* Handles "Graceful Expiration" (allows expired token if session is valid).
|
|
48
|
+
*/
|
|
49
|
+
async authorize(token) {
|
|
50
|
+
try {
|
|
51
|
+
const decoded = jsonwebtoken_1.default.verify(token, this.options.secret);
|
|
52
|
+
return await this.validateSession(decoded.sessionId);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// RESUME FEATURE: Handle "Graceful Expiration"
|
|
56
|
+
if (err.name === 'TokenExpiredError') {
|
|
57
|
+
const decoded = jsonwebtoken_1.default.decode(token);
|
|
58
|
+
if (!decoded || !decoded.sessionId) {
|
|
59
|
+
return { valid: false, error: 'Invalid Token Structure' };
|
|
60
|
+
}
|
|
61
|
+
// Check if the DB Session is still alive
|
|
62
|
+
return await this.validateSession(decoded.sessionId);
|
|
63
|
+
}
|
|
64
|
+
return { valid: false, error: err.message };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Helper to check DB and Slide the Window
|
|
69
|
+
*/
|
|
70
|
+
async validateSession(sessionId) {
|
|
71
|
+
const sessionData = await this.options.store.get(sessionId);
|
|
72
|
+
if (!sessionData) {
|
|
73
|
+
return { valid: false, error: 'Session expired in database' };
|
|
74
|
+
}
|
|
75
|
+
await this.options.store.touch(sessionId, this.options.sessionDuration);
|
|
76
|
+
return {
|
|
77
|
+
valid: true,
|
|
78
|
+
sessionId: sessionId,
|
|
79
|
+
user: JSON.parse(sessionData)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
signToken(sessionId, payload) {
|
|
83
|
+
return jsonwebtoken_1.default.sign({ sessionId, ...payload }, this.options.secret, { expiresIn: this.options.tokenDuration });
|
|
84
|
+
}
|
|
85
|
+
async logout(sessionId) {
|
|
86
|
+
await this.options.store.delete(sessionId);
|
|
87
|
+
}
|
|
88
|
+
// ==========================================
|
|
89
|
+
// DASHBOARD & DEVICE MANAGEMENT
|
|
90
|
+
// ==========================================
|
|
91
|
+
/**
|
|
92
|
+
* Get all active devices for a user.
|
|
93
|
+
*/
|
|
94
|
+
async getActiveSessions(userId) {
|
|
95
|
+
const sessions = await this.options.store.findAllByUser(userId);
|
|
96
|
+
return sessions.map(s => {
|
|
97
|
+
const data = JSON.parse(s);
|
|
98
|
+
return {
|
|
99
|
+
sessionId: 'hidden', // Don't leak IDs to frontend
|
|
100
|
+
device: data._meta || { ip: 'unknown', userAgent: 'unknown' },
|
|
101
|
+
loginAt: data._meta?.loginAt,
|
|
102
|
+
user: data // User data
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* "Log me out of everywhere"
|
|
108
|
+
*/
|
|
109
|
+
async logoutAll(userId) {
|
|
110
|
+
await this.options.store.deleteByUser(userId);
|
|
111
|
+
}
|
|
112
|
+
// ==========================================
|
|
113
|
+
// EMAIL VERIFICATION LOGIC
|
|
114
|
+
// ==========================================
|
|
115
|
+
async sendOTP(email) {
|
|
116
|
+
if (!this.mailer)
|
|
117
|
+
throw new Error('SMTP config not provided');
|
|
118
|
+
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
|
119
|
+
// Save to Store (TTL: 10 mins)
|
|
120
|
+
await this.options.store.set(`otp:${email}`, code, 600);
|
|
121
|
+
await this.mailer.sendMail({
|
|
122
|
+
from: '"ZenAuth Security" <no-reply@zenauth.com>',
|
|
123
|
+
to: email,
|
|
124
|
+
subject: 'Your Verification Code',
|
|
125
|
+
html: `<h1>${code}</h1><p>Expires in 10 minutes.</p>`
|
|
126
|
+
});
|
|
127
|
+
return { success: true };
|
|
128
|
+
}
|
|
129
|
+
async verifyOTP(email, code) {
|
|
130
|
+
const key = `otp:${email}`;
|
|
131
|
+
const storedCode = await this.options.store.get(key);
|
|
132
|
+
if (!storedCode)
|
|
133
|
+
return { valid: false, error: 'Code expired or invalid' };
|
|
134
|
+
if (storedCode !== code)
|
|
135
|
+
return { valid: false, error: 'Incorrect code' };
|
|
136
|
+
await this.options.store.delete(key);
|
|
137
|
+
return { valid: true };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.ZenAuth = ZenAuth;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ZenAuth } from './core/ZenAuth';
|
|
2
|
+
export { IStore } from './interfaces/IStore';
|
|
3
|
+
export { MemoryStore } from './adapters/MemoryStore';
|
|
4
|
+
export { MongoStore } from './adapters/MongoStore';
|
|
5
|
+
export { RedisStore } from './adapters/RedisStore';
|
|
6
|
+
export { PostgresStore } from './adapters/PostgressStore';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/index.ts
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.PostgresStore = exports.RedisStore = exports.MongoStore = exports.MemoryStore = exports.ZenAuth = void 0;
|
|
5
|
+
// Export Core
|
|
6
|
+
var ZenAuth_1 = require("./core/ZenAuth");
|
|
7
|
+
Object.defineProperty(exports, "ZenAuth", { enumerable: true, get: function () { return ZenAuth_1.ZenAuth; } });
|
|
8
|
+
// Export Adapters
|
|
9
|
+
var MemoryStore_1 = require("./adapters/MemoryStore");
|
|
10
|
+
Object.defineProperty(exports, "MemoryStore", { enumerable: true, get: function () { return MemoryStore_1.MemoryStore; } });
|
|
11
|
+
var MongoStore_1 = require("./adapters/MongoStore");
|
|
12
|
+
Object.defineProperty(exports, "MongoStore", { enumerable: true, get: function () { return MongoStore_1.MongoStore; } });
|
|
13
|
+
var RedisStore_1 = require("./adapters/RedisStore");
|
|
14
|
+
Object.defineProperty(exports, "RedisStore", { enumerable: true, get: function () { return RedisStore_1.RedisStore; } });
|
|
15
|
+
var PostgressStore_1 = require("./adapters/PostgressStore");
|
|
16
|
+
Object.defineProperty(exports, "PostgresStore", { enumerable: true, get: function () { return PostgressStore_1.PostgresStore; } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface IStore {
|
|
2
|
+
set(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
3
|
+
get(key: string): Promise<string | null>;
|
|
4
|
+
delete(key: string): Promise<void>;
|
|
5
|
+
touch(key: string, ttlSeconds: number): Promise<void>;
|
|
6
|
+
/** * NEW: Find all sessions for a specific user.
|
|
7
|
+
* This allows building the "Active Devices" dashboard.
|
|
8
|
+
*/
|
|
9
|
+
findAllByUser(userId: string): Promise<string[]>;
|
|
10
|
+
/**
|
|
11
|
+
* NEW: "Logout All Devices"
|
|
12
|
+
* Deletes all sessions for a specific user ID.
|
|
13
|
+
*/
|
|
14
|
+
deleteByUser(userId: string): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gatekeeper = gatekeeper;
|
|
4
|
+
function gatekeeper(auth) {
|
|
5
|
+
return async (req, res, next) => {
|
|
6
|
+
try {
|
|
7
|
+
// 1. Extract Token
|
|
8
|
+
const authHeader = req.headers['authorization'];
|
|
9
|
+
if (!authHeader) {
|
|
10
|
+
return res.status(401).json({ error: 'No token provided' });
|
|
11
|
+
}
|
|
12
|
+
const token = authHeader.split(' ')[1]; // Remove "Bearer "
|
|
13
|
+
if (!token) {
|
|
14
|
+
return res.status(401).json({ error: 'Invalid token format' });
|
|
15
|
+
}
|
|
16
|
+
// 2. Verify & Slide Session
|
|
17
|
+
const result = await auth.authorize(token);
|
|
18
|
+
if (!result.valid) {
|
|
19
|
+
return res.status(401).json({ error: 'Invalid or expired session' });
|
|
20
|
+
}
|
|
21
|
+
// 3. Attach User to Request (for the route handler to use)
|
|
22
|
+
if ('user' in result && 'sessionId' in result) {
|
|
23
|
+
req.user = result.user;
|
|
24
|
+
req.sessionId = result.sessionId;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
return res.status(401).json({ error: 'Invalid or expired session' });
|
|
28
|
+
}
|
|
29
|
+
// 4. AUTOMATIC ROTATION (The Resume "Wow" Factor)
|
|
30
|
+
// We generate a brand new token for the *next* request.
|
|
31
|
+
// This ensures that even if the current token is stolen,
|
|
32
|
+
// it's already "used" and the client has moved to a new one.
|
|
33
|
+
const newToken = auth.signToken(result.sessionId, result.user);
|
|
34
|
+
// We send it back in a custom header
|
|
35
|
+
res.setHeader('X-Zen-Token', newToken);
|
|
36
|
+
next();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error('Guard Middleware Error:', error);
|
|
40
|
+
res.status(500).json({ error: 'Internal Auth Error' });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ace-auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Enterprise-grade identity management with graceful token rotation",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"auth",
|
|
17
|
+
"jwt",
|
|
18
|
+
"security",
|
|
19
|
+
"redis",
|
|
20
|
+
"authentication",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"author": "Your Name",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bcryptjs": "^2.4.6",
|
|
27
|
+
"@types/jsonwebtoken": "^9.0.7",
|
|
28
|
+
"@types/node": "^20.10.0",
|
|
29
|
+
"@types/nodemailer": "^6.4.14",
|
|
30
|
+
"@types/uuid": "^9.0.7",
|
|
31
|
+
"typescript": "^5.3.3",
|
|
32
|
+
"vitest": "^1.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"bcryptjs": "^2.4.3",
|
|
36
|
+
"jsonwebtoken": "^9.0.2",
|
|
37
|
+
"nodemailer": "^6.9.7",
|
|
38
|
+
"uuid": "^9.0.1"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"redis": "^4.0.0",
|
|
42
|
+
"mongoose": "^7.0.0 || ^8.0.0 || ^9.0.0",
|
|
43
|
+
"pg": "^8.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|