@autorix/express 0.1.0 → 0.1.2
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 +151 -31
- package/dist/index.cjs +63 -4
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +62 -4
- package/package.json +4 -3
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; resource: string; context: any }) => {
|
|
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, // ← String for matching (e.g., 'post/123')
|
|
66
|
+
ctx: input.context, // ← Contains resource object in ctx.resource
|
|
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
|
|
|
@@ -176,6 +209,49 @@ authorize({
|
|
|
176
209
|
})
|
|
177
210
|
```
|
|
178
211
|
|
|
212
|
+
## 🔑 Understanding Resources
|
|
213
|
+
|
|
214
|
+
Autorix uses **two separate concepts** for resources:
|
|
215
|
+
|
|
216
|
+
1. **Resource String** (for pattern matching in policies)
|
|
217
|
+
- Format: `type/id` or `type/*`
|
|
218
|
+
- Used in policy `Resource` field
|
|
219
|
+
- Example: `'post/123'`, `'document/*'`
|
|
220
|
+
|
|
221
|
+
2. **Resource Object** (for attribute-based conditions)
|
|
222
|
+
- Contains resource properties/attributes
|
|
223
|
+
- Used in policy `Condition` blocks
|
|
224
|
+
- Example: `{ type: 'post', id: '123', authorId: 'u1' }`
|
|
225
|
+
|
|
226
|
+
**How they work together:**
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Policy matches by string pattern
|
|
230
|
+
Statement: [{
|
|
231
|
+
Effect: 'Allow',
|
|
232
|
+
Action: ['post:delete'],
|
|
233
|
+
Resource: 'post/*', // ← Matches 'post/123'
|
|
234
|
+
Condition: {
|
|
235
|
+
StringEquals: {
|
|
236
|
+
'resource.authorId': '${principal.id}' // ← Uses resource object
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}]
|
|
240
|
+
|
|
241
|
+
// Middleware automatically handles both
|
|
242
|
+
authorize({
|
|
243
|
+
action: 'post:delete',
|
|
244
|
+
resource: {
|
|
245
|
+
type: 'post',
|
|
246
|
+
idFrom: (req) => req.params.id, // → Builds string: 'post/123'
|
|
247
|
+
loader: async (id) => {
|
|
248
|
+
const post = await db.posts.findById(id);
|
|
249
|
+
return { authorId: post.authorId }; // → Object for conditions
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
```
|
|
254
|
+
|
|
179
255
|
## 🎯 Usage Examples
|
|
180
256
|
|
|
181
257
|
### Role-Based Access Control (RBAC)
|
|
@@ -220,16 +296,16 @@ app.get('/admin/settings',
|
|
|
220
296
|
### Attribute-Based Access Control (ABAC)
|
|
221
297
|
|
|
222
298
|
```typescript
|
|
223
|
-
// Policy with conditions
|
|
299
|
+
// Policy with conditions - checks resource attributes
|
|
224
300
|
await provider.setPolicy('owner-only', {
|
|
225
301
|
Version: '2024-01-01',
|
|
226
302
|
Statement: [{
|
|
227
303
|
Effect: 'Allow',
|
|
228
304
|
Action: ['post:update', 'post:delete'],
|
|
229
|
-
Resource: ['post/*'],
|
|
305
|
+
Resource: ['post/*'], // ← Matches 'post/123' string pattern
|
|
230
306
|
Condition: {
|
|
231
307
|
StringEquals: {
|
|
232
|
-
'
|
|
308
|
+
'resource.authorId': '${principal.id}' // ← Checks resource object property
|
|
233
309
|
}
|
|
234
310
|
}
|
|
235
311
|
}]
|
|
@@ -244,12 +320,16 @@ app.put('/posts/:id',
|
|
|
244
320
|
idFrom: (req) => req.params.id,
|
|
245
321
|
loader: async (id) => {
|
|
246
322
|
const post = await db.posts.findById(id);
|
|
247
|
-
|
|
323
|
+
// Return object with properties for conditions
|
|
324
|
+
return {
|
|
325
|
+
authorId: post.authorId, // ← This becomes resource.authorId in conditions
|
|
326
|
+
status: post.status
|
|
327
|
+
};
|
|
248
328
|
}
|
|
249
329
|
}
|
|
250
330
|
}),
|
|
251
331
|
(req, res) => {
|
|
252
|
-
// Only post
|
|
332
|
+
// Only post author can update
|
|
253
333
|
res.json({ message: 'Post updated!' });
|
|
254
334
|
}
|
|
255
335
|
);
|
|
@@ -359,31 +439,71 @@ app.use(autorixExpress({
|
|
|
359
439
|
}));
|
|
360
440
|
```
|
|
361
441
|
|
|
442
|
+
---
|
|
443
|
+
|
|
362
444
|
## 🔧 Error Handling
|
|
363
445
|
|
|
364
|
-
|
|
446
|
+
`@autorix/express` provides an official error handler middleware to ensure authorization errors
|
|
447
|
+
are returned as clean HTTP responses (`401`, `403`) instead of unhandled stack traces.
|
|
365
448
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
449
|
+
### ✅ Recommended (default)
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { autorixErrorHandler } from '@autorix/express';
|
|
453
|
+
|
|
454
|
+
// Register at the end (after routes)
|
|
455
|
+
app.use(autorixErrorHandler());
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
This middleware automatically handles all `AutorixHttpError` instances and formats them as JSON responses.
|
|
459
|
+
|
|
460
|
+
**Always register this middleware after all routes.**
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
### 🧩 Custom error handling (optional)
|
|
465
|
+
|
|
466
|
+
If you want full control over the error response format or logging, you can implement
|
|
467
|
+
your own handler using the exported error classes.
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { AutorixHttpError } from '@autorix/express';
|
|
372
471
|
|
|
373
472
|
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' });
|
|
473
|
+
if (err instanceof AutorixHttpError) {
|
|
474
|
+
return res.status(err.statusCode).json({
|
|
475
|
+
error: {
|
|
476
|
+
code: err.code,
|
|
477
|
+
message: err.message
|
|
478
|
+
}
|
|
479
|
+
});
|
|
382
480
|
}
|
|
481
|
+
|
|
383
482
|
next(err);
|
|
384
483
|
});
|
|
385
484
|
```
|
|
386
485
|
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### ⚠️ Important notes
|
|
489
|
+
|
|
490
|
+
* If no error handler is registered, Express will print authorization errors as stack traces.
|
|
491
|
+
* Using `autorixErrorHandler()` is strongly recommended to avoid this behavior.
|
|
492
|
+
* For protected routes, use `requireAuth: true` in `authorize()` to return a clean `401 Unauthenticated`
|
|
493
|
+
response when no principal is present.
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## 🧠 Why is this required?
|
|
498
|
+
|
|
499
|
+
Express does not support automatic global error handling inside normal middleware.
|
|
500
|
+
For this reason, error handlers must be registered explicitly at the application level.
|
|
501
|
+
|
|
502
|
+
This design follows the same pattern used by mature Express libraries
|
|
503
|
+
(e.g. `passport`, `express-rate-limit`, `celebrate`).
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
387
507
|
## 🔗 Related Packages
|
|
388
508
|
|
|
389
509
|
- [@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,
|
|
@@ -124,17 +125,31 @@ function autorixExpress(opts) {
|
|
|
124
125
|
const context = await buildRequestContext(req, opts);
|
|
125
126
|
req.autorix = {
|
|
126
127
|
context,
|
|
127
|
-
can: /* @__PURE__ */ __name(async (action,
|
|
128
|
+
can: /* @__PURE__ */ __name(async (action, resourceObj, ctxExtra) => {
|
|
129
|
+
let resourceString = "*";
|
|
130
|
+
if (typeof resourceObj === "object" && resourceObj && "type" in resourceObj) {
|
|
131
|
+
const resType = resourceObj.type;
|
|
132
|
+
const resId = resourceObj.id;
|
|
133
|
+
resourceString = resId ? `${resType}/${resId}` : `${resType}/*`;
|
|
134
|
+
} else if (typeof resourceObj === "string") {
|
|
135
|
+
resourceString = resourceObj;
|
|
136
|
+
} else if (resourceObj === void 0) {
|
|
137
|
+
const parts = action.split(":");
|
|
138
|
+
if (parts.length > 1) {
|
|
139
|
+
resourceString = `${parts[0]}/*`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
128
142
|
const decision = await opts.enforcer.can({
|
|
129
143
|
action,
|
|
144
|
+
resource: resourceString,
|
|
130
145
|
context: {
|
|
131
146
|
...context,
|
|
147
|
+
resource: resourceObj,
|
|
132
148
|
attributes: {
|
|
133
149
|
...context.attributes ?? {},
|
|
134
150
|
...ctxExtra ?? {}
|
|
135
151
|
}
|
|
136
|
-
}
|
|
137
|
-
resource
|
|
152
|
+
}
|
|
138
153
|
});
|
|
139
154
|
opts.onDecision?.({
|
|
140
155
|
...decision,
|
|
@@ -160,9 +175,16 @@ async function resolveResource(spec, req) {
|
|
|
160
175
|
if (typeof spec === "string") return {
|
|
161
176
|
type: spec
|
|
162
177
|
};
|
|
163
|
-
if ("loader" in spec) {
|
|
178
|
+
if ("loader" in spec && typeof spec.loader === "function" && typeof spec.idFrom === "function") {
|
|
164
179
|
const id = spec.idFrom(req);
|
|
165
180
|
const data = await spec.loader(id, req);
|
|
181
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
182
|
+
return {
|
|
183
|
+
type: spec.type,
|
|
184
|
+
id,
|
|
185
|
+
...data
|
|
186
|
+
};
|
|
187
|
+
}
|
|
166
188
|
return {
|
|
167
189
|
type: spec.type,
|
|
168
190
|
id,
|
|
@@ -199,6 +221,42 @@ function authorize(actionOrConfig, cfg) {
|
|
|
199
221
|
}, "authorizeMiddleware");
|
|
200
222
|
}
|
|
201
223
|
__name(authorize, "authorize");
|
|
224
|
+
|
|
225
|
+
// src/middleware/autorixErrorHandler.ts
|
|
226
|
+
function autorixErrorHandler(options = {}) {
|
|
227
|
+
const exposeStack = options.exposeStack ?? false;
|
|
228
|
+
const format = options.format ?? "json";
|
|
229
|
+
return (err, _req, res, next) => {
|
|
230
|
+
if (res.headersSent) return next(err);
|
|
231
|
+
if (err instanceof AutorixHttpError) {
|
|
232
|
+
if (format === "problem+json") {
|
|
233
|
+
return res.status(err.statusCode).type("application/problem+json").json({
|
|
234
|
+
type: `https://autorix.dev/errors/${err.code}`,
|
|
235
|
+
title: err.code,
|
|
236
|
+
status: err.statusCode,
|
|
237
|
+
detail: err.message
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return res.status(err.statusCode).json({
|
|
241
|
+
error: {
|
|
242
|
+
code: err.code,
|
|
243
|
+
message: err.message
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const status = err?.statusCode ?? err?.status ?? 500;
|
|
248
|
+
return res.status(status).json({
|
|
249
|
+
error: {
|
|
250
|
+
code: "INTERNAL_ERROR",
|
|
251
|
+
message: status === 500 ? "Internal Error" : err?.message ?? "Error",
|
|
252
|
+
...exposeStack ? {
|
|
253
|
+
stack: String(err?.stack ?? "")
|
|
254
|
+
} : {}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
__name(autorixErrorHandler, "autorixErrorHandler");
|
|
202
260
|
// Annotate the CommonJS export names for ESM import in node:
|
|
203
261
|
0 && (module.exports = {
|
|
204
262
|
AutorixForbiddenError,
|
|
@@ -206,6 +264,7 @@ __name(authorize, "authorize");
|
|
|
206
264
|
AutorixMissingMiddlewareError,
|
|
207
265
|
AutorixUnauthenticatedError,
|
|
208
266
|
authorize,
|
|
267
|
+
autorixErrorHandler,
|
|
209
268
|
autorixExpress,
|
|
210
269
|
buildRequestContext,
|
|
211
270
|
resolvePrincipal,
|
package/dist/index.d.cts
CHANGED
|
@@ -17,19 +17,19 @@ type AutorixRequestContext = {
|
|
|
17
17
|
type ResourceSpec = string | {
|
|
18
18
|
type: string;
|
|
19
19
|
id?: string;
|
|
20
|
-
|
|
20
|
+
[key: string]: unknown;
|
|
21
21
|
} | {
|
|
22
22
|
type: string;
|
|
23
23
|
idFrom: (req: Request) => string;
|
|
24
|
-
loader: (id: string, req: Request) => Promise<unknown
|
|
24
|
+
loader: (id: string, req: Request) => Promise<Record<string, unknown>>;
|
|
25
25
|
};
|
|
26
26
|
type GetContextFn = (req: Request) => Partial<AutorixRequestContext> | Promise<Partial<AutorixRequestContext>>;
|
|
27
27
|
type AutorixExpressOptions = {
|
|
28
28
|
enforcer: {
|
|
29
29
|
can: (input: {
|
|
30
30
|
action: string;
|
|
31
|
+
resource: string;
|
|
31
32
|
context: AutorixRequestContext;
|
|
32
|
-
resource?: unknown;
|
|
33
33
|
}) => Promise<{
|
|
34
34
|
allowed: boolean;
|
|
35
35
|
reason?: string;
|
|
@@ -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
|
@@ -17,19 +17,19 @@ type AutorixRequestContext = {
|
|
|
17
17
|
type ResourceSpec = string | {
|
|
18
18
|
type: string;
|
|
19
19
|
id?: string;
|
|
20
|
-
|
|
20
|
+
[key: string]: unknown;
|
|
21
21
|
} | {
|
|
22
22
|
type: string;
|
|
23
23
|
idFrom: (req: Request) => string;
|
|
24
|
-
loader: (id: string, req: Request) => Promise<unknown
|
|
24
|
+
loader: (id: string, req: Request) => Promise<Record<string, unknown>>;
|
|
25
25
|
};
|
|
26
26
|
type GetContextFn = (req: Request) => Partial<AutorixRequestContext> | Promise<Partial<AutorixRequestContext>>;
|
|
27
27
|
type AutorixExpressOptions = {
|
|
28
28
|
enforcer: {
|
|
29
29
|
can: (input: {
|
|
30
30
|
action: string;
|
|
31
|
+
resource: string;
|
|
31
32
|
context: AutorixRequestContext;
|
|
32
|
-
resource?: unknown;
|
|
33
33
|
}) => Promise<{
|
|
34
34
|
allowed: boolean;
|
|
35
35
|
reason?: string;
|
|
@@ -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
|
@@ -92,17 +92,31 @@ function autorixExpress(opts) {
|
|
|
92
92
|
const context = await buildRequestContext(req, opts);
|
|
93
93
|
req.autorix = {
|
|
94
94
|
context,
|
|
95
|
-
can: /* @__PURE__ */ __name(async (action,
|
|
95
|
+
can: /* @__PURE__ */ __name(async (action, resourceObj, ctxExtra) => {
|
|
96
|
+
let resourceString = "*";
|
|
97
|
+
if (typeof resourceObj === "object" && resourceObj && "type" in resourceObj) {
|
|
98
|
+
const resType = resourceObj.type;
|
|
99
|
+
const resId = resourceObj.id;
|
|
100
|
+
resourceString = resId ? `${resType}/${resId}` : `${resType}/*`;
|
|
101
|
+
} else if (typeof resourceObj === "string") {
|
|
102
|
+
resourceString = resourceObj;
|
|
103
|
+
} else if (resourceObj === void 0) {
|
|
104
|
+
const parts = action.split(":");
|
|
105
|
+
if (parts.length > 1) {
|
|
106
|
+
resourceString = `${parts[0]}/*`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
96
109
|
const decision = await opts.enforcer.can({
|
|
97
110
|
action,
|
|
111
|
+
resource: resourceString,
|
|
98
112
|
context: {
|
|
99
113
|
...context,
|
|
114
|
+
resource: resourceObj,
|
|
100
115
|
attributes: {
|
|
101
116
|
...context.attributes ?? {},
|
|
102
117
|
...ctxExtra ?? {}
|
|
103
118
|
}
|
|
104
|
-
}
|
|
105
|
-
resource
|
|
119
|
+
}
|
|
106
120
|
});
|
|
107
121
|
opts.onDecision?.({
|
|
108
122
|
...decision,
|
|
@@ -128,9 +142,16 @@ async function resolveResource(spec, req) {
|
|
|
128
142
|
if (typeof spec === "string") return {
|
|
129
143
|
type: spec
|
|
130
144
|
};
|
|
131
|
-
if ("loader" in spec) {
|
|
145
|
+
if ("loader" in spec && typeof spec.loader === "function" && typeof spec.idFrom === "function") {
|
|
132
146
|
const id = spec.idFrom(req);
|
|
133
147
|
const data = await spec.loader(id, req);
|
|
148
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
149
|
+
return {
|
|
150
|
+
type: spec.type,
|
|
151
|
+
id,
|
|
152
|
+
...data
|
|
153
|
+
};
|
|
154
|
+
}
|
|
134
155
|
return {
|
|
135
156
|
type: spec.type,
|
|
136
157
|
id,
|
|
@@ -167,12 +188,49 @@ function authorize(actionOrConfig, cfg) {
|
|
|
167
188
|
}, "authorizeMiddleware");
|
|
168
189
|
}
|
|
169
190
|
__name(authorize, "authorize");
|
|
191
|
+
|
|
192
|
+
// src/middleware/autorixErrorHandler.ts
|
|
193
|
+
function autorixErrorHandler(options = {}) {
|
|
194
|
+
const exposeStack = options.exposeStack ?? false;
|
|
195
|
+
const format = options.format ?? "json";
|
|
196
|
+
return (err, _req, res, next) => {
|
|
197
|
+
if (res.headersSent) return next(err);
|
|
198
|
+
if (err instanceof AutorixHttpError) {
|
|
199
|
+
if (format === "problem+json") {
|
|
200
|
+
return res.status(err.statusCode).type("application/problem+json").json({
|
|
201
|
+
type: `https://autorix.dev/errors/${err.code}`,
|
|
202
|
+
title: err.code,
|
|
203
|
+
status: err.statusCode,
|
|
204
|
+
detail: err.message
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return res.status(err.statusCode).json({
|
|
208
|
+
error: {
|
|
209
|
+
code: err.code,
|
|
210
|
+
message: err.message
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const status = err?.statusCode ?? err?.status ?? 500;
|
|
215
|
+
return res.status(status).json({
|
|
216
|
+
error: {
|
|
217
|
+
code: "INTERNAL_ERROR",
|
|
218
|
+
message: status === 500 ? "Internal Error" : err?.message ?? "Error",
|
|
219
|
+
...exposeStack ? {
|
|
220
|
+
stack: String(err?.stack ?? "")
|
|
221
|
+
} : {}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
__name(autorixErrorHandler, "autorixErrorHandler");
|
|
170
227
|
export {
|
|
171
228
|
AutorixForbiddenError,
|
|
172
229
|
AutorixHttpError,
|
|
173
230
|
AutorixMissingMiddlewareError,
|
|
174
231
|
AutorixUnauthenticatedError,
|
|
175
232
|
authorize,
|
|
233
|
+
autorixErrorHandler,
|
|
176
234
|
autorixExpress,
|
|
177
235
|
buildRequestContext,
|
|
178
236
|
resolvePrincipal,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autorix/express",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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"
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"@types/express": "^5.0.6",
|
|
56
56
|
"@types/supertest": "^6.0.3",
|
|
57
57
|
"express": "^5.2.1",
|
|
58
|
-
"supertest": "^7.2.2"
|
|
58
|
+
"supertest": "^7.2.2",
|
|
59
|
+
"@autorix/storage": "^0.1.0"
|
|
59
60
|
},
|
|
60
61
|
"engines": {
|
|
61
62
|
"node": ">=18"
|