@rodit/rodit-auth-be 9.11.14

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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Rate limiting middleware
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ /**
7
+ * Rate limiting middleware for API protection
8
+ * Updated to work with express-rate-limit v7.x
9
+ */
10
+ const rateLimit = require('express-rate-limit');
11
+ const logger = require('../../services/logger');
12
+ const { createLogContext, logErrorWithMetrics } = require('../../services/logger');
13
+ const { ulid } = require('ulid');
14
+ const { sendError } = require('../../services/error-response');
15
+
16
+ /**
17
+ * Creates a rate limiting middleware with the specified configuration
18
+ *
19
+ * @param {number} maxRequests - Maximum number of requests allowed per window
20
+ * @param {number} windowMinutes - Time window in minutes for rate limiting
21
+ * @returns {Function} - Express middleware function
22
+ */
23
+ function ratelimitmw(maxRequests = 100, windowMinutes = 15) {
24
+ const requestId = ulid();
25
+ const startTime = Date.now();
26
+
27
+ const baseContext = createLogContext(
28
+ "RateLimitMiddleware",
29
+ "ratelimitmw",
30
+ {
31
+ requestId,
32
+ maxRequests,
33
+ windowMinutes
34
+ }
35
+ );
36
+
37
+ logger.infoWithContext('Rate limiting middleware initialized', {
38
+ ...baseContext,
39
+ result: 'call',
40
+ reason: 'Rate limiting middleware initialized'
41
+ }); // Function call log
42
+
43
+ try {
44
+ const limiter = rateLimit({
45
+ // Define window in milliseconds (converting from minutes)
46
+ windowMs: windowMinutes * 60 * 1000,
47
+
48
+ // Maximum number of requests per window
49
+ max: maxRequests,
50
+
51
+ // Return rate limit info in the headers
52
+ standardHeaders: true,
53
+
54
+ // Disable X-RateLimit-* headers
55
+ legacyHeaders: false,
56
+
57
+ // Handler for when the rate limit is exceeded
58
+ handler: (req, res, next, handleroptions) => {
59
+ const exceedRequestId = ulid();
60
+
61
+ const exceedContext = createLogContext(
62
+ "RateLimitMiddleware",
63
+ "rateLimitExceeded",
64
+ {
65
+ requestId: exceedRequestId,
66
+ ip: req.ip,
67
+ path: req.path,
68
+ method: req.method,
69
+ userId: req.user ? req.user.id : 'anonymous',
70
+ maxRequests: handleroptions.max,
71
+ windowMinutes: handleroptions.windowMs / (60 * 1000)
72
+ }
73
+ );
74
+
75
+ // Log rate limit exceeded events
76
+ logger.warnWithContext('Rate limit exceeded', {
77
+ ...exceedContext,
78
+ result: 'failure',
79
+ reason: 'Rate limit exceeded'
80
+ });
81
+ // Add metric for rate limit exceeded
82
+ logger.metric("rate_limit_operations", 0, {
83
+ operation: "limit_exceeded",
84
+ path: req.path,
85
+ method: req.method,
86
+ result: "blocked",
87
+ reason: 'Rate limit exceeded'
88
+ });
89
+
90
+ // Send error response
91
+ sendError(res, {
92
+ statusCode: handleroptions.statusCode,
93
+ requestId: exceedRequestId,
94
+ code: 'RATE_LIMIT_EXCEEDED',
95
+ message: handleroptions.message,
96
+ details: {
97
+ maxRequests: handleroptions.max,
98
+ windowMinutes: handleroptions.windowMs / (60 * 1000)
99
+ }
100
+ });
101
+ },
102
+
103
+ // Skip rate limiting for certain requests (optional)
104
+ skip: (req, res) => {
105
+ const skipRequestId = ulid();
106
+
107
+ const skipContext = createLogContext(
108
+ "RateLimitMiddleware",
109
+ "skipRateLimit",
110
+ {
111
+ requestId: skipRequestId,
112
+ path: req.path,
113
+ method: req.method
114
+ }
115
+ );
116
+
117
+ // Example: Skip rate limiting for health check endpoints
118
+ const shouldSkip = req.path === '/api/health' || req.path === '/metrics';
119
+
120
+ if (shouldSkip) {
121
+ logger.debugWithContext('Skipping rate limit for endpoint', skipContext);
122
+
123
+ // Add metric for skipped rate limiting
124
+ logger.metric("rate_limit_operations", 0, {
125
+ operation: "skip",
126
+ path: req.path,
127
+ method: req.method,
128
+ result: "skipped"
129
+ });
130
+ }
131
+
132
+ return shouldSkip;
133
+ }
134
+ });
135
+
136
+ const duration = Date.now() - startTime;
137
+ logger.infoWithContext('Rate limiting middleware created successfully', {
138
+ ...baseContext,
139
+ duration
140
+ });
141
+
142
+ // Add metric for middleware creation
143
+ logger.metric("rate_limit_operations", duration, {
144
+ operation: "create",
145
+ result: "success"
146
+ });
147
+
148
+ return limiter;
149
+ } catch (error) {
150
+ const duration = Date.now() - startTime;
151
+
152
+ logErrorWithMetrics(
153
+ "Failed to create rate limiting middleware",
154
+ {
155
+ ...baseContext,
156
+ duration
157
+ },
158
+ error,
159
+ "rate_limit_error",
160
+ {
161
+ operation: "create",
162
+ result: "error",
163
+ duration
164
+ }
165
+ );
166
+
167
+ throw error;
168
+ }
169
+ }
170
+
171
+ module.exports = ratelimitmw;
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Permission validation middleware
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ const logger = require("../../services/logger");
7
+ const { createLogContext, logErrorWithMetrics } = logger;
8
+ const { sendError } = require("../../services/error-response");
9
+ const crypto = require("crypto");
10
+ const { ulid } = require("ulid");
11
+ const config = require('../../services/configsdk');
12
+
13
+ // Dynamic import for ESM 'jose' in CommonJS context
14
+ let _josePromise;
15
+ async function getJose() {
16
+ if (!_josePromise) {
17
+ _josePromise = import("jose");
18
+ }
19
+ return _josePromise;
20
+ }
21
+
22
+ class PermissionValidator {
23
+ constructor() {
24
+ // Load method permission map from config or use default if not available
25
+ this.methodPermissionMap = config.get('METHOD_PERMISSION_MAP');
26
+ }
27
+
28
+ /**
29
+ * Parse the rate value from the permission string
30
+ *
31
+ * @param {string} rateValue - The permission value from the token (e.g., "+0")
32
+ * @returns {Object} An object containing the rate limit and whether it's unlimited
33
+ * { limit: number|null, unlimited: boolean }
34
+ */
35
+ parseRateLimit(rateValue) {
36
+ // Skip if no value or not a string
37
+ if (!rateValue || typeof rateValue !== 'string') {
38
+ return { limit: null, unlimited: false };
39
+ }
40
+
41
+ // Remove the permission prefix (+ or -)
42
+ let rateString = rateValue;
43
+ if (rateString.startsWith('+') || rateString.startsWith('-')) {
44
+ rateString = rateString.substring(1);
45
+ }
46
+
47
+ // Try to parse the rate as a number (could be in scientific notation)
48
+ try {
49
+ const rate = parseFloat(rateString);
50
+
51
+ // Check if it's a valid number
52
+ if (isNaN(rate)) {
53
+ return { limit: null, unlimited: false };
54
+ }
55
+
56
+ // Special case: 0 indicates no limits
57
+ if (rate === 0) {
58
+ return { limit: null, unlimited: true };
59
+ }
60
+
61
+ // Return the actual rate limit
62
+ return { limit: rate, unlimited: false };
63
+ } catch (e) {
64
+ return { limit: null, unlimited: false };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Determine the permission scope from the rate value prefix
70
+ *
71
+ * @param {string} rateValue - The permission value from the token (e.g., "+0")
72
+ * @returns {string} The permission scope (entityAndProperties, propertiesOnly, or entityOnly)
73
+ */
74
+ getPermissionScope(rateValue) {
75
+ if (rateValue.startsWith("+")) {
76
+ return "entityAndProperties";
77
+ } else if (rateValue.startsWith("-")) {
78
+ return "propertiesOnly";
79
+ }
80
+ return "entityOnly";
81
+ }
82
+
83
+ isMethodAllowed(method, permissionScope) {
84
+ const startTime = Date.now();
85
+ const requestId = ulid();
86
+
87
+ // Create a base context for this method
88
+ const baseContext = createLogContext(
89
+ "PermissionValidator",
90
+ "isMethodAllowed",
91
+ {
92
+ requestId,
93
+ requestedMethod: method,
94
+ permissionScope
95
+ }
96
+ );
97
+
98
+ if (!this.methodPermissionMap[method]) {
99
+ logErrorWithMetrics(
100
+ "Unknown method detected",
101
+ {
102
+ ...baseContext,
103
+ duration: Date.now() - startTime
104
+ },
105
+ new Error(`Unknown method: ${method}`),
106
+ "permission_validation_error",
107
+ { error_type: "unknown_method" }
108
+ );
109
+ return false;
110
+ }
111
+
112
+ const methodConfig = this.methodPermissionMap[method];
113
+
114
+ // Handle both old array format and new object format
115
+ const scopes = Array.isArray(methodConfig) ? methodConfig : methodConfig.scopes;
116
+ const isAllowed = scopes.includes(permissionScope);
117
+
118
+ logger.debugWithContext("Method permission check completed", {
119
+ ...baseContext,
120
+ isAllowed,
121
+ duration: Date.now() - startTime
122
+ });
123
+
124
+ return isAllowed;
125
+ }
126
+
127
+ findMatchingEntity(entities, fullPath, requestId) {
128
+ const startTime = Date.now();
129
+ // Use provided requestId or generate a new one
130
+ const contextRequestId = requestId || ulid();
131
+
132
+ // Handle entities as an object with name and methods
133
+ const entity = entities.name;
134
+ const methods = entities.methods;
135
+
136
+ // Create a base context for this method
137
+ const baseContext = createLogContext(
138
+ "PermissionValidator",
139
+ "findMatchingEntity",
140
+ {
141
+ requestId: contextRequestId,
142
+ fullPath,
143
+ entity
144
+ }
145
+ );
146
+
147
+ // Log available methods for debugging
148
+ logger.debugWithContext("Checking permission for path", {
149
+ ...baseContext,
150
+ availableMethods: Object.keys(methods)
151
+ });
152
+
153
+ // Check for an exact match in the methods
154
+ if (methods.hasOwnProperty(fullPath)) {
155
+ const rateValue = methods[fullPath];
156
+ // Extract the operation name (last part of the path)
157
+ const operation = fullPath.split("/").pop();
158
+
159
+ logger.infoWithContext("Processing permission request", {
160
+ ...baseContext,
161
+ requestedMethod: operation,
162
+ });
163
+
164
+ const permissionScope = this.getPermissionScope(rateValue);
165
+ const isPermitted = this.isMethodAllowed(operation, permissionScope);
166
+
167
+ logger.debugWithContext("Permission check result", {
168
+ ...baseContext,
169
+ requestedMethod: operation,
170
+ permissionScope,
171
+ rateValue,
172
+ isPermitted,
173
+ duration: Date.now() - startTime
174
+ });
175
+
176
+ if (isPermitted) {
177
+ // Parse the rate limit from the permission value
178
+ const rateLimitInfo = this.parseRateLimit(rateValue);
179
+
180
+ return {
181
+ isPermitted: true,
182
+ commentsRate: rateValue,
183
+ permissionScope,
184
+ rateLimit: rateLimitInfo.limit,
185
+ unlimited: rateLimitInfo.unlimited,
186
+ operation, // Add the operation name for future rate limiting
187
+ };
188
+ }
189
+ }
190
+
191
+ // No special handling for session routes - all routes must be explicitly defined in the token
192
+
193
+ logger.warnWithContext("No matching permission found", {
194
+ ...baseContext,
195
+ duration: Date.now() - startTime
196
+ });
197
+
198
+ return {
199
+ isPermitted: false,
200
+ commentsRate: null,
201
+ permissionScope: null,
202
+ };
203
+ }
204
+
205
+ async validate(req) {
206
+ const requestId = req.headers["x-request-id"] || ulid();
207
+ const startTime = Date.now();
208
+
209
+ // Create a base context for this method
210
+ const baseContext = createLogContext(
211
+ "PermissionValidator",
212
+ "validate",
213
+ {
214
+ requestId,
215
+ path: req.path,
216
+ method: req.method,
217
+ ip: req.ip
218
+ }
219
+ );
220
+
221
+ const token = req.header("Authorization");
222
+ if (!token) {
223
+ logger.debugWithContext("Authorization token missing", {
224
+ ...baseContext,
225
+ userAgent: req.headers["user-agent"],
226
+ duration: Date.now() - startTime
227
+ });
228
+
229
+ return {
230
+ isValid: false,
231
+ status: 401,
232
+ message: "Access denied. No token provided.",
233
+ };
234
+ }
235
+
236
+ try {
237
+ const { decodeJwt } = await getJose();
238
+ const decodedToken = decodeJwt(token);
239
+
240
+ // Update context with user information
241
+ const contextWithUser = {
242
+ ...baseContext,
243
+ userId: decodedToken.sub || "unknown"
244
+ };
245
+
246
+ logger.infoWithContext("Endpoint access attempt", contextWithUser);
247
+
248
+ let permissionedRoutes;
249
+ try {
250
+ if (typeof decodedToken.rodit_permissionedroutes === 'string') {
251
+ permissionedRoutes = JSON.parse(decodedToken.rodit_permissionedroutes);
252
+ } else {
253
+ permissionedRoutes = decodedToken.rodit_permissionedroutes;
254
+ }
255
+ } catch (error) {
256
+ logErrorWithMetrics(
257
+ "Failed to parse permissioned routes",
258
+ contextWithUser,
259
+ error,
260
+ "permission_validation_error",
261
+ {
262
+ error_type: "parse_error",
263
+ permissionedRoutesType: typeof decodedToken.rodit_permissionedroutes,
264
+ valuePreview: decodedToken.rodit_permissionedroutes ?
265
+ decodedToken.rodit_permissionedroutes.substring(0, 100) : "undefined"
266
+ }
267
+ );
268
+ throw error;
269
+ }
270
+
271
+ // Add debug logging for permission routes content
272
+ logger.debugWithContext("Permission routes content", {
273
+ ...contextWithUser,
274
+ permissionedRoutes: JSON.stringify(permissionedRoutes).substring(0, 200) + "..."
275
+ });
276
+
277
+ // Use entities directly, assuming it's an object
278
+ const entities = permissionedRoutes.entities;
279
+
280
+ // Construct the full path for permission checking
281
+ // This must exactly match what's defined in the RODiT token
282
+ let fullPath = req.baseUrl + req.path;
283
+
284
+ // Normalize path by removing trailing slashes (except for root path)
285
+ // This handles common URL variations like /api/list_agents vs /api/agents/
286
+ if (fullPath.length > 1 && fullPath.endsWith('/')) {
287
+ fullPath = fullPath.slice(0, -1);
288
+ }
289
+
290
+ logger.debugWithContext("Validating permission for full path", {
291
+ ...contextWithUser,
292
+ baseUrl: req.baseUrl,
293
+ path: req.path,
294
+ fullPath
295
+ });
296
+
297
+ const { isPermitted, commentsRate, permissionScope, rateLimit, operation } =
298
+ this.findMatchingEntity(entities, fullPath, requestId);
299
+
300
+ if (!isPermitted) {
301
+ logger.warnWithContext("Permission denied", {
302
+ ...contextWithUser,
303
+ path: req.path,
304
+ fullPath,
305
+ userId: decodedToken.sub || "unknown",
306
+ duration: Date.now() - startTime,
307
+ requestId,
308
+ });
309
+
310
+ return {
311
+ isValid: false,
312
+ status: 403,
313
+ message: "Permission denied",
314
+ };
315
+ }
316
+
317
+ logger.infoWithContext("Authorization successful", {
318
+ ...contextWithUser,
319
+ fullPath,
320
+ permissionScope,
321
+ rateValue: commentsRate,
322
+ duration: Date.now() - startTime
323
+ });
324
+
325
+ return {
326
+ isValid: true,
327
+ commentsRate,
328
+ permissionScope,
329
+ rateLimit,
330
+ operation
331
+ };
332
+ } catch (error) {
333
+ // Use logErrorWithMetrics for better error tracking
334
+ logErrorWithMetrics(
335
+ "Permission check failed",
336
+ {
337
+ ...baseContext,
338
+ duration: Date.now() - startTime
339
+ },
340
+ error,
341
+ "permission_validation_error",
342
+ {
343
+ error_type: "validation_error",
344
+ errorCode: "112"
345
+ }
346
+ );
347
+
348
+ return {
349
+ isValid: false,
350
+ status: 400,
351
+ message: "Error 119: Invalid token or permissions.",
352
+ };
353
+ }
354
+ }
355
+ }
356
+
357
+ const permissionValidator = new PermissionValidator();
358
+
359
+ /**
360
+ * @swagger
361
+ * /authorize:
362
+ * get:
363
+ * summary: Authorize user and check JWT token fields
364
+ * description: Verify the JWT token and check specific fields before granting access
365
+ * security:
366
+ * - bearerAuth: []
367
+ * responses:
368
+ * 200:
369
+ * description: Authorization successful
370
+ * 401:
371
+ * description: Unauthorized - Invalid token or missing required fields
372
+ */
373
+ async function validatepermissions(req, res, next) {
374
+ const startTime = Date.now();
375
+ const requestId = req.headers["x-request-id"] || ulid();
376
+
377
+ if (!req.headers["x-request-id"]) {
378
+ req.headers["x-request-id"] = requestId;
379
+ }
380
+
381
+ // Create a base context for this middleware
382
+ const baseContext = createLogContext(
383
+ "validatepermissions",
384
+ "middleware",
385
+ {
386
+ requestId,
387
+ path: req.path,
388
+ baseUrl: req.baseUrl,
389
+ fullPath: req.baseUrl + req.path,
390
+ method: req.method,
391
+ ip: req.ip
392
+ }
393
+ );
394
+
395
+ logger.debugWithContext("Permission validation started", baseContext);
396
+
397
+ const result = await permissionValidator.validate(req);
398
+
399
+ if (!result.isValid) {
400
+ logger.warnWithContext("Permission validation failed", {
401
+ ...baseContext,
402
+ status: result.status,
403
+ message: result.message,
404
+ duration: Date.now() - startTime
405
+ });
406
+
407
+ return sendError(res, {
408
+ statusCode: result.status,
409
+ requestId,
410
+ code: result.code || "PERMISSION_DENIED",
411
+ message: result.message
412
+ });
413
+ }
414
+
415
+ if (result.commentsRate) {
416
+ req.commentsRate = result.commentsRate;
417
+ }
418
+
419
+ // Make rate limit information available for future implementation
420
+ req.rateLimit = {
421
+ value: result.rateLimit,
422
+ unlimited: result.unlimited === true,
423
+ operation: result.operation,
424
+ path: req.path,
425
+ timeWindow: 60 // Default time window in seconds
426
+ };
427
+
428
+ req.permissionScope = result.permissionScope;
429
+
430
+ logger.debugWithContext("Permission validation successful", {
431
+ ...baseContext,
432
+ permissionScope: result.permissionScope,
433
+ duration: Date.now() - startTime
434
+ });
435
+
436
+ next();
437
+ }
438
+
439
+ module.exports = validatepermissions;