@juspay/neurolink 9.31.2 → 9.32.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/CHANGELOG.md +6 -0
- package/dist/auth/AuthProviderFactory.d.ts +71 -0
- package/dist/auth/AuthProviderFactory.js +111 -0
- package/dist/auth/AuthProviderRegistry.d.ts +33 -0
- package/dist/auth/AuthProviderRegistry.js +190 -0
- package/dist/auth/RequestContext.d.ts +23 -0
- package/dist/auth/RequestContext.js +78 -0
- package/dist/auth/authContext.d.ts +198 -0
- package/dist/auth/authContext.js +314 -0
- package/dist/auth/errors.d.ts +63 -0
- package/dist/auth/errors.js +39 -0
- package/dist/auth/index.d.ts +20 -8
- package/dist/auth/index.js +35 -7
- package/dist/auth/middleware/AuthMiddleware.d.ts +181 -0
- package/dist/auth/middleware/AuthMiddleware.js +519 -0
- package/dist/auth/middleware/rateLimitByUser.d.ts +282 -0
- package/dist/auth/middleware/rateLimitByUser.js +554 -0
- package/dist/auth/providers/BaseAuthProvider.d.ts +259 -0
- package/dist/auth/providers/BaseAuthProvider.js +723 -0
- package/dist/auth/providers/CognitoProvider.d.ts +61 -0
- package/dist/auth/providers/CognitoProvider.js +304 -0
- package/dist/auth/providers/KeycloakProvider.d.ts +61 -0
- package/dist/auth/providers/KeycloakProvider.js +393 -0
- package/dist/auth/providers/auth0.d.ts +59 -0
- package/dist/auth/providers/auth0.js +274 -0
- package/dist/auth/providers/betterAuth.d.ts +51 -0
- package/dist/auth/providers/betterAuth.js +182 -0
- package/dist/auth/providers/clerk.d.ts +65 -0
- package/dist/auth/providers/clerk.js +317 -0
- package/dist/auth/providers/custom.d.ts +64 -0
- package/dist/auth/providers/custom.js +112 -0
- package/dist/auth/providers/firebase.d.ts +63 -0
- package/dist/auth/providers/firebase.js +226 -0
- package/dist/auth/providers/jwt.d.ts +68 -0
- package/dist/auth/providers/jwt.js +212 -0
- package/dist/auth/providers/oauth2.d.ts +73 -0
- package/dist/auth/providers/oauth2.js +303 -0
- package/dist/auth/providers/supabase.d.ts +63 -0
- package/dist/auth/providers/supabase.js +259 -0
- package/dist/auth/providers/workos.d.ts +61 -0
- package/dist/auth/providers/workos.js +284 -0
- package/dist/auth/serverBridge.d.ts +14 -0
- package/dist/auth/serverBridge.js +25 -0
- package/dist/auth/sessionManager.d.ts +142 -0
- package/dist/auth/sessionManager.js +437 -0
- package/dist/cli/commands/authProviders.d.ts +43 -0
- package/dist/cli/commands/authProviders.js +399 -0
- package/dist/cli/factories/authCommandFactory.d.ts +23 -5
- package/dist/cli/factories/authCommandFactory.js +108 -5
- package/dist/cli/parser.js +1 -1
- package/dist/client/auth/AuthProviderFactory.js +111 -0
- package/dist/client/auth/AuthProviderRegistry.js +190 -0
- package/dist/client/auth/RequestContext.js +78 -0
- package/dist/client/auth/accountPool.js +178 -0
- package/dist/client/auth/authContext.js +314 -0
- package/dist/client/auth/errors.js +39 -0
- package/dist/client/auth/index.js +61 -0
- package/dist/client/auth/middleware/AuthMiddleware.js +519 -0
- package/dist/client/auth/middleware/rateLimitByUser.js +554 -0
- package/dist/client/auth/providers/BaseAuthProvider.js +723 -0
- package/dist/client/auth/providers/CognitoProvider.js +304 -0
- package/dist/client/auth/providers/KeycloakProvider.js +393 -0
- package/dist/client/auth/providers/auth0.js +274 -0
- package/dist/client/auth/providers/betterAuth.js +182 -0
- package/dist/client/auth/providers/clerk.js +317 -0
- package/dist/client/auth/providers/custom.js +112 -0
- package/dist/client/auth/providers/firebase.js +226 -0
- package/dist/client/auth/providers/jwt.js +212 -0
- package/dist/client/auth/providers/oauth2.js +303 -0
- package/dist/client/auth/providers/supabase.js +259 -0
- package/dist/client/auth/providers/workos.js +284 -0
- package/dist/client/auth/serverBridge.js +25 -0
- package/dist/client/auth/sessionManager.js +437 -0
- package/dist/client/core/infrastructure/baseRegistry.js +5 -1
- package/dist/client/index.js +25 -0
- package/dist/client/mcp/toolRegistry.js +11 -1
- package/dist/client/neurolink.js +218 -0
- package/dist/client/rag/ChunkerRegistry.js +2 -2
- package/dist/client/rag/metadata/MetadataExtractorRegistry.js +2 -2
- package/dist/client/rag/reranker/RerankerRegistry.js +2 -2
- package/dist/client/server/routes/agentRoutes.js +20 -2
- package/dist/client/types/authTypes.js +2 -1
- package/dist/core/infrastructure/baseRegistry.d.ts +3 -1
- package/dist/core/infrastructure/baseRegistry.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -0
- package/dist/lib/auth/AuthProviderFactory.d.ts +71 -0
- package/dist/lib/auth/AuthProviderFactory.js +112 -0
- package/dist/lib/auth/AuthProviderRegistry.d.ts +33 -0
- package/dist/lib/auth/AuthProviderRegistry.js +191 -0
- package/dist/lib/auth/RequestContext.d.ts +23 -0
- package/dist/lib/auth/RequestContext.js +79 -0
- package/dist/lib/auth/authContext.d.ts +198 -0
- package/dist/lib/auth/authContext.js +315 -0
- package/dist/lib/auth/errors.d.ts +63 -0
- package/dist/lib/auth/errors.js +40 -0
- package/dist/lib/auth/index.d.ts +20 -8
- package/dist/lib/auth/index.js +35 -7
- package/dist/lib/auth/middleware/AuthMiddleware.d.ts +181 -0
- package/dist/lib/auth/middleware/AuthMiddleware.js +520 -0
- package/dist/lib/auth/middleware/rateLimitByUser.d.ts +282 -0
- package/dist/lib/auth/middleware/rateLimitByUser.js +555 -0
- package/dist/lib/auth/providers/BaseAuthProvider.d.ts +259 -0
- package/dist/lib/auth/providers/BaseAuthProvider.js +724 -0
- package/dist/lib/auth/providers/CognitoProvider.d.ts +61 -0
- package/dist/lib/auth/providers/CognitoProvider.js +305 -0
- package/dist/lib/auth/providers/KeycloakProvider.d.ts +61 -0
- package/dist/lib/auth/providers/KeycloakProvider.js +394 -0
- package/dist/lib/auth/providers/auth0.d.ts +59 -0
- package/dist/lib/auth/providers/auth0.js +275 -0
- package/dist/lib/auth/providers/betterAuth.d.ts +51 -0
- package/dist/lib/auth/providers/betterAuth.js +183 -0
- package/dist/lib/auth/providers/clerk.d.ts +65 -0
- package/dist/lib/auth/providers/clerk.js +318 -0
- package/dist/lib/auth/providers/custom.d.ts +64 -0
- package/dist/lib/auth/providers/custom.js +113 -0
- package/dist/lib/auth/providers/firebase.d.ts +63 -0
- package/dist/lib/auth/providers/firebase.js +227 -0
- package/dist/lib/auth/providers/jwt.d.ts +68 -0
- package/dist/lib/auth/providers/jwt.js +213 -0
- package/dist/lib/auth/providers/oauth2.d.ts +73 -0
- package/dist/lib/auth/providers/oauth2.js +304 -0
- package/dist/lib/auth/providers/supabase.d.ts +63 -0
- package/dist/lib/auth/providers/supabase.js +260 -0
- package/dist/lib/auth/providers/workos.d.ts +61 -0
- package/dist/lib/auth/providers/workos.js +285 -0
- package/dist/lib/auth/serverBridge.d.ts +14 -0
- package/dist/lib/auth/serverBridge.js +26 -0
- package/dist/lib/auth/sessionManager.d.ts +142 -0
- package/dist/lib/auth/sessionManager.js +438 -0
- package/dist/lib/core/infrastructure/baseRegistry.d.ts +3 -1
- package/dist/lib/core/infrastructure/baseRegistry.js +5 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +25 -0
- package/dist/lib/mcp/toolRegistry.js +11 -1
- package/dist/lib/neurolink.d.ts +42 -1
- package/dist/lib/neurolink.js +218 -0
- package/dist/lib/rag/ChunkerRegistry.js +2 -2
- package/dist/lib/rag/metadata/MetadataExtractorRegistry.js +2 -2
- package/dist/lib/rag/reranker/RerankerRegistry.js +2 -2
- package/dist/lib/server/routes/agentRoutes.js +20 -2
- package/dist/lib/types/authTypes.d.ts +937 -1
- package/dist/lib/types/authTypes.js +2 -1
- package/dist/lib/types/configTypes.d.ts +46 -0
- package/dist/lib/types/generateTypes.d.ts +6 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/streamTypes.d.ts +6 -0
- package/dist/mcp/toolRegistry.js +11 -1
- package/dist/neurolink.d.ts +42 -1
- package/dist/neurolink.js +218 -0
- package/dist/rag/ChunkerRegistry.js +2 -2
- package/dist/rag/metadata/MetadataExtractorRegistry.js +2 -2
- package/dist/rag/reranker/RerankerRegistry.js +2 -2
- package/dist/server/routes/agentRoutes.js +20 -2
- package/dist/types/authTypes.d.ts +937 -1
- package/dist/types/authTypes.js +2 -1
- package/dist/types/configTypes.d.ts +46 -0
- package/dist/types/generateTypes.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/streamTypes.d.ts +6 -0
- package/package.json +2 -1
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseAuthProvider - Abstract base class for authentication providers
|
|
3
|
+
*
|
|
4
|
+
* Provides common functionality for all auth providers including:
|
|
5
|
+
* - Token extraction (header, cookie, query param, custom function)
|
|
6
|
+
* - Session management (create, validate, refresh, revoke)
|
|
7
|
+
* - RBAC authorization (roles, permissions, wildcards, hierarchy)
|
|
8
|
+
* - Token validation utilities (JWT parsing, expiry checks)
|
|
9
|
+
* - Event emission for auth lifecycle hooks
|
|
10
|
+
* - Error handling via unified AuthError factory
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "crypto";
|
|
13
|
+
import { EventEmitter } from "events";
|
|
14
|
+
import { logger } from "../../utils/logger.js";
|
|
15
|
+
import { AuthError } from "../errors.js";
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// BACKWARD-COMPAT RE-EXPORTS
|
|
18
|
+
// =============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Use `AuthError` from `../errors.js` instead.
|
|
21
|
+
* Kept for backward compatibility with CognitoProvider / KeycloakProvider.
|
|
22
|
+
*/
|
|
23
|
+
export const AuthProviderError = AuthError;
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// IN-MEMORY SESSION STORAGE
|
|
26
|
+
// =============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Default in-memory session storage
|
|
29
|
+
*/
|
|
30
|
+
export class InMemorySessionStorage {
|
|
31
|
+
sessions = new Map();
|
|
32
|
+
userSessions = new Map();
|
|
33
|
+
async get(sessionId) {
|
|
34
|
+
return this.sessions.get(sessionId) ?? null;
|
|
35
|
+
}
|
|
36
|
+
async save(session) {
|
|
37
|
+
this.sessions.set(session.id, session);
|
|
38
|
+
// Track sessions by user
|
|
39
|
+
const userSessionSet = this.userSessions.get(session.user.id) ?? new Set();
|
|
40
|
+
userSessionSet.add(session.id);
|
|
41
|
+
this.userSessions.set(session.user.id, userSessionSet);
|
|
42
|
+
}
|
|
43
|
+
async delete(sessionId) {
|
|
44
|
+
const session = this.sessions.get(sessionId);
|
|
45
|
+
if (session) {
|
|
46
|
+
this.sessions.delete(sessionId);
|
|
47
|
+
// Remove from user tracking
|
|
48
|
+
const userSessionSet = this.userSessions.get(session.user.id);
|
|
49
|
+
if (userSessionSet) {
|
|
50
|
+
userSessionSet.delete(sessionId);
|
|
51
|
+
if (userSessionSet.size === 0) {
|
|
52
|
+
this.userSessions.delete(session.user.id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async deleteAllForUser(userId) {
|
|
58
|
+
const userSessionSet = this.userSessions.get(userId);
|
|
59
|
+
if (userSessionSet) {
|
|
60
|
+
for (const sessionId of userSessionSet) {
|
|
61
|
+
this.sessions.delete(sessionId);
|
|
62
|
+
}
|
|
63
|
+
this.userSessions.delete(userId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async getForUser(userId) {
|
|
67
|
+
const userSessionSet = this.userSessions.get(userId);
|
|
68
|
+
if (!userSessionSet) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const sessions = [];
|
|
73
|
+
const expiredIds = [];
|
|
74
|
+
for (const sessionId of userSessionSet) {
|
|
75
|
+
const session = this.sessions.get(sessionId);
|
|
76
|
+
if (!session) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Filter out expired and revoked sessions so maxSessionsPerUser counts are accurate
|
|
80
|
+
if (session.expiresAt && session.expiresAt.getTime() < now) {
|
|
81
|
+
expiredIds.push(sessionId);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!session.isValid) {
|
|
85
|
+
expiredIds.push(sessionId);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
sessions.push(session);
|
|
89
|
+
}
|
|
90
|
+
// Clean up expired sessions lazily
|
|
91
|
+
for (const id of expiredIds) {
|
|
92
|
+
this.sessions.delete(id);
|
|
93
|
+
userSessionSet.delete(id);
|
|
94
|
+
}
|
|
95
|
+
if (userSessionSet.size === 0) {
|
|
96
|
+
this.userSessions.delete(userId);
|
|
97
|
+
}
|
|
98
|
+
return sessions;
|
|
99
|
+
}
|
|
100
|
+
async exists(sessionId) {
|
|
101
|
+
return this.sessions.has(sessionId);
|
|
102
|
+
}
|
|
103
|
+
async touch(sessionId) {
|
|
104
|
+
const session = this.sessions.get(sessionId);
|
|
105
|
+
if (session) {
|
|
106
|
+
session.lastActivityAt = new Date();
|
|
107
|
+
this.sessions.set(sessionId, session);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async clear() {
|
|
111
|
+
this.sessions.clear();
|
|
112
|
+
this.userSessions.clear();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get session count (for testing/monitoring)
|
|
116
|
+
*/
|
|
117
|
+
get size() {
|
|
118
|
+
return this.sessions.size;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// BASE PROVIDER IMPLEMENTATION
|
|
123
|
+
// =============================================================================
|
|
124
|
+
/**
|
|
125
|
+
* BaseAuthProvider - Abstract base class for all auth providers
|
|
126
|
+
*
|
|
127
|
+
* Subclasses must implement:
|
|
128
|
+
* - authenticateToken() - Validate and decode JWT/access tokens
|
|
129
|
+
*
|
|
130
|
+
* Optionally override:
|
|
131
|
+
* - getUser() - Fetch user by ID from provider
|
|
132
|
+
* - updateUserRoles() - Update user roles in provider
|
|
133
|
+
* - updateUserPermissions() - Update user permissions in provider
|
|
134
|
+
* - dispose() - Clean up resources
|
|
135
|
+
*/
|
|
136
|
+
export class BaseAuthProvider {
|
|
137
|
+
config;
|
|
138
|
+
sessionStorage;
|
|
139
|
+
sessionConfig;
|
|
140
|
+
rbacConfig;
|
|
141
|
+
emitter = new EventEmitter();
|
|
142
|
+
constructor(config) {
|
|
143
|
+
// Deep-merge tokenExtraction: preserve header defaults when partial config given
|
|
144
|
+
const defaultTokenExtraction = {
|
|
145
|
+
fromHeader: { name: "Authorization", scheme: "Bearer" },
|
|
146
|
+
};
|
|
147
|
+
this.config = {
|
|
148
|
+
required: true,
|
|
149
|
+
...config,
|
|
150
|
+
tokenExtraction: {
|
|
151
|
+
...defaultTokenExtraction,
|
|
152
|
+
...config.tokenExtraction,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
// Initialize session configuration
|
|
156
|
+
this.sessionConfig = {
|
|
157
|
+
storage: "memory",
|
|
158
|
+
duration: 3600, // 1 hour default
|
|
159
|
+
autoRefresh: true,
|
|
160
|
+
refreshThreshold: 300, // 5 minutes
|
|
161
|
+
allowMultipleSessions: true,
|
|
162
|
+
maxSessionsPerUser: 10,
|
|
163
|
+
prefix: "neurolink:session:",
|
|
164
|
+
...config.session,
|
|
165
|
+
};
|
|
166
|
+
// Initialize RBAC configuration
|
|
167
|
+
this.rbacConfig = {
|
|
168
|
+
enabled: true,
|
|
169
|
+
defaultRoles: [],
|
|
170
|
+
roleHierarchy: {},
|
|
171
|
+
rolePermissions: {},
|
|
172
|
+
superAdminRoles: ["super_admin", "root"],
|
|
173
|
+
...config.rbac,
|
|
174
|
+
};
|
|
175
|
+
// Initialize session storage
|
|
176
|
+
this.sessionStorage =
|
|
177
|
+
config.session?.customStorage ?? new InMemorySessionStorage();
|
|
178
|
+
logger.debug(`[BaseAuthProvider] Initialized`);
|
|
179
|
+
}
|
|
180
|
+
// ===========================================================================
|
|
181
|
+
// TOKEN EXTRACTION
|
|
182
|
+
// ===========================================================================
|
|
183
|
+
/**
|
|
184
|
+
* Extract token using configured strategy
|
|
185
|
+
*
|
|
186
|
+
* Attempts extraction in order:
|
|
187
|
+
* 1. Header (Authorization: Bearer <token> by default)
|
|
188
|
+
* 2. Cookie
|
|
189
|
+
* 3. Query parameter
|
|
190
|
+
* 4. Custom function
|
|
191
|
+
*
|
|
192
|
+
* @param context - Request context containing headers, cookies, etc.
|
|
193
|
+
* @returns Extracted token or null if not found
|
|
194
|
+
*/
|
|
195
|
+
async extractToken(context) {
|
|
196
|
+
const strategy = this.config.tokenExtraction;
|
|
197
|
+
// Try header extraction (case-insensitive header lookup)
|
|
198
|
+
if (strategy?.fromHeader) {
|
|
199
|
+
const headerName = strategy.fromHeader.name.toLowerCase();
|
|
200
|
+
// Find header value with case-insensitive lookup
|
|
201
|
+
let headerValue;
|
|
202
|
+
for (const [key, value] of Object.entries(context.headers)) {
|
|
203
|
+
if (key.toLowerCase() === headerName && typeof value === "string") {
|
|
204
|
+
headerValue = value;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (typeof headerValue === "string") {
|
|
209
|
+
if (strategy.fromHeader.scheme) {
|
|
210
|
+
const prefix = `${strategy.fromHeader.scheme} `;
|
|
211
|
+
if (headerValue.startsWith(prefix)) {
|
|
212
|
+
return headerValue.slice(prefix.length);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
return headerValue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Try cookie extraction
|
|
221
|
+
if (strategy?.fromCookie && context.cookies) {
|
|
222
|
+
const cookieValue = context.cookies[strategy.fromCookie.name];
|
|
223
|
+
if (cookieValue) {
|
|
224
|
+
return cookieValue;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Try query parameter extraction
|
|
228
|
+
if (strategy?.fromQuery && context.path) {
|
|
229
|
+
try {
|
|
230
|
+
const url = new URL(context.path, "http://localhost");
|
|
231
|
+
const queryValue = url.searchParams.get(strategy.fromQuery.name);
|
|
232
|
+
if (queryValue) {
|
|
233
|
+
return queryValue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Invalid URL, skip query extraction
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Try custom extraction (may be sync or async)
|
|
241
|
+
if (strategy?.custom) {
|
|
242
|
+
return await Promise.resolve(strategy.custom(context));
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
// ===========================================================================
|
|
247
|
+
// SESSION MANAGEMENT
|
|
248
|
+
// ===========================================================================
|
|
249
|
+
/**
|
|
250
|
+
* Create a new session for an authenticated user
|
|
251
|
+
*
|
|
252
|
+
* Session duration and metadata are derived from `this.sessionConfig` and
|
|
253
|
+
* the optional `context`. This matches the `AuthSessionManager` type
|
|
254
|
+
* signature: `createSession(user, context?)`.
|
|
255
|
+
*/
|
|
256
|
+
async createSession(user, context) {
|
|
257
|
+
const now = new Date();
|
|
258
|
+
const duration = this.sessionConfig.duration ?? 3600;
|
|
259
|
+
// Check session limits
|
|
260
|
+
if (!this.sessionConfig.allowMultipleSessions) {
|
|
261
|
+
await this.revokeAllSessions(user.id);
|
|
262
|
+
}
|
|
263
|
+
else if (this.sessionConfig.maxSessionsPerUser) {
|
|
264
|
+
const existingSessions = await this.sessionStorage.getForUser(user.id);
|
|
265
|
+
if (existingSessions.length >= this.sessionConfig.maxSessionsPerUser) {
|
|
266
|
+
// Remove oldest session
|
|
267
|
+
const oldestSession = existingSessions.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())[0];
|
|
268
|
+
if (oldestSession) {
|
|
269
|
+
await this.sessionStorage.delete(oldestSession.id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const session = {
|
|
274
|
+
id: randomUUID(),
|
|
275
|
+
user,
|
|
276
|
+
accessToken: randomUUID(), // Internal session token
|
|
277
|
+
isValid: true,
|
|
278
|
+
expiresAt: new Date(now.getTime() + duration * 1000),
|
|
279
|
+
createdAt: now,
|
|
280
|
+
lastActivityAt: now,
|
|
281
|
+
ipAddress: context?.ip ?? context?.ipAddress,
|
|
282
|
+
userAgent: context?.userAgent,
|
|
283
|
+
};
|
|
284
|
+
await this.sessionStorage.save(session);
|
|
285
|
+
logger.debug(`[BaseAuthProvider] Created session ${session.id} for user ${user.id}`);
|
|
286
|
+
return session;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Validate an existing session
|
|
290
|
+
*/
|
|
291
|
+
async validateSession(sessionId) {
|
|
292
|
+
const session = await this.sessionStorage.get(sessionId);
|
|
293
|
+
if (!session) {
|
|
294
|
+
return {
|
|
295
|
+
valid: false,
|
|
296
|
+
error: "Session not found",
|
|
297
|
+
errorCode: "AUTH-010",
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// Check expiration
|
|
301
|
+
if (session.expiresAt && session.expiresAt.getTime() < Date.now()) {
|
|
302
|
+
await this.sessionStorage.delete(sessionId);
|
|
303
|
+
return {
|
|
304
|
+
valid: false,
|
|
305
|
+
error: "Session expired",
|
|
306
|
+
errorCode: "AUTH-011",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// Check if revoked
|
|
310
|
+
if (!session.isValid) {
|
|
311
|
+
return {
|
|
312
|
+
valid: false,
|
|
313
|
+
error: "Session revoked",
|
|
314
|
+
errorCode: "AUTH-012",
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// Auto-refresh if near expiration
|
|
318
|
+
let refreshed = false;
|
|
319
|
+
if (this.sessionConfig.autoRefresh &&
|
|
320
|
+
this.sessionConfig.refreshThreshold &&
|
|
321
|
+
session.expiresAt &&
|
|
322
|
+
session.expiresAt.getTime() - Date.now() <
|
|
323
|
+
this.sessionConfig.refreshThreshold * 1000) {
|
|
324
|
+
const refreshedSession = await this.refreshSession(sessionId);
|
|
325
|
+
refreshed = true;
|
|
326
|
+
return {
|
|
327
|
+
valid: true,
|
|
328
|
+
session: refreshedSession ?? undefined,
|
|
329
|
+
refreshed,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
// Update last activity
|
|
333
|
+
await this.sessionStorage.touch(sessionId);
|
|
334
|
+
return {
|
|
335
|
+
valid: true,
|
|
336
|
+
session,
|
|
337
|
+
refreshed,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Refresh a session (extend expiration)
|
|
342
|
+
*/
|
|
343
|
+
async refreshSession(sessionId) {
|
|
344
|
+
const session = await this.sessionStorage.get(sessionId);
|
|
345
|
+
if (!session) {
|
|
346
|
+
throw AuthError.create("SESSION_NOT_FOUND", `Session not found: ${sessionId}`, { details: { sessionId } });
|
|
347
|
+
}
|
|
348
|
+
// Don't refresh revoked sessions
|
|
349
|
+
if (!session.isValid) {
|
|
350
|
+
throw AuthError.create("SESSION_REVOKED", `Cannot refresh revoked session: ${sessionId}`, { details: { sessionId } });
|
|
351
|
+
}
|
|
352
|
+
// Don't refresh expired sessions
|
|
353
|
+
if (session.expiresAt && session.expiresAt.getTime() < Date.now()) {
|
|
354
|
+
await this.sessionStorage.delete(sessionId);
|
|
355
|
+
throw AuthError.create("SESSION_EXPIRED", `Cannot refresh expired session: ${sessionId}`, { details: { sessionId } });
|
|
356
|
+
}
|
|
357
|
+
const duration = this.sessionConfig.duration ?? 3600;
|
|
358
|
+
session.expiresAt = new Date(Date.now() + duration * 1000);
|
|
359
|
+
session.lastActivityAt = new Date();
|
|
360
|
+
await this.sessionStorage.save(session);
|
|
361
|
+
logger.debug(`[BaseAuthProvider] Refreshed session ${sessionId}`);
|
|
362
|
+
return session;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Revoke a session
|
|
366
|
+
*
|
|
367
|
+
* Marks the session as invalid rather than deleting it immediately.
|
|
368
|
+
* This keeps a tombstone so that "revoked" is distinguishable from
|
|
369
|
+
* "not found" during subsequent validation attempts.
|
|
370
|
+
*/
|
|
371
|
+
async revokeSession(sessionId) {
|
|
372
|
+
const session = await this.sessionStorage.get(sessionId);
|
|
373
|
+
if (session) {
|
|
374
|
+
session.isValid = false;
|
|
375
|
+
await this.sessionStorage.save(session);
|
|
376
|
+
logger.debug(`[BaseAuthProvider] Revoked session ${sessionId}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Revoke all sessions for a user
|
|
381
|
+
*/
|
|
382
|
+
async revokeAllSessions(userId) {
|
|
383
|
+
await this.sessionStorage.deleteAllForUser(userId);
|
|
384
|
+
logger.debug(`[BaseAuthProvider] Revoked all sessions for user ${userId}`);
|
|
385
|
+
}
|
|
386
|
+
// ===========================================================================
|
|
387
|
+
// AUTHORIZATION (RBAC)
|
|
388
|
+
// ===========================================================================
|
|
389
|
+
/**
|
|
390
|
+
* Check if a user is authorized for specific roles/permissions
|
|
391
|
+
*/
|
|
392
|
+
async authorize(user, options) {
|
|
393
|
+
// Check if RBAC is enabled
|
|
394
|
+
if (!this.rbacConfig.enabled) {
|
|
395
|
+
return { authorized: true, user };
|
|
396
|
+
}
|
|
397
|
+
// Super admin bypass
|
|
398
|
+
if (this.isSuperAdmin(user)) {
|
|
399
|
+
return { authorized: true, user };
|
|
400
|
+
}
|
|
401
|
+
const result = {
|
|
402
|
+
authorized: true,
|
|
403
|
+
user,
|
|
404
|
+
requiredRoles: options.roles,
|
|
405
|
+
requiredPermissions: options.permissions,
|
|
406
|
+
missingRoles: [],
|
|
407
|
+
missingPermissions: [],
|
|
408
|
+
};
|
|
409
|
+
// Check roles
|
|
410
|
+
if (options.roles && options.roles.length > 0) {
|
|
411
|
+
const userRoles = this.getEffectiveRoles(user);
|
|
412
|
+
const missingRoles = options.roles.filter((r) => !userRoles.has(r));
|
|
413
|
+
if (options.requireAllRoles) {
|
|
414
|
+
// All roles required
|
|
415
|
+
if (missingRoles.length > 0) {
|
|
416
|
+
result.authorized = false;
|
|
417
|
+
result.missingRoles = missingRoles;
|
|
418
|
+
result.reason = `Missing required roles: ${missingRoles.join(", ")}`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
// Any role is sufficient
|
|
423
|
+
const hasAnyRole = options.roles.some((r) => userRoles.has(r));
|
|
424
|
+
if (!hasAnyRole) {
|
|
425
|
+
result.authorized = false;
|
|
426
|
+
result.missingRoles = options.roles;
|
|
427
|
+
result.reason = `Missing any of required roles: ${options.roles.join(", ")}`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Check permissions (all required)
|
|
432
|
+
if (options.permissions && options.permissions.length > 0) {
|
|
433
|
+
const userPermissions = this.getEffectivePermissions(user);
|
|
434
|
+
const missingPermissions = options.permissions.filter((p) => !this.hasPermission(userPermissions, p));
|
|
435
|
+
if (missingPermissions.length > 0) {
|
|
436
|
+
result.authorized = false;
|
|
437
|
+
result.missingPermissions = missingPermissions;
|
|
438
|
+
result.reason = result.reason
|
|
439
|
+
? `${result.reason}; Missing permissions: ${missingPermissions.join(", ")}`
|
|
440
|
+
: `Missing required permissions: ${missingPermissions.join(", ")}`;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Check if user is a super admin
|
|
447
|
+
*/
|
|
448
|
+
isSuperAdmin(user) {
|
|
449
|
+
const superAdminRoles = this.rbacConfig.superAdminRoles ?? [];
|
|
450
|
+
return user.roles.some((r) => superAdminRoles.includes(r));
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Get effective roles including inherited roles from hierarchy (transitive)
|
|
454
|
+
*/
|
|
455
|
+
getEffectiveRoles(user) {
|
|
456
|
+
const effectiveRoles = new Set(user.roles);
|
|
457
|
+
// Transitive closure: keep expanding until no new roles are added
|
|
458
|
+
const hierarchy = this.rbacConfig.roleHierarchy ?? {};
|
|
459
|
+
let added = true;
|
|
460
|
+
while (added) {
|
|
461
|
+
added = false;
|
|
462
|
+
for (const role of effectiveRoles) {
|
|
463
|
+
const inheritedRoles = hierarchy[role] ?? [];
|
|
464
|
+
for (const inherited of inheritedRoles) {
|
|
465
|
+
if (!effectiveRoles.has(inherited)) {
|
|
466
|
+
effectiveRoles.add(inherited);
|
|
467
|
+
added = true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return effectiveRoles;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get effective permissions including role-based permissions
|
|
476
|
+
*/
|
|
477
|
+
getEffectivePermissions(user) {
|
|
478
|
+
const effectivePermissions = new Set(user.permissions);
|
|
479
|
+
// Add permissions from roles
|
|
480
|
+
const rolePermissions = this.rbacConfig.rolePermissions ?? {};
|
|
481
|
+
const effectiveRoles = this.getEffectiveRoles(user);
|
|
482
|
+
for (const role of effectiveRoles) {
|
|
483
|
+
const permissions = rolePermissions[role] ?? [];
|
|
484
|
+
for (const permission of permissions) {
|
|
485
|
+
effectivePermissions.add(permission);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return effectivePermissions;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Check if a permission set grants a given permission.
|
|
492
|
+
* Supports exact match, global wildcard ("*"), and hierarchical wildcards
|
|
493
|
+
* (e.g. "tools:*" grants "tools:execute").
|
|
494
|
+
*/
|
|
495
|
+
hasPermission(permissions, required) {
|
|
496
|
+
if (permissions.has(required)) {
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
if (permissions.has("*")) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
const parts = required.split(":");
|
|
503
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
504
|
+
const wildcard = [...parts.slice(0, i), "*"].join(":");
|
|
505
|
+
if (permissions.has(wildcard)) {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
// ===========================================================================
|
|
512
|
+
// UTILITY METHODS
|
|
513
|
+
// ===========================================================================
|
|
514
|
+
/**
|
|
515
|
+
* Parse JWT token (without validation)
|
|
516
|
+
*/
|
|
517
|
+
parseJWT(token) {
|
|
518
|
+
try {
|
|
519
|
+
const parts = token.split(".");
|
|
520
|
+
if (parts.length !== 3) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const payload = parts[1];
|
|
524
|
+
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
|
|
525
|
+
return JSON.parse(decoded);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Check if token is expired
|
|
533
|
+
*/
|
|
534
|
+
isTokenExpired(claims, clockTolerance = 0) {
|
|
535
|
+
if (!claims.exp) {
|
|
536
|
+
return false; // No expiration claim
|
|
537
|
+
}
|
|
538
|
+
const now = Math.floor(Date.now() / 1000);
|
|
539
|
+
return claims.exp + clockTolerance < now;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Check if token is not yet valid
|
|
543
|
+
*/
|
|
544
|
+
isTokenNotYetValid(claims, clockTolerance = 0) {
|
|
545
|
+
if (!claims.nbf) {
|
|
546
|
+
return false; // No nbf claim
|
|
547
|
+
}
|
|
548
|
+
const now = Math.floor(Date.now() / 1000);
|
|
549
|
+
return claims.nbf - clockTolerance > now;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Extract user from token claims
|
|
553
|
+
*/
|
|
554
|
+
extractUserFromClaims(claims, options) {
|
|
555
|
+
const rolesKey = options?.rolesClaimKey ?? "roles";
|
|
556
|
+
const permissionsKey = options?.permissionsClaimKey ?? "permissions";
|
|
557
|
+
const idKey = options?.idClaimKey ?? "sub";
|
|
558
|
+
const roles = Array.isArray(claims[rolesKey])
|
|
559
|
+
? claims[rolesKey]
|
|
560
|
+
: (this.rbacConfig.defaultRoles ?? []);
|
|
561
|
+
const permissions = Array.isArray(claims[permissionsKey])
|
|
562
|
+
? claims[permissionsKey]
|
|
563
|
+
: [];
|
|
564
|
+
return {
|
|
565
|
+
id: claims[idKey] ?? "",
|
|
566
|
+
email: claims.email,
|
|
567
|
+
name: claims.name,
|
|
568
|
+
picture: claims.picture,
|
|
569
|
+
roles,
|
|
570
|
+
permissions,
|
|
571
|
+
emailVerified: claims.email_verified,
|
|
572
|
+
providerData: claims,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
// ===========================================================================
|
|
576
|
+
// OPTIONAL METHODS (can be overridden by subclasses)
|
|
577
|
+
// ===========================================================================
|
|
578
|
+
/**
|
|
579
|
+
* Get user by ID
|
|
580
|
+
* Override in subclass if provider supports user lookup
|
|
581
|
+
*/
|
|
582
|
+
async getUser(_userId) {
|
|
583
|
+
logger.debug(`[BaseAuthProvider] getUser not implemented for ${this.type}`);
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Update user roles
|
|
588
|
+
* Override in subclass if provider supports role updates.
|
|
589
|
+
* Returns the user with updated roles.
|
|
590
|
+
*/
|
|
591
|
+
async updateUserRoles(_userId, _roles) {
|
|
592
|
+
throw AuthError.create("PROVIDER_ERROR", `updateUserRoles not supported by ${this.type} provider`);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Update user permissions
|
|
596
|
+
* Override in subclass if provider supports permission updates.
|
|
597
|
+
* Returns the user with updated permissions.
|
|
598
|
+
*/
|
|
599
|
+
async updateUserPermissions(_userId, _permissions) {
|
|
600
|
+
throw AuthError.create("PROVIDER_ERROR", `updateUserPermissions not supported by ${this.type} provider`);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Clean up resources
|
|
604
|
+
*/
|
|
605
|
+
async dispose() {
|
|
606
|
+
await this.sessionStorage.clear();
|
|
607
|
+
logger.debug(`[BaseAuthProvider] Disposed ${this.type} provider`);
|
|
608
|
+
}
|
|
609
|
+
// ===========================================================================
|
|
610
|
+
// METHODS FROM MastraAuthProvider INTERFACE
|
|
611
|
+
// ===========================================================================
|
|
612
|
+
/**
|
|
613
|
+
* Check if a user is authorized to perform an action
|
|
614
|
+
*/
|
|
615
|
+
async authorizeUser(user, permission) {
|
|
616
|
+
return this.authorize(user, { permissions: [permission] });
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Check if user has specific roles
|
|
620
|
+
*/
|
|
621
|
+
async authorizeRoles(user, roles) {
|
|
622
|
+
return this.authorize(user, { roles });
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Check if user has all specified permissions
|
|
626
|
+
*/
|
|
627
|
+
async authorizePermissions(user, permissions) {
|
|
628
|
+
return this.authorize(user, { permissions });
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Get an existing session by ID
|
|
632
|
+
*/
|
|
633
|
+
async getSession(sessionId) {
|
|
634
|
+
return this.sessionStorage.get(sessionId);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Invalidate/destroy a session
|
|
638
|
+
*/
|
|
639
|
+
async destroySession(sessionId) {
|
|
640
|
+
await this.revokeSession(sessionId);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Get all active sessions for a user
|
|
644
|
+
*/
|
|
645
|
+
async getUserSessions(userId) {
|
|
646
|
+
return this.sessionStorage.getForUser(userId);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Invalidate all sessions for a user (global logout)
|
|
650
|
+
*/
|
|
651
|
+
async destroyAllUserSessions(userId) {
|
|
652
|
+
await this.revokeAllSessions(userId);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Full request authentication flow
|
|
656
|
+
*
|
|
657
|
+
* Combines token extraction (with full strategy support), validation,
|
|
658
|
+
* and session creation/reuse.
|
|
659
|
+
*
|
|
660
|
+
* @param context - Request context
|
|
661
|
+
* @returns Authenticated context with user and session, or null
|
|
662
|
+
*/
|
|
663
|
+
async authenticateRequest(context) {
|
|
664
|
+
// Extract token (async to support custom extractors)
|
|
665
|
+
const token = await this.extractToken(context);
|
|
666
|
+
if (!token) {
|
|
667
|
+
if (!this.config.required) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
this.emitter.emit("auth:unauthorized", context, "No token provided");
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
// Validate token
|
|
674
|
+
const validation = await this.authenticateToken(token, context);
|
|
675
|
+
if (!validation.valid || !validation.user) {
|
|
676
|
+
this.emitter.emit("auth:unauthorized", context, validation.error ?? "Invalid token");
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
// Reuse existing session if one exists for this user
|
|
680
|
+
const existingSessions = await this.getUserSessions(validation.user.id);
|
|
681
|
+
const validSession = existingSessions.find((s) => s.isValid && (!s.expiresAt || s.expiresAt.getTime() > Date.now()));
|
|
682
|
+
const session = validSession ?? (await this.createSession(validation.user, context));
|
|
683
|
+
return {
|
|
684
|
+
...context,
|
|
685
|
+
user: validation.user,
|
|
686
|
+
session,
|
|
687
|
+
request: context,
|
|
688
|
+
authenticatedAt: new Date(),
|
|
689
|
+
provider: this.type,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Check provider health
|
|
694
|
+
*/
|
|
695
|
+
async healthCheck() {
|
|
696
|
+
return {
|
|
697
|
+
healthy: true,
|
|
698
|
+
providerConnected: true,
|
|
699
|
+
sessionStorageHealthy: true,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
// ===========================================================================
|
|
703
|
+
// EVENT HELPERS
|
|
704
|
+
// ===========================================================================
|
|
705
|
+
/**
|
|
706
|
+
* Subscribe to auth events
|
|
707
|
+
*/
|
|
708
|
+
on(event, listener) {
|
|
709
|
+
this.emitter.on(event, listener);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Unsubscribe from auth events
|
|
713
|
+
*/
|
|
714
|
+
off(event, listener) {
|
|
715
|
+
this.emitter.off(event, listener);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Emit an auth event
|
|
719
|
+
*/
|
|
720
|
+
emit(event, ...args) {
|
|
721
|
+
this.emitter.emit(event, ...args);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
//# sourceMappingURL=BaseAuthProvider.js.map
|