@autorix/express 0.1.1 → 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 CHANGED
@@ -46,7 +46,7 @@ const policyProvider = new MemoryPolicyProvider();
46
46
 
47
47
  // Create enforcer
48
48
  const enforcer = {
49
- can: async (input: { action: string; context: any; resource?: unknown }) => {
49
+ can: async (input: { action: string; resource: string; context: any }) => {
50
50
  // ✅ IMPORTANT: If your app allows unauthenticated requests, guard here
51
51
  // to avoid storage/providers crashing when principal is null.
52
52
  if (!input.context?.principal) {
@@ -62,8 +62,8 @@ const enforcer = {
62
62
  const result = evaluateAll({
63
63
  policies: policies.map(p => p.document),
64
64
  action: input.action,
65
- resource: input.resource,
66
- context: input.context,
65
+ resource: input.resource, // ← String for matching (e.g., 'post/123')
66
+ ctx: input.context, // ← Contains resource object in ctx.resource
67
67
  });
68
68
 
69
69
  return { allowed: result.allowed, reason: result.reason };
@@ -209,6 +209,49 @@ authorize({
209
209
  })
210
210
  ```
211
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
+
212
255
  ## 🎯 Usage Examples
213
256
 
214
257
  ### Role-Based Access Control (RBAC)
@@ -253,16 +296,16 @@ app.get('/admin/settings',
253
296
  ### Attribute-Based Access Control (ABAC)
254
297
 
255
298
  ```typescript
256
- // Policy with conditions
299
+ // Policy with conditions - checks resource attributes
257
300
  await provider.setPolicy('owner-only', {
258
301
  Version: '2024-01-01',
259
302
  Statement: [{
260
303
  Effect: 'Allow',
261
304
  Action: ['post:update', 'post:delete'],
262
- Resource: ['post/*'],
305
+ Resource: ['post/*'], // ← Matches 'post/123' string pattern
263
306
  Condition: {
264
307
  StringEquals: {
265
- 'principal.id': '${resource.ownerId}'
308
+ 'resource.authorId': '${principal.id}' // ← Checks resource object property
266
309
  }
267
310
  }
268
311
  }]
@@ -277,12 +320,16 @@ app.put('/posts/:id',
277
320
  idFrom: (req) => req.params.id,
278
321
  loader: async (id) => {
279
322
  const post = await db.posts.findById(id);
280
- return { ...post, ownerId: post.userId };
323
+ // Return object with properties for conditions
324
+ return {
325
+ authorId: post.authorId, // ← This becomes resource.authorId in conditions
326
+ status: post.status
327
+ };
281
328
  }
282
329
  }
283
330
  }),
284
331
  (req, res) => {
285
- // Only post owner can update
332
+ // Only post author can update
286
333
  res.json({ message: 'Post updated!' });
287
334
  }
288
335
  );
package/dist/index.cjs CHANGED
@@ -125,17 +125,31 @@ function autorixExpress(opts) {
125
125
  const context = await buildRequestContext(req, opts);
126
126
  req.autorix = {
127
127
  context,
128
- can: /* @__PURE__ */ __name(async (action, resource, ctxExtra) => {
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
+ }
129
142
  const decision = await opts.enforcer.can({
130
143
  action,
144
+ resource: resourceString,
131
145
  context: {
132
146
  ...context,
147
+ resource: resourceObj,
133
148
  attributes: {
134
149
  ...context.attributes ?? {},
135
150
  ...ctxExtra ?? {}
136
151
  }
137
- },
138
- resource
152
+ }
139
153
  });
140
154
  opts.onDecision?.({
141
155
  ...decision,
@@ -161,9 +175,16 @@ async function resolveResource(spec, req) {
161
175
  if (typeof spec === "string") return {
162
176
  type: spec
163
177
  };
164
- if ("loader" in spec) {
178
+ if ("loader" in spec && typeof spec.loader === "function" && typeof spec.idFrom === "function") {
165
179
  const id = spec.idFrom(req);
166
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
+ }
167
188
  return {
168
189
  type: spec.type,
169
190
  id,
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
- data?: unknown;
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;
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
- data?: unknown;
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;
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, resource, ctxExtra) => {
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autorix/express",
3
- "version": "0.1.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",
@@ -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"