ace-auth 1.2.0 โ 1.2.3
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 +206 -129
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ๐ก๏ธ AceAuth
|
|
2
2
|
|
|
3
|
-
> **Stateful
|
|
4
|
-
>
|
|
3
|
+
> **Stateful security. Stateless speed.**
|
|
4
|
+
> A production-grade authentication engine that combines JWT performance with server-side control using a hybrid, cache-aware architecture.
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/ace-auth)
|
|
7
7
|

|
|
@@ -12,22 +12,47 @@
|
|
|
12
12
|
|
|
13
13
|
## ๐ก Why AceAuth?
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Most authentication systems force a trade-off:
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
- **Stateless JWTs** โ Fast, scalable, but impossible to revoke
|
|
18
|
+
- **Server sessions** โ Secure and controllable, but harder to scale
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
**AceAuth removes this trade-off.**
|
|
21
|
+
|
|
22
|
+
AceAuth uses:
|
|
23
|
+
- **JWTs as identifiers (not authority)**
|
|
24
|
+
- **A database as the source of truth**
|
|
25
|
+
- **A two-tier cache (RAM + DB) for performance**
|
|
26
|
+
|
|
27
|
+
This allows AceAuth to provide:
|
|
28
|
+
- Immediate revocation
|
|
29
|
+
- Transparent token rotation
|
|
30
|
+
- High throughput on hot paths
|
|
31
|
+
- Explicit, documented trade-offs
|
|
25
32
|
|
|
26
33
|
---
|
|
27
34
|
|
|
28
|
-
##
|
|
35
|
+
## ๐ง Architecture Overview
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Client
|
|
39
|
+
โ
|
|
40
|
+
JWT (sessionId only)
|
|
41
|
+
โ
|
|
42
|
+
L1 Cache (RAM, short TTL)
|
|
43
|
+
โ
|
|
44
|
+
L2 Store (Redis / SQL / Mongo)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **Hot path**: Served entirely from RAM
|
|
48
|
+
- **Cold path**: Falls back to database
|
|
49
|
+
- **Writes**: Throttled to avoid load amplification
|
|
50
|
+
|
|
51
|
+
Bounded inconsistency window: **โค cacheTTL (default: 2 seconds)**
|
|
29
52
|
|
|
30
|
-
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## ๐ฆ Installation
|
|
31
56
|
|
|
32
57
|
```bash
|
|
33
58
|
npm install ace-auth
|
|
@@ -37,186 +62,238 @@ npm install ace-auth
|
|
|
37
62
|
|
|
38
63
|
## ๐ Quick Start
|
|
39
64
|
|
|
40
|
-
### 1
|
|
41
|
-
|
|
42
|
-
AceAuth is database-agnostic. Below is a standard production setup using Redis:
|
|
65
|
+
### 1๏ธโฃ Initialize AceAuth
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
import { AceAuth, RedisStore } from 'ace-auth';
|
|
46
|
-
import { createClient } from 'redis';
|
|
67
|
+
AceAuth is storage-agnostic. You can plug in any supported database adapter.
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await redis.connect();
|
|
69
|
+
```ts
|
|
70
|
+
import { AceAuth } from 'ace-auth';
|
|
51
71
|
|
|
52
|
-
// 2. Initialize Auth Engine
|
|
53
72
|
const auth = new AceAuth({
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
host: 'smtp.example.com',
|
|
60
|
-
auth: { user: '...', pass: '...' }
|
|
61
|
-
}
|
|
73
|
+
secret: process.env.JWT_SECRET!,
|
|
74
|
+
store: yourStore,
|
|
75
|
+
sessionDuration: 30 * 24 * 60 * 60, // 30 days
|
|
76
|
+
tokenDuration: '15m',
|
|
77
|
+
cacheTTL: 2000
|
|
62
78
|
});
|
|
63
79
|
```
|
|
64
80
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Pass the request object (`req`) so AceAuth can fingerprint the device (IP/User-Agent).
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
import express from 'express';
|
|
71
|
-
const app = express();
|
|
81
|
+
---
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
// ... validate user credentials first ...
|
|
75
|
-
const userId = 'user_123';
|
|
83
|
+
## ๐ Authentication Flow
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
const { token, sessionId } = await auth.login({ id: userId, role: 'admin' }, req);
|
|
85
|
+
### Login
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
}
|
|
87
|
+
```ts
|
|
88
|
+
const { token, sessionId } = await auth.login(
|
|
89
|
+
{ id: user.id, role: 'user' },
|
|
90
|
+
req
|
|
91
|
+
);
|
|
82
92
|
```
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
- Creates a session in the database
|
|
95
|
+
- Stores session in L1 cache
|
|
96
|
+
- Issues a short-lived JWT (identifier only)
|
|
97
|
+
|
|
98
|
+
---
|
|
85
99
|
|
|
86
|
-
|
|
100
|
+
### Protect Routes (Middleware)
|
|
87
101
|
|
|
88
|
-
```
|
|
102
|
+
```ts
|
|
89
103
|
import { gatekeeper } from 'ace-auth/middleware';
|
|
90
104
|
|
|
91
|
-
app.get('/
|
|
92
|
-
|
|
93
|
-
res.json({ message: `Hello User ${req.user.id}` });
|
|
105
|
+
app.get('/profile', gatekeeper(auth), (req, res) => {
|
|
106
|
+
res.json({ user: req.user });
|
|
94
107
|
});
|
|
95
108
|
```
|
|
96
109
|
|
|
110
|
+
If a token expires but the session is valid, AceAuth **automatically rotates it** and returns a new token via:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
X-Ace-Token: <new-token>
|
|
114
|
+
```
|
|
115
|
+
|
|
97
116
|
---
|
|
98
117
|
|
|
99
|
-
## ๐ Database Adapters
|
|
118
|
+
## ๐ Database Adapters (Full Implementations)
|
|
119
|
+
|
|
120
|
+
AceAuth works with any persistent store implementing `IStore`.
|
|
121
|
+
|
|
122
|
+
---
|
|
100
123
|
|
|
101
|
-
|
|
124
|
+
## ๐ฅ Redis Adapter (Recommended)
|
|
102
125
|
|
|
103
|
-
###
|
|
126
|
+
### When to use
|
|
127
|
+
- High traffic APIs
|
|
128
|
+
- Real-time systems
|
|
129
|
+
- Horizontally scaled services
|
|
104
130
|
|
|
105
|
-
|
|
131
|
+
### Setup
|
|
106
132
|
|
|
107
|
-
```
|
|
133
|
+
```ts
|
|
134
|
+
import { createClient } from 'redis';
|
|
108
135
|
import { RedisStore } from 'ace-auth/adapters';
|
|
109
|
-
|
|
110
|
-
const
|
|
136
|
+
|
|
137
|
+
const redis = createClient();
|
|
138
|
+
await redis.connect();
|
|
139
|
+
|
|
140
|
+
const store = new RedisStore(redis);
|
|
111
141
|
```
|
|
112
142
|
|
|
113
|
-
###
|
|
143
|
+
### How it works
|
|
144
|
+
- Sessions stored as `sessionId โ payload`
|
|
145
|
+
- Secondary index: `userId โ set(sessionIds)`
|
|
146
|
+
- TTL enforced by Redis
|
|
147
|
+
- O(1) lookup for session revocation
|
|
148
|
+
|
|
149
|
+
---
|
|
114
150
|
|
|
115
|
-
|
|
151
|
+
## ๐ฆ PostgreSQL Adapter
|
|
116
152
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
153
|
+
### When to use
|
|
154
|
+
- Strong consistency requirements
|
|
155
|
+
- Existing SQL infrastructure
|
|
156
|
+
- Auditable session history
|
|
157
|
+
|
|
158
|
+
### Schema
|
|
159
|
+
|
|
160
|
+
```sql
|
|
161
|
+
CREATE TABLE auth_sessions (
|
|
162
|
+
sid TEXT PRIMARY KEY,
|
|
163
|
+
user_id TEXT NOT NULL,
|
|
164
|
+
sess JSONB NOT NULL,
|
|
165
|
+
expires_at TIMESTAMP NOT NULL
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
CREATE INDEX idx_auth_sessions_user
|
|
169
|
+
ON auth_sessions(user_id);
|
|
121
170
|
```
|
|
122
171
|
|
|
123
|
-
###
|
|
172
|
+
### Setup
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { Pool } from 'pg';
|
|
176
|
+
import { PostgresStore } from 'ace-auth/adapters';
|
|
124
177
|
|
|
125
|
-
|
|
178
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
126
179
|
|
|
127
|
-
|
|
128
|
-
import { MongoStore } from 'ace-auth/adapters';
|
|
129
|
-
// Requires 'mongoose' connection
|
|
130
|
-
const store = new MongoStore(mongoose.connection.collection('sessions'));
|
|
180
|
+
const store = new PostgresStore(pool, 'auth_sessions');
|
|
131
181
|
```
|
|
132
182
|
|
|
133
|
-
|
|
183
|
+
### Notes
|
|
184
|
+
- Expired sessions are lazily cleaned
|
|
185
|
+
- Indexed by `user_id` for fast logout-all
|
|
186
|
+
- Suitable for compliance-heavy systems
|
|
134
187
|
|
|
135
|
-
|
|
188
|
+
---
|
|
136
189
|
|
|
137
|
-
|
|
190
|
+
## ๐ฉ MongoDB Adapter
|
|
138
191
|
|
|
139
|
-
|
|
192
|
+
### When to use
|
|
193
|
+
- Document-based stacks
|
|
194
|
+
- Rapid prototyping
|
|
195
|
+
- Flexible schemas
|
|
140
196
|
|
|
141
|
-
|
|
142
|
-
// GET /api/devices
|
|
143
|
-
app.get('/api/devices', gatekeeper(auth), async (req, res) => {
|
|
144
|
-
const sessions = await auth.getActiveSessions(req.user.id);
|
|
145
|
-
res.json(sessions);
|
|
146
|
-
});
|
|
197
|
+
### Schema (Example)
|
|
147
198
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
199
|
+
```js
|
|
200
|
+
{
|
|
201
|
+
_id: sessionId,
|
|
202
|
+
userId: "user_123",
|
|
203
|
+
sess: { ... },
|
|
204
|
+
expiresAt: ISODate()
|
|
205
|
+
}
|
|
153
206
|
```
|
|
154
207
|
|
|
155
|
-
###
|
|
208
|
+
### Setup
|
|
156
209
|
|
|
157
|
-
|
|
210
|
+
```ts
|
|
211
|
+
import mongoose from 'mongoose';
|
|
212
|
+
import { MongoStore } from 'ace-auth/adapters';
|
|
158
213
|
|
|
159
|
-
|
|
160
|
-
// 1. Send Code
|
|
161
|
-
app.post('/auth/send-code', async (req, res) => {
|
|
162
|
-
await auth.sendOTP(req.body.email);
|
|
163
|
-
res.send('Code sent!');
|
|
164
|
-
});
|
|
214
|
+
await mongoose.connect(process.env.MONGO_URL);
|
|
165
215
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (valid) {
|
|
171
|
-
const { token } = await auth.login({ email: req.body.email }, req);
|
|
172
|
-
res.json({ token });
|
|
173
|
-
} else {
|
|
174
|
-
res.status(401).send('Invalid Code');
|
|
175
|
-
}
|
|
176
|
-
});
|
|
216
|
+
const store = new MongoStore(
|
|
217
|
+
mongoose.connection.collection('auth_sessions')
|
|
218
|
+
);
|
|
177
219
|
```
|
|
178
220
|
|
|
221
|
+
### Notes
|
|
222
|
+
- TTL index recommended on `expiresAt`
|
|
223
|
+
- Simple setup, no migrations required
|
|
224
|
+
|
|
179
225
|
---
|
|
180
226
|
|
|
181
|
-
##
|
|
227
|
+
## ๐ฑ Device & Session Management
|
|
182
228
|
|
|
183
|
-
|
|
229
|
+
```ts
|
|
230
|
+
// List active sessions
|
|
231
|
+
const sessions = await auth.getActiveSessions(userId);
|
|
232
|
+
|
|
233
|
+
// Logout everywhere
|
|
234
|
+
await auth.logoutAll(userId);
|
|
235
|
+
```
|
|
184
236
|
|
|
185
|
-
|
|
237
|
+
- Device info captured at login
|
|
238
|
+
- Bounded cache delay โค cacheTTL
|
|
239
|
+
- Redis/DB is always source of truth
|
|
186
240
|
|
|
187
|
-
|
|
188
|
-
- **AceAuth:** Middleware catches the expiry error, checks the database, and issues a new token if the session is still valid.
|
|
241
|
+
---
|
|
189
242
|
|
|
190
|
-
|
|
191
|
-
sequenceDiagram
|
|
192
|
-
participant Client
|
|
193
|
-
participant Middleware
|
|
194
|
-
participant Database
|
|
243
|
+
## ๐ง Passwordless OTP (Email)
|
|
195
244
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
Note right of Middleware: "Graceful Rescue" Triggered
|
|
201
|
-
|
|
202
|
-
Middleware->>Database: Check Session ID
|
|
203
|
-
Database-->>Middleware: Session Active (30 Days left)
|
|
204
|
-
|
|
205
|
-
Middleware->>Client: 200 OK + New Token (Header)
|
|
245
|
+
```ts
|
|
246
|
+
await auth.sendOTP(email);
|
|
247
|
+
await auth.verifyOTP(email, code);
|
|
206
248
|
```
|
|
207
249
|
|
|
250
|
+
- OTPs are single-use
|
|
251
|
+
- Auto-expire (10 minutes)
|
|
252
|
+
- Stored server-side only
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## ๐ Benchmarks
|
|
257
|
+
|
|
258
|
+
AceAuth is benchmarked against:
|
|
259
|
+
- Raw JWT
|
|
260
|
+
- Passport.js
|
|
261
|
+
- express-session
|
|
262
|
+
|
|
263
|
+
Results show AceAuth:
|
|
264
|
+
- Outperforms Passport.js on hot paths
|
|
265
|
+
- Retains server-side revocation
|
|
266
|
+
- Trades minimal latency for correctness
|
|
267
|
+
|
|
268
|
+
See **BENCHMARKS.md** for full data.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## ๐ Security Guarantees
|
|
273
|
+
|
|
274
|
+
- Server-side revocation
|
|
275
|
+
- Token rotation handled internally
|
|
276
|
+
- JWT payloads contain no user data
|
|
277
|
+
- Bounded cache staleness (explicit)
|
|
278
|
+
- Write throttling prevents DB overload
|
|
279
|
+
|
|
208
280
|
---
|
|
209
281
|
|
|
210
|
-
##
|
|
282
|
+
## โ When to Use AceAuth
|
|
211
283
|
|
|
212
|
-
|
|
284
|
+
Use AceAuth if you need:
|
|
285
|
+
- JWT-like scalability
|
|
286
|
+
- Immediate logout across devices
|
|
287
|
+
- Transparent refresh UX
|
|
288
|
+
- Measured, explainable behavior
|
|
213
289
|
|
|
214
|
-
|
|
215
|
-
-
|
|
216
|
-
-
|
|
290
|
+
Avoid if you want:
|
|
291
|
+
- Pure stateless JWT only
|
|
292
|
+
- Cookie-only sessions
|
|
293
|
+
- OAuth / SSO (out of scope)
|
|
217
294
|
|
|
218
295
|
---
|
|
219
296
|
|
|
220
297
|
## ๐ License
|
|
221
298
|
|
|
222
|
-
MIT
|
|
299
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { MemoryStore } from './adapters/MemoryStore';
|
|
|
4
4
|
export { MongoStore } from './adapters/MongoStore';
|
|
5
5
|
export { RedisStore } from './adapters/RedisStore';
|
|
6
6
|
export { PostgresStore } from './adapters/PostgressStore';
|
|
7
|
+
export { gatekeeper } from './middleware/gatekeeper';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// src/index.ts
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.PostgresStore = exports.RedisStore = exports.MongoStore = exports.MemoryStore = exports.AceAuth = void 0;
|
|
4
|
+
exports.gatekeeper = exports.PostgresStore = exports.RedisStore = exports.MongoStore = exports.MemoryStore = exports.AceAuth = void 0;
|
|
5
5
|
// Export Core
|
|
6
6
|
var AceAuth_1 = require("./core/AceAuth");
|
|
7
7
|
Object.defineProperty(exports, "AceAuth", { enumerable: true, get: function () { return AceAuth_1.AceAuth; } });
|
|
@@ -14,3 +14,5 @@ var RedisStore_1 = require("./adapters/RedisStore");
|
|
|
14
14
|
Object.defineProperty(exports, "RedisStore", { enumerable: true, get: function () { return RedisStore_1.RedisStore; } });
|
|
15
15
|
var PostgressStore_1 = require("./adapters/PostgressStore");
|
|
16
16
|
Object.defineProperty(exports, "PostgresStore", { enumerable: true, get: function () { return PostgressStore_1.PostgresStore; } });
|
|
17
|
+
var gatekeeper_1 = require("./middleware/gatekeeper");
|
|
18
|
+
Object.defineProperty(exports, "gatekeeper", { enumerable: true, get: function () { return gatekeeper_1.gatekeeper; } });
|