@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 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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](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
+ });
@@ -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 };
@@ -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
+ }