@autorix/express 0.1.0 → 0.1.1

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 CHANGED
@@ -34,21 +34,47 @@ yarn add @autorix/express @autorix/core @autorix/storage
34
34
 
35
35
  ```typescript
36
36
  import express from 'express';
37
- import { autorixExpress, authorize } from '@autorix/express';
38
- import { Autorix } from '@autorix/core';
37
+ import { autorixExpress, authorize, autorixErrorHandler } from '@autorix/express';
38
+ import { evaluateAll } from '@autorix/core';
39
39
  import { MemoryPolicyProvider } from '@autorix/storage';
40
40
 
41
41
  const app = express();
42
+ app.use(express.json());
42
43
 
43
- // Initialize Autorix
44
+ // Initialize policy provider
44
45
  const policyProvider = new MemoryPolicyProvider();
45
- const autorix = new Autorix(policyProvider);
46
46
 
47
- // Add the Autorix middleware
47
+ // Create enforcer
48
+ const enforcer = {
49
+ can: async (input: { action: string; context: any; resource?: unknown }) => {
50
+ // ✅ IMPORTANT: If your app allows unauthenticated requests, guard here
51
+ // to avoid storage/providers crashing when principal is null.
52
+ if (!input.context?.principal) {
53
+ return { allowed: false, reason: 'Unauthenticated' };
54
+ }
55
+
56
+ const policies = await policyProvider.getPolicies({
57
+ scope: input.context.tenantId || 'default',
58
+ principal: input.context.principal,
59
+ roleIds: input.context.principal?.roles,
60
+ });
61
+
62
+ const result = evaluateAll({
63
+ policies: policies.map(p => p.document),
64
+ action: input.action,
65
+ resource: input.resource,
66
+ context: input.context,
67
+ });
68
+
69
+ return { allowed: result.allowed, reason: result.reason };
70
+ }
71
+ };
72
+
73
+ // Add the Autorix middleware (must be registered before routes)
48
74
  app.use(autorixExpress({
49
- enforcer: autorix,
75
+ enforcer,
50
76
  getPrincipal: async (req) => {
51
- // Extract user from your auth middleware
77
+ // Extract user from your auth middleware (e.g., JWT/Passport/session)
52
78
  return req.user ? { id: req.user.id, roles: req.user.roles } : null;
53
79
  },
54
80
  getTenant: async (req) => {
@@ -58,20 +84,23 @@ app.use(autorixExpress({
58
84
  }));
59
85
 
60
86
  // Protect routes with authorize middleware
61
- app.get('/admin/users',
62
- authorize('user:list'),
87
+ app.get(
88
+ '/admin/users',
89
+ authorize('user:list', { requireAuth: true }),
63
90
  (req, res) => {
64
91
  res.json({ message: 'Authorized!' });
65
92
  }
66
93
  );
67
94
 
68
- app.delete('/posts/:id',
95
+ app.delete(
96
+ '/posts/:id',
69
97
  authorize({
70
98
  action: 'post:delete',
99
+ requireAuth: true,
71
100
  resource: {
72
101
  type: 'post',
73
102
  idFrom: (req) => req.params.id,
74
- loader: async (id) => await db.posts.findById(id)
103
+ loader: async (id) => await db.posts.findById(id),
75
104
  }
76
105
  }),
77
106
  (req, res) => {
@@ -79,6 +108,10 @@ app.delete('/posts/:id',
79
108
  }
80
109
  );
81
110
 
111
+ // ✅ IMPORTANT: Register Autorix error handler at the end
112
+ // so authorization errors return clean HTTP responses (401/403) instead of stack traces.
113
+ app.use(autorixErrorHandler());
114
+
82
115
  app.listen(3000);
83
116
  ```
84
117
 
@@ -359,31 +392,71 @@ app.use(autorixExpress({
359
392
  }));
360
393
  ```
361
394
 
395
+ ---
396
+
362
397
  ## 🔧 Error Handling
363
398
 
364
- The package provides custom error classes:
399
+ `@autorix/express` provides an official error handler middleware to ensure authorization errors
400
+ are returned as clean HTTP responses (`401`, `403`) instead of unhandled stack traces.
365
401
 
366
- ```typescript
367
- import {
368
- AutorixForbiddenError,
369
- AutorixUnauthenticatedError,
370
- AutorixMissingMiddlewareError
371
- } from '@autorix/express';
402
+ ### ✅ Recommended (default)
403
+
404
+ ```ts
405
+ import { autorixErrorHandler } from '@autorix/express';
406
+
407
+ // Register at the end (after routes)
408
+ app.use(autorixErrorHandler());
409
+ ```
410
+
411
+ This middleware automatically handles all `AutorixHttpError` instances and formats them as JSON responses.
412
+
413
+ **Always register this middleware after all routes.**
414
+
415
+ ---
416
+
417
+ ### 🧩 Custom error handling (optional)
418
+
419
+ If you want full control over the error response format or logging, you can implement
420
+ your own handler using the exported error classes.
421
+
422
+ ```ts
423
+ import { AutorixHttpError } from '@autorix/express';
372
424
 
373
425
  app.use((err, req, res, next) => {
374
- if (err instanceof AutorixForbiddenError) {
375
- return res.status(403).json({ error: 'Forbidden' });
376
- }
377
- if (err instanceof AutorixUnauthenticatedError) {
378
- return res.status(401).json({ error: 'Authentication required' });
379
- }
380
- if (err instanceof AutorixMissingMiddlewareError) {
381
- return res.status(500).json({ error: 'Autorix middleware not configured' });
426
+ if (err instanceof AutorixHttpError) {
427
+ return res.status(err.statusCode).json({
428
+ error: {
429
+ code: err.code,
430
+ message: err.message
431
+ }
432
+ });
382
433
  }
434
+
383
435
  next(err);
384
436
  });
385
437
  ```
386
438
 
439
+ ---
440
+
441
+ ### ⚠️ Important notes
442
+
443
+ * If no error handler is registered, Express will print authorization errors as stack traces.
444
+ * Using `autorixErrorHandler()` is strongly recommended to avoid this behavior.
445
+ * For protected routes, use `requireAuth: true` in `authorize()` to return a clean `401 Unauthenticated`
446
+ response when no principal is present.
447
+
448
+ ---
449
+
450
+ ## 🧠 Why is this required?
451
+
452
+ Express does not support automatic global error handling inside normal middleware.
453
+ For this reason, error handlers must be registered explicitly at the application level.
454
+
455
+ This design follows the same pattern used by mature Express libraries
456
+ (e.g. `passport`, `express-rate-limit`, `celebrate`).
457
+
458
+ ---
459
+
387
460
  ## 🔗 Related Packages
388
461
 
389
462
  - [@autorix/core](../core) - Core policy evaluation engine
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  AutorixMissingMiddlewareError: () => AutorixMissingMiddlewareError,
27
27
  AutorixUnauthenticatedError: () => AutorixUnauthenticatedError,
28
28
  authorize: () => authorize,
29
+ autorixErrorHandler: () => autorixErrorHandler,
29
30
  autorixExpress: () => autorixExpress,
30
31
  buildRequestContext: () => buildRequestContext,
31
32
  resolvePrincipal: () => resolvePrincipal,
@@ -199,6 +200,42 @@ function authorize(actionOrConfig, cfg) {
199
200
  }, "authorizeMiddleware");
200
201
  }
201
202
  __name(authorize, "authorize");
203
+
204
+ // src/middleware/autorixErrorHandler.ts
205
+ function autorixErrorHandler(options = {}) {
206
+ const exposeStack = options.exposeStack ?? false;
207
+ const format = options.format ?? "json";
208
+ return (err, _req, res, next) => {
209
+ if (res.headersSent) return next(err);
210
+ if (err instanceof AutorixHttpError) {
211
+ if (format === "problem+json") {
212
+ return res.status(err.statusCode).type("application/problem+json").json({
213
+ type: `https://autorix.dev/errors/${err.code}`,
214
+ title: err.code,
215
+ status: err.statusCode,
216
+ detail: err.message
217
+ });
218
+ }
219
+ return res.status(err.statusCode).json({
220
+ error: {
221
+ code: err.code,
222
+ message: err.message
223
+ }
224
+ });
225
+ }
226
+ const status = err?.statusCode ?? err?.status ?? 500;
227
+ return res.status(status).json({
228
+ error: {
229
+ code: "INTERNAL_ERROR",
230
+ message: status === 500 ? "Internal Error" : err?.message ?? "Error",
231
+ ...exposeStack ? {
232
+ stack: String(err?.stack ?? "")
233
+ } : {}
234
+ }
235
+ });
236
+ };
237
+ }
238
+ __name(autorixErrorHandler, "autorixErrorHandler");
202
239
  // Annotate the CommonJS export names for ESM import in node:
203
240
  0 && (module.exports = {
204
241
  AutorixForbiddenError,
@@ -206,6 +243,7 @@ __name(authorize, "authorize");
206
243
  AutorixMissingMiddlewareError,
207
244
  AutorixUnauthenticatedError,
208
245
  authorize,
246
+ autorixErrorHandler,
209
247
  autorixExpress,
210
248
  buildRequestContext,
211
249
  resolvePrincipal,
package/dist/index.d.cts CHANGED
@@ -95,4 +95,10 @@ declare class AutorixMissingMiddlewareError extends AutorixHttpError {
95
95
  constructor(details?: unknown);
96
96
  }
97
97
 
98
- export { type AutorixErrorCode, type AutorixExpressOptions, AutorixForbiddenError, AutorixHttpError, AutorixMissingMiddlewareError, type AutorixRequestContext, AutorixUnauthenticatedError, type GetContextFn, type GetPrincipalFn, type Principal, type ResourceSpec, authorize, autorixExpress, buildRequestContext, resolvePrincipal, resolveResource };
98
+ type AutorixErrorHandlerOptions = {
99
+ exposeStack?: boolean;
100
+ format?: "json" | "problem+json";
101
+ };
102
+ declare function autorixErrorHandler(options?: AutorixErrorHandlerOptions): (err: any, _req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
103
+
104
+ export { type AutorixErrorCode, type AutorixErrorHandlerOptions, type AutorixExpressOptions, AutorixForbiddenError, AutorixHttpError, AutorixMissingMiddlewareError, type AutorixRequestContext, AutorixUnauthenticatedError, type GetContextFn, type GetPrincipalFn, type Principal, type ResourceSpec, authorize, autorixErrorHandler, autorixExpress, buildRequestContext, resolvePrincipal, resolveResource };
package/dist/index.d.ts CHANGED
@@ -95,4 +95,10 @@ declare class AutorixMissingMiddlewareError extends AutorixHttpError {
95
95
  constructor(details?: unknown);
96
96
  }
97
97
 
98
- export { type AutorixErrorCode, type AutorixExpressOptions, AutorixForbiddenError, AutorixHttpError, AutorixMissingMiddlewareError, type AutorixRequestContext, AutorixUnauthenticatedError, type GetContextFn, type GetPrincipalFn, type Principal, type ResourceSpec, authorize, autorixExpress, buildRequestContext, resolvePrincipal, resolveResource };
98
+ type AutorixErrorHandlerOptions = {
99
+ exposeStack?: boolean;
100
+ format?: "json" | "problem+json";
101
+ };
102
+ declare function autorixErrorHandler(options?: AutorixErrorHandlerOptions): (err: any, _req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
103
+
104
+ export { type AutorixErrorCode, type AutorixErrorHandlerOptions, type AutorixExpressOptions, AutorixForbiddenError, AutorixHttpError, AutorixMissingMiddlewareError, type AutorixRequestContext, AutorixUnauthenticatedError, type GetContextFn, type GetPrincipalFn, type Principal, type ResourceSpec, authorize, autorixErrorHandler, autorixExpress, buildRequestContext, resolvePrincipal, resolveResource };
package/dist/index.js CHANGED
@@ -167,12 +167,49 @@ function authorize(actionOrConfig, cfg) {
167
167
  }, "authorizeMiddleware");
168
168
  }
169
169
  __name(authorize, "authorize");
170
+
171
+ // src/middleware/autorixErrorHandler.ts
172
+ function autorixErrorHandler(options = {}) {
173
+ const exposeStack = options.exposeStack ?? false;
174
+ const format = options.format ?? "json";
175
+ return (err, _req, res, next) => {
176
+ if (res.headersSent) return next(err);
177
+ if (err instanceof AutorixHttpError) {
178
+ if (format === "problem+json") {
179
+ return res.status(err.statusCode).type("application/problem+json").json({
180
+ type: `https://autorix.dev/errors/${err.code}`,
181
+ title: err.code,
182
+ status: err.statusCode,
183
+ detail: err.message
184
+ });
185
+ }
186
+ return res.status(err.statusCode).json({
187
+ error: {
188
+ code: err.code,
189
+ message: err.message
190
+ }
191
+ });
192
+ }
193
+ const status = err?.statusCode ?? err?.status ?? 500;
194
+ return res.status(status).json({
195
+ error: {
196
+ code: "INTERNAL_ERROR",
197
+ message: status === 500 ? "Internal Error" : err?.message ?? "Error",
198
+ ...exposeStack ? {
199
+ stack: String(err?.stack ?? "")
200
+ } : {}
201
+ }
202
+ });
203
+ };
204
+ }
205
+ __name(autorixErrorHandler, "autorixErrorHandler");
170
206
  export {
171
207
  AutorixForbiddenError,
172
208
  AutorixHttpError,
173
209
  AutorixMissingMiddlewareError,
174
210
  AutorixUnauthenticatedError,
175
211
  authorize,
212
+ autorixErrorHandler,
176
213
  autorixExpress,
177
214
  buildRequestContext,
178
215
  resolvePrincipal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autorix/express",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Express.js integration for Autorix policy-based authorization (RBAC + ABAC)",
5
5
  "keywords": [
6
6
  "authorization",
@@ -17,7 +17,7 @@
17
17
  "repository": {
18
18
  "type": "git",
19
19
  "url": "https://github.com/chechooxd/autorix.git",
20
- "directory": "packages/autorix-express"
20
+ "directory": "packages/express"
21
21
  },
22
22
  "bugs": {
23
23
  "url": "https://github.com/chechooxd/autorix/issues"