@autorix/express 0.1.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/License +21 -0
- package/README.md +403 -0
- package/dist/index.cjs +213 -0
- package/dist/index.d.cts +98 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +180 -0
- package/package.json +63 -0
package/License
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Sergio Galaz <www.adinet.app>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# @autorix/express
|
|
2
|
+
|
|
3
|
+
**Express.js integration for Autorix policy-based authorization (RBAC + ABAC)**
|
|
4
|
+
|
|
5
|
+
Middleware and utilities to integrate Autorix authorization into your Express.js applications with a clean, flexible API.
|
|
6
|
+
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
|
|
10
|
+
## ✨ Features
|
|
11
|
+
|
|
12
|
+
- 🛡️ **Middleware-based** - Simple Express.js middleware integration
|
|
13
|
+
- 🎯 **Route-level Authorization** - Protect specific routes with the `authorize` middleware
|
|
14
|
+
- 🔄 **Request Context** - Automatic context building from Express requests
|
|
15
|
+
- 💪 **Flexible Principal Resolution** - Custom logic for extracting user information
|
|
16
|
+
- 🏢 **Multi-tenant Support** - Built-in tenant isolation
|
|
17
|
+
- 📦 **Resource Loading** - Automatic resource resolution from route parameters
|
|
18
|
+
- 🚀 **TypeScript Ready** - Full type safety with Express types augmentation
|
|
19
|
+
- ⚡ **Zero Config** - Works out of the box with sensible defaults
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @autorix/express @autorix/core @autorix/storage
|
|
25
|
+
# or
|
|
26
|
+
pnpm add @autorix/express @autorix/core @autorix/storage
|
|
27
|
+
# or
|
|
28
|
+
yarn add @autorix/express @autorix/core @autorix/storage
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 🚀 Quick Start
|
|
32
|
+
|
|
33
|
+
### Basic Setup
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import express from 'express';
|
|
37
|
+
import { autorixExpress, authorize } from '@autorix/express';
|
|
38
|
+
import { Autorix } from '@autorix/core';
|
|
39
|
+
import { MemoryPolicyProvider } from '@autorix/storage';
|
|
40
|
+
|
|
41
|
+
const app = express();
|
|
42
|
+
|
|
43
|
+
// Initialize Autorix
|
|
44
|
+
const policyProvider = new MemoryPolicyProvider();
|
|
45
|
+
const autorix = new Autorix(policyProvider);
|
|
46
|
+
|
|
47
|
+
// Add the Autorix middleware
|
|
48
|
+
app.use(autorixExpress({
|
|
49
|
+
enforcer: autorix,
|
|
50
|
+
getPrincipal: async (req) => {
|
|
51
|
+
// Extract user from your auth middleware
|
|
52
|
+
return req.user ? { id: req.user.id, roles: req.user.roles } : null;
|
|
53
|
+
},
|
|
54
|
+
getTenant: async (req) => {
|
|
55
|
+
// Extract tenant/organization ID
|
|
56
|
+
return req.user?.tenantId || null;
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// Protect routes with authorize middleware
|
|
61
|
+
app.get('/admin/users',
|
|
62
|
+
authorize('user:list'),
|
|
63
|
+
(req, res) => {
|
|
64
|
+
res.json({ message: 'Authorized!' });
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
app.delete('/posts/:id',
|
|
69
|
+
authorize({
|
|
70
|
+
action: 'post:delete',
|
|
71
|
+
resource: {
|
|
72
|
+
type: 'post',
|
|
73
|
+
idFrom: (req) => req.params.id,
|
|
74
|
+
loader: async (id) => await db.posts.findById(id)
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
77
|
+
(req, res) => {
|
|
78
|
+
res.json({ message: 'Post deleted!' });
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
app.listen(3000);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 📚 API Reference
|
|
86
|
+
|
|
87
|
+
### `autorixExpress(options)`
|
|
88
|
+
|
|
89
|
+
Main middleware that initializes Autorix on the Express request object.
|
|
90
|
+
|
|
91
|
+
#### Options
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
type AutorixExpressOptions = {
|
|
95
|
+
enforcer: {
|
|
96
|
+
can: (input: {
|
|
97
|
+
action: string;
|
|
98
|
+
context: AutorixRequestContext;
|
|
99
|
+
resource?: unknown;
|
|
100
|
+
}) => Promise<{ allowed: boolean; reason?: string }>;
|
|
101
|
+
};
|
|
102
|
+
getPrincipal: (req: Request) => Principal | Promise<Principal>;
|
|
103
|
+
getTenant?: (req: Request) => string | null | Promise<string | null>;
|
|
104
|
+
getContext?: (req: Request) => Partial<AutorixRequestContext> | Promise<Partial<AutorixRequestContext>>;
|
|
105
|
+
onDecision?: (decision: { allowed: boolean; action: string; reason?: string }, req: Request) => void;
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- **`enforcer`**: The Autorix instance or compatible enforcer
|
|
110
|
+
- **`getPrincipal`**: Function to extract user/principal information from the request
|
|
111
|
+
- **`getTenant`** *(optional)*: Function to extract tenant/organization ID
|
|
112
|
+
- **`getContext`** *(optional)*: Additional context builder (IP, user agent, custom attributes)
|
|
113
|
+
- **`onDecision`** *(optional)*: Callback for logging/auditing authorization decisions
|
|
114
|
+
|
|
115
|
+
#### Request Augmentation
|
|
116
|
+
|
|
117
|
+
After this middleware runs, `req.autorix` is available with:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
req.autorix = {
|
|
121
|
+
context: AutorixRequestContext,
|
|
122
|
+
can: (action: string, resource?: unknown, ctxExtra?: Record<string, unknown>) => Promise<boolean>,
|
|
123
|
+
enforce: (action: string, resource?: unknown, ctxExtra?: Record<string, unknown>) => Promise<void>
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `authorize(config)`
|
|
128
|
+
|
|
129
|
+
Route-level middleware for authorization checks.
|
|
130
|
+
|
|
131
|
+
#### Simple Usage
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
app.get('/users', authorize('user:list'), handler);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Advanced Usage
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
authorize({
|
|
141
|
+
action: string;
|
|
142
|
+
resource?: ResourceSpec;
|
|
143
|
+
context?: Record<string, unknown> | ((req: Request) => Record<string, unknown> | Promise<Record<string, unknown>>);
|
|
144
|
+
requireAuth?: boolean;
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **`action`**: The action to authorize (e.g., `'user:read'`, `'post:delete'`)
|
|
149
|
+
- **`resource`** *(optional)*: Resource specification
|
|
150
|
+
- String: Static resource identifier
|
|
151
|
+
- Object with `type`, `id`, `data`: Static resource object
|
|
152
|
+
- Object with `type`, `idFrom`, `loader`: Dynamic resource loading
|
|
153
|
+
- **`context`** *(optional)*: Additional context attributes or function to compute them
|
|
154
|
+
- **`requireAuth`** *(optional)*: Whether to require authenticated principal (throws `AutorixUnauthenticatedError` if not present)
|
|
155
|
+
|
|
156
|
+
#### Resource Specification
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Static resource
|
|
160
|
+
authorize({ action: 'post:read', resource: 'post/123' })
|
|
161
|
+
|
|
162
|
+
// Resource from route params
|
|
163
|
+
authorize({
|
|
164
|
+
action: 'post:delete',
|
|
165
|
+
resource: {
|
|
166
|
+
type: 'post',
|
|
167
|
+
idFrom: (req) => req.params.id,
|
|
168
|
+
loader: async (id, req) => await db.posts.findById(id)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// Pre-loaded resource
|
|
173
|
+
authorize({
|
|
174
|
+
action: 'user:update',
|
|
175
|
+
resource: { type: 'user', id: '123', data: { ownerId: 'u1' } }
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 🎯 Usage Examples
|
|
180
|
+
|
|
181
|
+
### Role-Based Access Control (RBAC)
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { autorixExpress, authorize } from '@autorix/express';
|
|
185
|
+
import { Autorix } from '@autorix/core';
|
|
186
|
+
import { MemoryPolicyProvider } from '@autorix/storage';
|
|
187
|
+
|
|
188
|
+
const provider = new MemoryPolicyProvider();
|
|
189
|
+
const autorix = new Autorix(provider);
|
|
190
|
+
|
|
191
|
+
// Add policies
|
|
192
|
+
await provider.attachPolicy({
|
|
193
|
+
scope: { type: 'TENANT', id: 't1' },
|
|
194
|
+
policyId: 'admin-policy',
|
|
195
|
+
principals: [{ type: 'ROLE', id: 'admin' }]
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await provider.setPolicy('admin-policy', {
|
|
199
|
+
Version: '2024-01-01',
|
|
200
|
+
Statement: [{
|
|
201
|
+
Effect: 'Allow',
|
|
202
|
+
Action: ['*'],
|
|
203
|
+
Resource: ['*']
|
|
204
|
+
}]
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
app.use(autorixExpress({
|
|
208
|
+
enforcer: autorix,
|
|
209
|
+
getPrincipal: (req) => req.user || null,
|
|
210
|
+
getTenant: (req) => req.user?.tenantId || 't1'
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
// Only admins can access
|
|
214
|
+
app.get('/admin/settings',
|
|
215
|
+
authorize('admin:settings:read'),
|
|
216
|
+
(req, res) => res.json({ settings: {} })
|
|
217
|
+
);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Attribute-Based Access Control (ABAC)
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Policy with conditions
|
|
224
|
+
await provider.setPolicy('owner-only', {
|
|
225
|
+
Version: '2024-01-01',
|
|
226
|
+
Statement: [{
|
|
227
|
+
Effect: 'Allow',
|
|
228
|
+
Action: ['post:update', 'post:delete'],
|
|
229
|
+
Resource: ['post/*'],
|
|
230
|
+
Condition: {
|
|
231
|
+
StringEquals: {
|
|
232
|
+
'principal.id': '${resource.ownerId}'
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}]
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Route with resource loading
|
|
239
|
+
app.put('/posts/:id',
|
|
240
|
+
authorize({
|
|
241
|
+
action: 'post:update',
|
|
242
|
+
resource: {
|
|
243
|
+
type: 'post',
|
|
244
|
+
idFrom: (req) => req.params.id,
|
|
245
|
+
loader: async (id) => {
|
|
246
|
+
const post = await db.posts.findById(id);
|
|
247
|
+
return { ...post, ownerId: post.userId };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}),
|
|
251
|
+
(req, res) => {
|
|
252
|
+
// Only post owner can update
|
|
253
|
+
res.json({ message: 'Post updated!' });
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Custom Context Attributes
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
app.use(autorixExpress({
|
|
262
|
+
enforcer: autorix,
|
|
263
|
+
getPrincipal: (req) => req.user,
|
|
264
|
+
getContext: (req) => ({
|
|
265
|
+
ip: req.ip,
|
|
266
|
+
userAgent: req.get('user-agent'),
|
|
267
|
+
requestId: req.id,
|
|
268
|
+
attributes: {
|
|
269
|
+
timeOfDay: new Date().getHours(),
|
|
270
|
+
department: req.user?.department
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}));
|
|
274
|
+
|
|
275
|
+
// Policy can check custom attributes
|
|
276
|
+
await provider.setPolicy('business-hours', {
|
|
277
|
+
Version: '2024-01-01',
|
|
278
|
+
Statement: [{
|
|
279
|
+
Effect: 'Allow',
|
|
280
|
+
Action: ['report:generate'],
|
|
281
|
+
Resource: ['*'],
|
|
282
|
+
Condition: {
|
|
283
|
+
NumericGreaterThanEquals: { 'context.attributes.timeOfDay': 9 },
|
|
284
|
+
NumericLessThanEquals: { 'context.attributes.timeOfDay': 17 }
|
|
285
|
+
}
|
|
286
|
+
}]
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Manual Authorization Checks
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
app.get('/mixed-access', async (req, res) => {
|
|
294
|
+
// Check without throwing
|
|
295
|
+
const canRead = await req.autorix.can('document:read', { type: 'document', id: '123' });
|
|
296
|
+
const canEdit = await req.autorix.can('document:edit', { type: 'document', id: '123' });
|
|
297
|
+
|
|
298
|
+
res.json({
|
|
299
|
+
document: canRead ? await loadDocument('123') : null,
|
|
300
|
+
editable: canEdit
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
app.post('/critical-action', async (req, res) => {
|
|
305
|
+
// Enforce (throws on deny)
|
|
306
|
+
await req.autorix.enforce('admin:critical:execute');
|
|
307
|
+
|
|
308
|
+
// This code only runs if authorized
|
|
309
|
+
await performCriticalAction();
|
|
310
|
+
res.json({ success: true });
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Multi-tenant Isolation
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
app.use(autorixExpress({
|
|
318
|
+
enforcer: autorix,
|
|
319
|
+
getPrincipal: (req) => req.user,
|
|
320
|
+
getTenant: (req) => {
|
|
321
|
+
// Extract from subdomain
|
|
322
|
+
const subdomain = req.subdomains[0];
|
|
323
|
+
return subdomain || req.user?.tenantId;
|
|
324
|
+
}
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
// Each tenant has isolated policies
|
|
328
|
+
await provider.attachPolicy({
|
|
329
|
+
scope: { type: 'TENANT', id: 'acme-corp' },
|
|
330
|
+
policyId: 'acme-admins',
|
|
331
|
+
principals: [{ type: 'USER', id: 'alice' }]
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await provider.attachPolicy({
|
|
335
|
+
scope: { type: 'TENANT', id: 'other-corp' },
|
|
336
|
+
policyId: 'other-admins',
|
|
337
|
+
principals: [{ type: 'USER', id: 'bob' }]
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Audit Logging
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
app.use(autorixExpress({
|
|
345
|
+
enforcer: autorix,
|
|
346
|
+
getPrincipal: (req) => req.user,
|
|
347
|
+
onDecision: (decision, req) => {
|
|
348
|
+
// Log all authorization decisions
|
|
349
|
+
logger.info('Authorization decision', {
|
|
350
|
+
userId: req.user?.id,
|
|
351
|
+
action: decision.action,
|
|
352
|
+
allowed: decision.allowed,
|
|
353
|
+
reason: decision.reason,
|
|
354
|
+
path: req.path,
|
|
355
|
+
method: req.method,
|
|
356
|
+
timestamp: new Date()
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}));
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## 🔧 Error Handling
|
|
363
|
+
|
|
364
|
+
The package provides custom error classes:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import {
|
|
368
|
+
AutorixForbiddenError,
|
|
369
|
+
AutorixUnauthenticatedError,
|
|
370
|
+
AutorixMissingMiddlewareError
|
|
371
|
+
} from '@autorix/express';
|
|
372
|
+
|
|
373
|
+
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' });
|
|
382
|
+
}
|
|
383
|
+
next(err);
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## 🔗 Related Packages
|
|
388
|
+
|
|
389
|
+
- [@autorix/core](../core) - Core policy evaluation engine
|
|
390
|
+
- [@autorix/storage](../storage) - Policy storage providers
|
|
391
|
+
- [@autorix/nestjs](../nestjs) - NestJS integration
|
|
392
|
+
|
|
393
|
+
## 📝 License
|
|
394
|
+
|
|
395
|
+
MIT © [Chechooxd](https://github.com/chechooxd)
|
|
396
|
+
|
|
397
|
+
## 🤝 Contributing
|
|
398
|
+
|
|
399
|
+
Contributions are welcome! Please check the [main repository](https://github.com/chechooxd/autorix) for guidelines.
|
|
400
|
+
|
|
401
|
+
## 📖 Documentation
|
|
402
|
+
|
|
403
|
+
For more information, visit the [Autorix documentation](https://github.com/chechooxd/autorix).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
AutorixForbiddenError: () => AutorixForbiddenError,
|
|
25
|
+
AutorixHttpError: () => AutorixHttpError,
|
|
26
|
+
AutorixMissingMiddlewareError: () => AutorixMissingMiddlewareError,
|
|
27
|
+
AutorixUnauthenticatedError: () => AutorixUnauthenticatedError,
|
|
28
|
+
authorize: () => authorize,
|
|
29
|
+
autorixExpress: () => autorixExpress,
|
|
30
|
+
buildRequestContext: () => buildRequestContext,
|
|
31
|
+
resolvePrincipal: () => resolvePrincipal,
|
|
32
|
+
resolveResource: () => resolveResource
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
|
|
36
|
+
// src/context/principal.ts
|
|
37
|
+
async function resolvePrincipal(req, getPrincipal) {
|
|
38
|
+
return await getPrincipal(req);
|
|
39
|
+
}
|
|
40
|
+
__name(resolvePrincipal, "resolvePrincipal");
|
|
41
|
+
|
|
42
|
+
// src/context/buildRequestContext.ts
|
|
43
|
+
async function buildRequestContext(req, opts) {
|
|
44
|
+
const principal = await resolvePrincipal(req, opts.getPrincipal);
|
|
45
|
+
const tenantId = opts.getTenant ? await opts.getTenant(req) : null;
|
|
46
|
+
const extra = opts.getContext ? await opts.getContext(req) : {};
|
|
47
|
+
const context = {
|
|
48
|
+
principal,
|
|
49
|
+
tenantId: tenantId ?? void 0,
|
|
50
|
+
ip: req.ip,
|
|
51
|
+
userAgent: req.headers["user-agent"],
|
|
52
|
+
requestId: req.headers["x-request-id"],
|
|
53
|
+
attributes: extra?.attributes,
|
|
54
|
+
resource: void 0,
|
|
55
|
+
...extra
|
|
56
|
+
};
|
|
57
|
+
return context;
|
|
58
|
+
}
|
|
59
|
+
__name(buildRequestContext, "buildRequestContext");
|
|
60
|
+
|
|
61
|
+
// src/errors/AutorixHttpErrors.ts
|
|
62
|
+
var AutorixHttpError = class extends Error {
|
|
63
|
+
static {
|
|
64
|
+
__name(this, "AutorixHttpError");
|
|
65
|
+
}
|
|
66
|
+
statusCode;
|
|
67
|
+
code;
|
|
68
|
+
details;
|
|
69
|
+
constructor(params) {
|
|
70
|
+
super(params.message);
|
|
71
|
+
this.name = "AutorixHttpError";
|
|
72
|
+
this.statusCode = params.statusCode;
|
|
73
|
+
this.code = params.code;
|
|
74
|
+
this.details = params.details;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var AutorixForbiddenError = class extends AutorixHttpError {
|
|
78
|
+
static {
|
|
79
|
+
__name(this, "AutorixForbiddenError");
|
|
80
|
+
}
|
|
81
|
+
constructor(reason, details) {
|
|
82
|
+
super({
|
|
83
|
+
message: reason ?? "Forbidden",
|
|
84
|
+
statusCode: 403,
|
|
85
|
+
code: "AUTORIX_FORBIDDEN",
|
|
86
|
+
details
|
|
87
|
+
});
|
|
88
|
+
this.name = "AutorixForbiddenError";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var AutorixUnauthenticatedError = class extends AutorixHttpError {
|
|
92
|
+
static {
|
|
93
|
+
__name(this, "AutorixUnauthenticatedError");
|
|
94
|
+
}
|
|
95
|
+
constructor(details) {
|
|
96
|
+
super({
|
|
97
|
+
message: "Unauthenticated",
|
|
98
|
+
statusCode: 401,
|
|
99
|
+
code: "AUTORIX_UNAUTHENTICATED",
|
|
100
|
+
details
|
|
101
|
+
});
|
|
102
|
+
this.name = "AutorixUnauthenticatedError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var AutorixMissingMiddlewareError = class extends AutorixHttpError {
|
|
106
|
+
static {
|
|
107
|
+
__name(this, "AutorixMissingMiddlewareError");
|
|
108
|
+
}
|
|
109
|
+
constructor(details) {
|
|
110
|
+
super({
|
|
111
|
+
message: "Autorix middleware not registered",
|
|
112
|
+
statusCode: 500,
|
|
113
|
+
code: "AUTORIX_MISSING_MIDDLEWARE",
|
|
114
|
+
details
|
|
115
|
+
});
|
|
116
|
+
this.name = "AutorixMissingMiddlewareError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/middleware/autorixExpress.ts
|
|
121
|
+
function autorixExpress(opts) {
|
|
122
|
+
return /* @__PURE__ */ __name(async function autorixMiddleware(req, _res, next) {
|
|
123
|
+
try {
|
|
124
|
+
const context = await buildRequestContext(req, opts);
|
|
125
|
+
req.autorix = {
|
|
126
|
+
context,
|
|
127
|
+
can: /* @__PURE__ */ __name(async (action, resource, ctxExtra) => {
|
|
128
|
+
const decision = await opts.enforcer.can({
|
|
129
|
+
action,
|
|
130
|
+
context: {
|
|
131
|
+
...context,
|
|
132
|
+
attributes: {
|
|
133
|
+
...context.attributes ?? {},
|
|
134
|
+
...ctxExtra ?? {}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
resource
|
|
138
|
+
});
|
|
139
|
+
opts.onDecision?.({
|
|
140
|
+
...decision,
|
|
141
|
+
action
|
|
142
|
+
}, req);
|
|
143
|
+
return decision.allowed;
|
|
144
|
+
}, "can"),
|
|
145
|
+
enforce: /* @__PURE__ */ __name(async (action, resource, ctxExtra) => {
|
|
146
|
+
const allowed = await req.autorix.can(action, resource, ctxExtra);
|
|
147
|
+
if (!allowed) throw new AutorixForbiddenError();
|
|
148
|
+
}, "enforce")
|
|
149
|
+
};
|
|
150
|
+
return next();
|
|
151
|
+
} catch (e) {
|
|
152
|
+
return next(e);
|
|
153
|
+
}
|
|
154
|
+
}, "autorixMiddleware");
|
|
155
|
+
}
|
|
156
|
+
__name(autorixExpress, "autorixExpress");
|
|
157
|
+
|
|
158
|
+
// src/context/resource.ts
|
|
159
|
+
async function resolveResource(spec, req) {
|
|
160
|
+
if (typeof spec === "string") return {
|
|
161
|
+
type: spec
|
|
162
|
+
};
|
|
163
|
+
if ("loader" in spec) {
|
|
164
|
+
const id = spec.idFrom(req);
|
|
165
|
+
const data = await spec.loader(id, req);
|
|
166
|
+
return {
|
|
167
|
+
type: spec.type,
|
|
168
|
+
id,
|
|
169
|
+
data
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return spec;
|
|
173
|
+
}
|
|
174
|
+
__name(resolveResource, "resolveResource");
|
|
175
|
+
|
|
176
|
+
// src/middleware/authorize.ts
|
|
177
|
+
function authorize(actionOrConfig, cfg) {
|
|
178
|
+
const config = typeof actionOrConfig === "string" ? {
|
|
179
|
+
action: actionOrConfig,
|
|
180
|
+
...cfg ?? {}
|
|
181
|
+
} : actionOrConfig;
|
|
182
|
+
return /* @__PURE__ */ __name(async function authorizeMiddleware(req, _res, next) {
|
|
183
|
+
try {
|
|
184
|
+
if (!req.autorix) throw new AutorixMissingMiddlewareError();
|
|
185
|
+
if (config.requireAuth && !req.autorix.context.principal) {
|
|
186
|
+
throw new AutorixUnauthenticatedError();
|
|
187
|
+
}
|
|
188
|
+
const ctxExtra = typeof config.context === "function" ? await config.context(req) : config.context ?? {};
|
|
189
|
+
let resource = void 0;
|
|
190
|
+
if (config.resource) {
|
|
191
|
+
resource = await resolveResource(config.resource, req);
|
|
192
|
+
req.autorix.context.resource = resource;
|
|
193
|
+
}
|
|
194
|
+
await req.autorix.enforce(config.action, resource, ctxExtra);
|
|
195
|
+
return next();
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return next(e);
|
|
198
|
+
}
|
|
199
|
+
}, "authorizeMiddleware");
|
|
200
|
+
}
|
|
201
|
+
__name(authorize, "authorize");
|
|
202
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
203
|
+
0 && (module.exports = {
|
|
204
|
+
AutorixForbiddenError,
|
|
205
|
+
AutorixHttpError,
|
|
206
|
+
AutorixMissingMiddlewareError,
|
|
207
|
+
AutorixUnauthenticatedError,
|
|
208
|
+
authorize,
|
|
209
|
+
autorixExpress,
|
|
210
|
+
buildRequestContext,
|
|
211
|
+
resolvePrincipal,
|
|
212
|
+
resolveResource
|
|
213
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
type Principal = {
|
|
4
|
+
id: string;
|
|
5
|
+
roles?: string[];
|
|
6
|
+
[k: string]: unknown;
|
|
7
|
+
} | null;
|
|
8
|
+
type AutorixRequestContext = {
|
|
9
|
+
principal: Principal;
|
|
10
|
+
tenantId?: string | null;
|
|
11
|
+
ip?: string;
|
|
12
|
+
userAgent?: string;
|
|
13
|
+
requestId?: string;
|
|
14
|
+
attributes?: Record<string, unknown>;
|
|
15
|
+
resource?: unknown;
|
|
16
|
+
};
|
|
17
|
+
type ResourceSpec = string | {
|
|
18
|
+
type: string;
|
|
19
|
+
id?: string;
|
|
20
|
+
data?: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
type: string;
|
|
23
|
+
idFrom: (req: Request) => string;
|
|
24
|
+
loader: (id: string, req: Request) => Promise<unknown>;
|
|
25
|
+
};
|
|
26
|
+
type GetContextFn = (req: Request) => Partial<AutorixRequestContext> | Promise<Partial<AutorixRequestContext>>;
|
|
27
|
+
type AutorixExpressOptions = {
|
|
28
|
+
enforcer: {
|
|
29
|
+
can: (input: {
|
|
30
|
+
action: string;
|
|
31
|
+
context: AutorixRequestContext;
|
|
32
|
+
resource?: unknown;
|
|
33
|
+
}) => Promise<{
|
|
34
|
+
allowed: boolean;
|
|
35
|
+
reason?: string;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
38
|
+
getPrincipal: (req: Request) => Principal | Promise<Principal>;
|
|
39
|
+
getTenant?: (req: Request) => string | null | Promise<string | null>;
|
|
40
|
+
getContext?: GetContextFn;
|
|
41
|
+
onDecision?: (d: {
|
|
42
|
+
allowed: boolean;
|
|
43
|
+
action: string;
|
|
44
|
+
reason?: string;
|
|
45
|
+
}, req: Request) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
declare global {
|
|
49
|
+
namespace Express {
|
|
50
|
+
interface Request {
|
|
51
|
+
autorix?: {
|
|
52
|
+
context: AutorixRequestContext;
|
|
53
|
+
can: (action: string, resource?: unknown, ctxExtra?: Record<string, unknown>) => Promise<boolean>;
|
|
54
|
+
enforce: (action: string, resource?: unknown, ctxExtra?: Record<string, unknown>) => Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
declare function autorixExpress(opts: AutorixExpressOptions): (req: Request, _res: Response, next: NextFunction) => Promise<void>;
|
|
60
|
+
|
|
61
|
+
type AuthorizeConfig = {
|
|
62
|
+
action: string;
|
|
63
|
+
resource?: ResourceSpec;
|
|
64
|
+
context?: Record<string, unknown> | ((req: Request) => Record<string, unknown> | Promise<Record<string, unknown>>);
|
|
65
|
+
requireAuth?: boolean;
|
|
66
|
+
};
|
|
67
|
+
declare function authorize(actionOrConfig: string | AuthorizeConfig, cfg?: Omit<AuthorizeConfig, "action">): (req: Request, _res: Response, next: NextFunction) => Promise<void>;
|
|
68
|
+
|
|
69
|
+
declare function buildRequestContext(req: Request, opts: AutorixExpressOptions): Promise<AutorixRequestContext>;
|
|
70
|
+
|
|
71
|
+
type GetPrincipalFn = (req: Request) => Principal | Promise<Principal>;
|
|
72
|
+
declare function resolvePrincipal(req: Request, getPrincipal: GetPrincipalFn): Promise<Principal>;
|
|
73
|
+
|
|
74
|
+
declare function resolveResource(spec: ResourceSpec, req: Request): Promise<unknown>;
|
|
75
|
+
|
|
76
|
+
type AutorixErrorCode = "AUTORIX_FORBIDDEN" | "AUTORIX_UNAUTHENTICATED" | "AUTORIX_MISSING_MIDDLEWARE" | "AUTORIX_INTERNAL";
|
|
77
|
+
declare class AutorixHttpError extends Error {
|
|
78
|
+
readonly statusCode: number;
|
|
79
|
+
readonly code: AutorixErrorCode;
|
|
80
|
+
readonly details?: unknown;
|
|
81
|
+
constructor(params: {
|
|
82
|
+
message: string;
|
|
83
|
+
statusCode: number;
|
|
84
|
+
code: AutorixErrorCode;
|
|
85
|
+
details?: unknown;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
declare class AutorixForbiddenError extends AutorixHttpError {
|
|
89
|
+
constructor(reason?: string, details?: unknown);
|
|
90
|
+
}
|
|
91
|
+
declare class AutorixUnauthenticatedError extends AutorixHttpError {
|
|
92
|
+
constructor(details?: unknown);
|
|
93
|
+
}
|
|
94
|
+
declare class AutorixMissingMiddlewareError extends AutorixHttpError {
|
|
95
|
+
constructor(details?: unknown);
|
|
96
|
+
}
|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
type Principal = {
|
|
4
|
+
id: string;
|
|
5
|
+
roles?: string[];
|
|
6
|
+
[k: string]: unknown;
|
|
7
|
+
} | null;
|
|
8
|
+
type AutorixRequestContext = {
|
|
9
|
+
principal: Principal;
|
|
10
|
+
tenantId?: string | null;
|
|
11
|
+
ip?: string;
|
|
12
|
+
userAgent?: string;
|
|
13
|
+
requestId?: string;
|
|
14
|
+
attributes?: Record<string, unknown>;
|
|
15
|
+
resource?: unknown;
|
|
16
|
+
};
|
|
17
|
+
type ResourceSpec = string | {
|
|
18
|
+
type: string;
|
|
19
|
+
id?: string;
|
|
20
|
+
data?: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
type: string;
|
|
23
|
+
idFrom: (req: Request) => string;
|
|
24
|
+
loader: (id: string, req: Request) => Promise<unknown>;
|
|
25
|
+
};
|
|
26
|
+
type GetContextFn = (req: Request) => Partial<AutorixRequestContext> | Promise<Partial<AutorixRequestContext>>;
|
|
27
|
+
type AutorixExpressOptions = {
|
|
28
|
+
enforcer: {
|
|
29
|
+
can: (input: {
|
|
30
|
+
action: string;
|
|
31
|
+
context: AutorixRequestContext;
|
|
32
|
+
resource?: unknown;
|
|
33
|
+
}) => Promise<{
|
|
34
|
+
allowed: boolean;
|
|
35
|
+
reason?: string;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
38
|
+
getPrincipal: (req: Request) => Principal | Promise<Principal>;
|
|
39
|
+
getTenant?: (req: Request) => string | null | Promise<string | null>;
|
|
40
|
+
getContext?: GetContextFn;
|
|
41
|
+
onDecision?: (d: {
|
|
42
|
+
allowed: boolean;
|
|
43
|
+
action: string;
|
|
44
|
+
reason?: string;
|
|
45
|
+
}, req: Request) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
declare global {
|
|
49
|
+
namespace Express {
|
|
50
|
+
interface Request {
|
|
51
|
+
autorix?: {
|
|
52
|
+
context: AutorixRequestContext;
|
|
53
|
+
can: (action: string, resource?: unknown, ctxExtra?: Record<string, unknown>) => Promise<boolean>;
|
|
54
|
+
enforce: (action: string, resource?: unknown, ctxExtra?: Record<string, unknown>) => Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
declare function autorixExpress(opts: AutorixExpressOptions): (req: Request, _res: Response, next: NextFunction) => Promise<void>;
|
|
60
|
+
|
|
61
|
+
type AuthorizeConfig = {
|
|
62
|
+
action: string;
|
|
63
|
+
resource?: ResourceSpec;
|
|
64
|
+
context?: Record<string, unknown> | ((req: Request) => Record<string, unknown> | Promise<Record<string, unknown>>);
|
|
65
|
+
requireAuth?: boolean;
|
|
66
|
+
};
|
|
67
|
+
declare function authorize(actionOrConfig: string | AuthorizeConfig, cfg?: Omit<AuthorizeConfig, "action">): (req: Request, _res: Response, next: NextFunction) => Promise<void>;
|
|
68
|
+
|
|
69
|
+
declare function buildRequestContext(req: Request, opts: AutorixExpressOptions): Promise<AutorixRequestContext>;
|
|
70
|
+
|
|
71
|
+
type GetPrincipalFn = (req: Request) => Principal | Promise<Principal>;
|
|
72
|
+
declare function resolvePrincipal(req: Request, getPrincipal: GetPrincipalFn): Promise<Principal>;
|
|
73
|
+
|
|
74
|
+
declare function resolveResource(spec: ResourceSpec, req: Request): Promise<unknown>;
|
|
75
|
+
|
|
76
|
+
type AutorixErrorCode = "AUTORIX_FORBIDDEN" | "AUTORIX_UNAUTHENTICATED" | "AUTORIX_MISSING_MIDDLEWARE" | "AUTORIX_INTERNAL";
|
|
77
|
+
declare class AutorixHttpError extends Error {
|
|
78
|
+
readonly statusCode: number;
|
|
79
|
+
readonly code: AutorixErrorCode;
|
|
80
|
+
readonly details?: unknown;
|
|
81
|
+
constructor(params: {
|
|
82
|
+
message: string;
|
|
83
|
+
statusCode: number;
|
|
84
|
+
code: AutorixErrorCode;
|
|
85
|
+
details?: unknown;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
declare class AutorixForbiddenError extends AutorixHttpError {
|
|
89
|
+
constructor(reason?: string, details?: unknown);
|
|
90
|
+
}
|
|
91
|
+
declare class AutorixUnauthenticatedError extends AutorixHttpError {
|
|
92
|
+
constructor(details?: unknown);
|
|
93
|
+
}
|
|
94
|
+
declare class AutorixMissingMiddlewareError extends AutorixHttpError {
|
|
95
|
+
constructor(details?: unknown);
|
|
96
|
+
}
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/context/principal.ts
|
|
5
|
+
async function resolvePrincipal(req, getPrincipal) {
|
|
6
|
+
return await getPrincipal(req);
|
|
7
|
+
}
|
|
8
|
+
__name(resolvePrincipal, "resolvePrincipal");
|
|
9
|
+
|
|
10
|
+
// src/context/buildRequestContext.ts
|
|
11
|
+
async function buildRequestContext(req, opts) {
|
|
12
|
+
const principal = await resolvePrincipal(req, opts.getPrincipal);
|
|
13
|
+
const tenantId = opts.getTenant ? await opts.getTenant(req) : null;
|
|
14
|
+
const extra = opts.getContext ? await opts.getContext(req) : {};
|
|
15
|
+
const context = {
|
|
16
|
+
principal,
|
|
17
|
+
tenantId: tenantId ?? void 0,
|
|
18
|
+
ip: req.ip,
|
|
19
|
+
userAgent: req.headers["user-agent"],
|
|
20
|
+
requestId: req.headers["x-request-id"],
|
|
21
|
+
attributes: extra?.attributes,
|
|
22
|
+
resource: void 0,
|
|
23
|
+
...extra
|
|
24
|
+
};
|
|
25
|
+
return context;
|
|
26
|
+
}
|
|
27
|
+
__name(buildRequestContext, "buildRequestContext");
|
|
28
|
+
|
|
29
|
+
// src/errors/AutorixHttpErrors.ts
|
|
30
|
+
var AutorixHttpError = class extends Error {
|
|
31
|
+
static {
|
|
32
|
+
__name(this, "AutorixHttpError");
|
|
33
|
+
}
|
|
34
|
+
statusCode;
|
|
35
|
+
code;
|
|
36
|
+
details;
|
|
37
|
+
constructor(params) {
|
|
38
|
+
super(params.message);
|
|
39
|
+
this.name = "AutorixHttpError";
|
|
40
|
+
this.statusCode = params.statusCode;
|
|
41
|
+
this.code = params.code;
|
|
42
|
+
this.details = params.details;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var AutorixForbiddenError = class extends AutorixHttpError {
|
|
46
|
+
static {
|
|
47
|
+
__name(this, "AutorixForbiddenError");
|
|
48
|
+
}
|
|
49
|
+
constructor(reason, details) {
|
|
50
|
+
super({
|
|
51
|
+
message: reason ?? "Forbidden",
|
|
52
|
+
statusCode: 403,
|
|
53
|
+
code: "AUTORIX_FORBIDDEN",
|
|
54
|
+
details
|
|
55
|
+
});
|
|
56
|
+
this.name = "AutorixForbiddenError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var AutorixUnauthenticatedError = class extends AutorixHttpError {
|
|
60
|
+
static {
|
|
61
|
+
__name(this, "AutorixUnauthenticatedError");
|
|
62
|
+
}
|
|
63
|
+
constructor(details) {
|
|
64
|
+
super({
|
|
65
|
+
message: "Unauthenticated",
|
|
66
|
+
statusCode: 401,
|
|
67
|
+
code: "AUTORIX_UNAUTHENTICATED",
|
|
68
|
+
details
|
|
69
|
+
});
|
|
70
|
+
this.name = "AutorixUnauthenticatedError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var AutorixMissingMiddlewareError = class extends AutorixHttpError {
|
|
74
|
+
static {
|
|
75
|
+
__name(this, "AutorixMissingMiddlewareError");
|
|
76
|
+
}
|
|
77
|
+
constructor(details) {
|
|
78
|
+
super({
|
|
79
|
+
message: "Autorix middleware not registered",
|
|
80
|
+
statusCode: 500,
|
|
81
|
+
code: "AUTORIX_MISSING_MIDDLEWARE",
|
|
82
|
+
details
|
|
83
|
+
});
|
|
84
|
+
this.name = "AutorixMissingMiddlewareError";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/middleware/autorixExpress.ts
|
|
89
|
+
function autorixExpress(opts) {
|
|
90
|
+
return /* @__PURE__ */ __name(async function autorixMiddleware(req, _res, next) {
|
|
91
|
+
try {
|
|
92
|
+
const context = await buildRequestContext(req, opts);
|
|
93
|
+
req.autorix = {
|
|
94
|
+
context,
|
|
95
|
+
can: /* @__PURE__ */ __name(async (action, resource, ctxExtra) => {
|
|
96
|
+
const decision = await opts.enforcer.can({
|
|
97
|
+
action,
|
|
98
|
+
context: {
|
|
99
|
+
...context,
|
|
100
|
+
attributes: {
|
|
101
|
+
...context.attributes ?? {},
|
|
102
|
+
...ctxExtra ?? {}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
resource
|
|
106
|
+
});
|
|
107
|
+
opts.onDecision?.({
|
|
108
|
+
...decision,
|
|
109
|
+
action
|
|
110
|
+
}, req);
|
|
111
|
+
return decision.allowed;
|
|
112
|
+
}, "can"),
|
|
113
|
+
enforce: /* @__PURE__ */ __name(async (action, resource, ctxExtra) => {
|
|
114
|
+
const allowed = await req.autorix.can(action, resource, ctxExtra);
|
|
115
|
+
if (!allowed) throw new AutorixForbiddenError();
|
|
116
|
+
}, "enforce")
|
|
117
|
+
};
|
|
118
|
+
return next();
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return next(e);
|
|
121
|
+
}
|
|
122
|
+
}, "autorixMiddleware");
|
|
123
|
+
}
|
|
124
|
+
__name(autorixExpress, "autorixExpress");
|
|
125
|
+
|
|
126
|
+
// src/context/resource.ts
|
|
127
|
+
async function resolveResource(spec, req) {
|
|
128
|
+
if (typeof spec === "string") return {
|
|
129
|
+
type: spec
|
|
130
|
+
};
|
|
131
|
+
if ("loader" in spec) {
|
|
132
|
+
const id = spec.idFrom(req);
|
|
133
|
+
const data = await spec.loader(id, req);
|
|
134
|
+
return {
|
|
135
|
+
type: spec.type,
|
|
136
|
+
id,
|
|
137
|
+
data
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return spec;
|
|
141
|
+
}
|
|
142
|
+
__name(resolveResource, "resolveResource");
|
|
143
|
+
|
|
144
|
+
// src/middleware/authorize.ts
|
|
145
|
+
function authorize(actionOrConfig, cfg) {
|
|
146
|
+
const config = typeof actionOrConfig === "string" ? {
|
|
147
|
+
action: actionOrConfig,
|
|
148
|
+
...cfg ?? {}
|
|
149
|
+
} : actionOrConfig;
|
|
150
|
+
return /* @__PURE__ */ __name(async function authorizeMiddleware(req, _res, next) {
|
|
151
|
+
try {
|
|
152
|
+
if (!req.autorix) throw new AutorixMissingMiddlewareError();
|
|
153
|
+
if (config.requireAuth && !req.autorix.context.principal) {
|
|
154
|
+
throw new AutorixUnauthenticatedError();
|
|
155
|
+
}
|
|
156
|
+
const ctxExtra = typeof config.context === "function" ? await config.context(req) : config.context ?? {};
|
|
157
|
+
let resource = void 0;
|
|
158
|
+
if (config.resource) {
|
|
159
|
+
resource = await resolveResource(config.resource, req);
|
|
160
|
+
req.autorix.context.resource = resource;
|
|
161
|
+
}
|
|
162
|
+
await req.autorix.enforce(config.action, resource, ctxExtra);
|
|
163
|
+
return next();
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return next(e);
|
|
166
|
+
}
|
|
167
|
+
}, "authorizeMiddleware");
|
|
168
|
+
}
|
|
169
|
+
__name(authorize, "authorize");
|
|
170
|
+
export {
|
|
171
|
+
AutorixForbiddenError,
|
|
172
|
+
AutorixHttpError,
|
|
173
|
+
AutorixMissingMiddlewareError,
|
|
174
|
+
AutorixUnauthenticatedError,
|
|
175
|
+
authorize,
|
|
176
|
+
autorixExpress,
|
|
177
|
+
buildRequestContext,
|
|
178
|
+
resolvePrincipal,
|
|
179
|
+
resolveResource
|
|
180
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@autorix/express",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Express.js integration for Autorix policy-based authorization (RBAC + ABAC)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"authorization",
|
|
7
|
+
"rbac",
|
|
8
|
+
"abac",
|
|
9
|
+
"iam",
|
|
10
|
+
"policy",
|
|
11
|
+
"express",
|
|
12
|
+
"autorix"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Chechooxd <sergiogalaz60@gmail.com>",
|
|
16
|
+
"homepage": "https://github.com/chechooxd/autorix",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/chechooxd/autorix.git",
|
|
20
|
+
"directory": "packages/autorix-express"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/chechooxd/autorix/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.cjs",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"require": "./dist/index.cjs"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
45
|
+
"test": "vitest -c ../../vitest.config.ts run",
|
|
46
|
+
"clean": "rm -rf dist"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@autorix/core": "^0.1.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"express": "^5.2.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/express": "^5.0.6",
|
|
56
|
+
"@types/supertest": "^6.0.3",
|
|
57
|
+
"express": "^5.2.1",
|
|
58
|
+
"supertest": "^7.2.2"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18"
|
|
62
|
+
}
|
|
63
|
+
}
|