@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 +99 -26
- package/dist/index.cjs +38 -0
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +37 -0
- package/package.json +2 -2
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 {
|
|
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
|
|
44
|
+
// Initialize policy provider
|
|
44
45
|
const policyProvider = new MemoryPolicyProvider();
|
|
45
|
-
const autorix = new Autorix(policyProvider);
|
|
46
46
|
|
|
47
|
-
//
|
|
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
|
|
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(
|
|
62
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
375
|
-
return res.status(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
20
|
+
"directory": "packages/express"
|
|
21
21
|
},
|
|
22
22
|
"bugs": {
|
|
23
23
|
"url": "https://github.com/chechooxd/autorix/issues"
|