odgn-rights 0.1.0 → 0.5.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.
Files changed (53) hide show
  1. package/README.md +489 -0
  2. package/dist/adapters/base-adapter.d.ts +83 -0
  3. package/dist/adapters/base-adapter.js +142 -0
  4. package/dist/adapters/factories.d.ts +31 -0
  5. package/dist/adapters/factories.js +48 -0
  6. package/dist/adapters/index.d.ts +11 -0
  7. package/dist/adapters/index.js +12 -0
  8. package/dist/adapters/postgres-adapter.d.ts +51 -0
  9. package/dist/adapters/postgres-adapter.js +469 -0
  10. package/dist/adapters/redis-adapter.d.ts +84 -0
  11. package/dist/adapters/redis-adapter.js +673 -0
  12. package/dist/adapters/schema.d.ts +25 -0
  13. package/dist/adapters/schema.js +186 -0
  14. package/dist/adapters/sqlite-adapter.d.ts +78 -0
  15. package/dist/adapters/sqlite-adapter.js +655 -0
  16. package/dist/adapters/types.d.ts +174 -0
  17. package/dist/adapters/types.js +1 -0
  18. package/dist/cli/commands/check.d.ts +2 -0
  19. package/dist/cli/commands/check.js +38 -0
  20. package/dist/cli/commands/explain.d.ts +2 -0
  21. package/dist/cli/commands/explain.js +93 -0
  22. package/dist/cli/commands/validate.d.ts +2 -0
  23. package/dist/cli/commands/validate.js +177 -0
  24. package/dist/cli/helpers/config-loader.d.ts +3 -0
  25. package/dist/cli/helpers/config-loader.js +13 -0
  26. package/dist/cli/helpers/flag-parser.d.ts +3 -0
  27. package/dist/cli/helpers/flag-parser.js +40 -0
  28. package/dist/cli/helpers/output.d.ts +10 -0
  29. package/dist/cli/helpers/output.js +29 -0
  30. package/dist/cli/index.d.ts +2 -0
  31. package/dist/cli/index.js +15 -0
  32. package/dist/cli/types.d.ts +10 -0
  33. package/dist/cli/types.js +1 -0
  34. package/dist/helpers.d.ts +16 -0
  35. package/dist/{utils.js → helpers.js} +22 -0
  36. package/dist/index.d.ts +3 -1
  37. package/dist/index.js +3 -1
  38. package/dist/integrations/elysia.d.ts +235 -0
  39. package/dist/integrations/elysia.js +375 -0
  40. package/dist/right.d.ts +25 -1
  41. package/dist/right.js +183 -19
  42. package/dist/rights.d.ts +31 -0
  43. package/dist/rights.js +162 -31
  44. package/dist/role-registry.d.ts +9 -0
  45. package/dist/role-registry.js +15 -0
  46. package/dist/role.d.ts +3 -1
  47. package/dist/role.js +11 -0
  48. package/dist/subject-registry.d.ts +77 -0
  49. package/dist/subject-registry.js +123 -0
  50. package/dist/subject.d.ts +21 -2
  51. package/dist/subject.js +51 -8
  52. package/package.json +63 -7
  53. package/dist/utils.d.ts +0 -2
@@ -1,4 +1,26 @@
1
1
  import { Flags, hasBit } from './constants';
2
+ /**
3
+ * Parse a path string, detecting and stripping negation prefix.
4
+ * Handles double negation (!! becomes positive).
5
+ *
6
+ * @example
7
+ * parsePath('!/api/internal') // { path: '/api/internal', negated: true }
8
+ * parsePath('/api/public') // { path: '/api/public', negated: false }
9
+ * parsePath('!!/api/path') // { path: '/api/path', negated: false }
10
+ */
11
+ export const parsePath = (p) => {
12
+ let trimmed = p.trim();
13
+ let negated = false;
14
+ // Handle negation prefix(es) - !! cancels out
15
+ while (trimmed.startsWith('!')) {
16
+ negated = !negated;
17
+ trimmed = trimmed.slice(1).trimStart();
18
+ }
19
+ return {
20
+ negated,
21
+ path: normalizePath(trimmed)
22
+ };
23
+ };
2
24
  export const normalizePath = (p) => {
3
25
  if (!p) {
4
26
  return '/';
package/dist/index.d.ts CHANGED
@@ -4,4 +4,6 @@ export * from './rights';
4
4
  export * from './role';
5
5
  export * from './role-registry';
6
6
  export * from './subject';
7
- export * from './utils';
7
+ export * from './subject-registry';
8
+ export * from './helpers';
9
+ export * from './adapters';
package/dist/index.js CHANGED
@@ -4,4 +4,6 @@ export * from './rights';
4
4
  export * from './role';
5
5
  export * from './role-registry';
6
6
  export * from './subject';
7
- export * from './utils';
7
+ export * from './subject-registry';
8
+ export * from './helpers';
9
+ export * from './adapters';
@@ -0,0 +1,235 @@
1
+ /**
2
+ * ElysiaJS integration for odgn-rights
3
+ *
4
+ * @module odgn-rights/integrations/elysia
5
+ *
6
+ * This module provides middleware and utilities for integrating
7
+ * odgn-rights with ElysiaJS applications.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { Elysia } from 'elysia';
12
+ * import { elysiaRights } from 'odgn-rights/integrations/elysia';
13
+ *
14
+ * const app = new Elysia()
15
+ * .use(elysiaRights({
16
+ * registry: subjectRegistry,
17
+ * getSubject: ({ headers }) => headers.get('x-user-id')
18
+ * }))
19
+ * .get('/users', () => 'list users')
20
+ * .listen(3000);
21
+ * ```
22
+ */
23
+ import type { Flags } from '../constants';
24
+ import type { ConditionContext } from '../right';
25
+ import type { Rights } from '../rights';
26
+ import type { Subject } from '../subject';
27
+ import type { SubjectRegistry } from '../subject-registry';
28
+ /**
29
+ * Configuration options for the Elysia rights middleware
30
+ */
31
+ export type ElysiaRightsOptions<Store = unknown, Derive = unknown> = {
32
+ /**
33
+ * Function to determine the required flags based on the request.
34
+ * Defaults to mapping HTTP methods to flags:
35
+ * - GET, HEAD, OPTIONS -> READ
36
+ * - POST -> CREATE
37
+ * - PUT, PATCH -> WRITE
38
+ * - DELETE -> DELETE
39
+ */
40
+ flagMapper?: (ctx: {
41
+ method: string;
42
+ path: string;
43
+ request: Request;
44
+ }) => Flags;
45
+ /**
46
+ * Function to build the condition context for ABAC-style checks.
47
+ * The returned context is passed to the permission check.
48
+ */
49
+ getContext?: (ctx: {
50
+ headers: Headers;
51
+ path: string;
52
+ request: Request;
53
+ store: Store;
54
+ subject: Subject;
55
+ } & Derive) => ConditionContext | Promise<ConditionContext>;
56
+ /**
57
+ * Function to extract the subject or subject identifier from the request context.
58
+ * If registry is provided, this should return a string identifier.
59
+ * Otherwise, it should return a Subject instance directly.
60
+ */
61
+ getSubject: (ctx: {
62
+ headers: Headers;
63
+ path: string;
64
+ request: Request;
65
+ store: Store;
66
+ } & Derive) => string | Subject | undefined | null | Promise<string | Subject | undefined | null>;
67
+ /**
68
+ * Custom handler when no subject is found.
69
+ * Defaults to returning 401 status.
70
+ */
71
+ onNoSubject?: (ctx: {
72
+ path: string;
73
+ request: Request;
74
+ }) => Response | Promise<Response>;
75
+ /**
76
+ * Custom handler for unauthorized responses.
77
+ * Defaults to returning 403 status.
78
+ */
79
+ onUnauthorized?: (ctx: {
80
+ path: string;
81
+ request: Request;
82
+ requiredFlags: Flags;
83
+ subject?: Subject;
84
+ }) => Response | Promise<Response>;
85
+ /**
86
+ * Function to map the request path to a rights path.
87
+ * Useful for stripping prefixes or transforming paths.
88
+ * Defaults to using the request path as-is.
89
+ */
90
+ pathMapper?: (ctx: {
91
+ method: string;
92
+ path: string;
93
+ request: Request;
94
+ }) => string;
95
+ /**
96
+ * The SubjectRegistry to look up subjects from.
97
+ * If provided, getSubject should return the subject identifier.
98
+ */
99
+ registry?: SubjectRegistry;
100
+ };
101
+ /**
102
+ * Options for standalone rights-based checks (without subjects)
103
+ */
104
+ export type ElysiaRightsStandaloneOptions<Store = unknown, Derive = unknown> = Omit<ElysiaRightsOptions<Store, Derive>, 'registry' | 'getSubject'> & {
105
+ /**
106
+ * A Rights instance to check against directly.
107
+ * Use this when you don't need subject/role-based access control.
108
+ */
109
+ rights: Rights;
110
+ };
111
+ type ElysiaContext = {
112
+ [key: string]: unknown;
113
+ path: string;
114
+ request: Request;
115
+ set: {
116
+ status?: number | string;
117
+ };
118
+ };
119
+ /**
120
+ * Create an Elysia plugin for rights-based authorization.
121
+ *
122
+ * @example Using with SubjectRegistry
123
+ * ```typescript
124
+ * import { Elysia } from 'elysia';
125
+ * import { elysiaRights } from 'odgn-rights/integrations/elysia';
126
+ *
127
+ * const app = new Elysia()
128
+ * .use(elysiaRights({
129
+ * registry: subjectRegistry,
130
+ * getSubject: ({ headers }) => headers.get('x-user-id')
131
+ * }))
132
+ * .get('/users', () => 'list users')
133
+ * .listen(3000);
134
+ * ```
135
+ *
136
+ * @example Using with direct Subject
137
+ * ```typescript
138
+ * const app = new Elysia()
139
+ * .derive(({ headers }) => ({
140
+ * user: getUserFromToken(headers.get('authorization'))
141
+ * }))
142
+ * .use(elysiaRights({
143
+ * getSubject: ({ user }) => user?.subject
144
+ * }))
145
+ * .get('/users', () => 'list users')
146
+ * .listen(3000);
147
+ * ```
148
+ */
149
+ export declare const elysiaRights: <Store = unknown, Derive = unknown>(options: ElysiaRightsOptions<Store, Derive>) => any;
150
+ /**
151
+ * Create an Elysia plugin for rights-based authorization using a Rights instance directly.
152
+ * Use this when you don't need subject/role-based access control.
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * import { Elysia } from 'elysia';
157
+ * import { elysiaRightsStandalone } from 'odgn-rights/integrations/elysia';
158
+ * import { Rights, Flags } from 'odgn-rights';
159
+ *
160
+ * const rights = new Rights();
161
+ * rights.allow('/api/**', Flags.READ);
162
+ * rights.allow('/api/admin/**', Flags.ALL);
163
+ *
164
+ * const app = new Elysia()
165
+ * .use(elysiaRightsStandalone({ rights }))
166
+ * .get('/api/users', () => 'list users')
167
+ * .listen(3000);
168
+ * ```
169
+ */
170
+ export declare const elysiaRightsStandalone: <Store = unknown, Derive = unknown>(options: ElysiaRightsStandaloneOptions<Store, Derive>) => any;
171
+ /**
172
+ * Type for beforeHandle guard configuration
173
+ */
174
+ export type RightsGuardConfig = {
175
+ beforeHandle: (ctx: ElysiaContext) => Promise<Response | object | void>;
176
+ };
177
+ /**
178
+ * Create a guard configuration for use with Elysia's .guard() method.
179
+ * This allows more fine-grained control over which routes are protected.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * import { Elysia } from 'elysia';
184
+ * import { createRightsGuard } from 'odgn-rights/integrations/elysia';
185
+ *
186
+ * const guard = createRightsGuard({
187
+ * registry: subjectRegistry,
188
+ * getSubject: ({ headers }) => headers.get('x-user-id')
189
+ * });
190
+ *
191
+ * const app = new Elysia()
192
+ * .get('/public', () => 'anyone can see this')
193
+ * .guard(guard, (app) =>
194
+ * app
195
+ * .get('/protected', () => 'only authorized users')
196
+ * .post('/protected', () => 'create something')
197
+ * )
198
+ * .listen(3000);
199
+ * ```
200
+ */
201
+ export declare const createRightsGuard: <Store = unknown, Derive = unknown>(options: ElysiaRightsOptions<Store, Derive>) => {
202
+ beforeHandle({ path, request, set, ...rest }: ElysiaContext): Promise<Response | {
203
+ error: string;
204
+ message: string;
205
+ } | undefined>;
206
+ };
207
+ /**
208
+ * Create a macro for declarative per-route authorization.
209
+ * This allows you to specify rights requirements directly on route definitions.
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * import { Elysia } from 'elysia';
214
+ * import { createRightsMacro } from 'odgn-rights/integrations/elysia';
215
+ * import { Flags } from 'odgn-rights';
216
+ *
217
+ * const rightsMacro = createRightsMacro({
218
+ * registry: subjectRegistry,
219
+ * getSubject: ({ headers }) => headers.get('x-user-id')
220
+ * });
221
+ *
222
+ * const app = new Elysia()
223
+ * .use(rightsMacro)
224
+ * .get('/public', () => 'anyone')
225
+ * .get('/users', () => 'list users', {
226
+ * rights: { path: '/users', flags: Flags.READ }
227
+ * })
228
+ * .delete('/users/:id', () => 'delete user', {
229
+ * rights: { path: '/users/*', flags: Flags.DELETE }
230
+ * })
231
+ * .listen(3000);
232
+ * ```
233
+ */
234
+ export declare const createRightsMacro: <Store = unknown, Derive = unknown>(options: ElysiaRightsOptions<Store, Derive>) => any;
235
+ export {};
@@ -0,0 +1,375 @@
1
+ /**
2
+ * ElysiaJS integration for odgn-rights
3
+ *
4
+ * @module odgn-rights/integrations/elysia
5
+ *
6
+ * This module provides middleware and utilities for integrating
7
+ * odgn-rights with ElysiaJS applications.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { Elysia } from 'elysia';
12
+ * import { elysiaRights } from 'odgn-rights/integrations/elysia';
13
+ *
14
+ * const app = new Elysia()
15
+ * .use(elysiaRights({
16
+ * registry: subjectRegistry,
17
+ * getSubject: ({ headers }) => headers.get('x-user-id')
18
+ * }))
19
+ * .get('/users', () => 'list users')
20
+ * .listen(3000);
21
+ * ```
22
+ */
23
+ /**
24
+ * HTTP method to Flags mapping
25
+ */
26
+ const DEFAULT_METHOD_FLAG_MAP = {
27
+ DELETE: 4, // DELETE
28
+ GET: 1, // READ
29
+ HEAD: 1,
30
+ OPTIONS: 1,
31
+ PATCH: 2,
32
+ POST: 8, // CREATE
33
+ PUT: 2 // WRITE
34
+ };
35
+ /**
36
+ * Create an Elysia plugin for rights-based authorization.
37
+ *
38
+ * @example Using with SubjectRegistry
39
+ * ```typescript
40
+ * import { Elysia } from 'elysia';
41
+ * import { elysiaRights } from 'odgn-rights/integrations/elysia';
42
+ *
43
+ * const app = new Elysia()
44
+ * .use(elysiaRights({
45
+ * registry: subjectRegistry,
46
+ * getSubject: ({ headers }) => headers.get('x-user-id')
47
+ * }))
48
+ * .get('/users', () => 'list users')
49
+ * .listen(3000);
50
+ * ```
51
+ *
52
+ * @example Using with direct Subject
53
+ * ```typescript
54
+ * const app = new Elysia()
55
+ * .derive(({ headers }) => ({
56
+ * user: getUserFromToken(headers.get('authorization'))
57
+ * }))
58
+ * .use(elysiaRights({
59
+ * getSubject: ({ user }) => user?.subject
60
+ * }))
61
+ * .get('/users', () => 'list users')
62
+ * .listen(3000);
63
+ * ```
64
+ */
65
+ export const elysiaRights = (options) => {
66
+ // Dynamic import to avoid requiring elysia as a hard dependency
67
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
68
+ const { Elysia } = require('elysia');
69
+ const { flagMapper, getContext, getSubject, onNoSubject, onUnauthorized, pathMapper, registry } = options;
70
+ return new Elysia({ name: 'odgn-rights' }).onBeforeHandle({ as: 'scoped' }, async ({ path, request, set, ...rest }) => {
71
+ const method = request.method;
72
+ // Get the subject
73
+ const subjectOrId = await getSubject({
74
+ headers: request.headers,
75
+ path,
76
+ request,
77
+ ...rest
78
+ });
79
+ let subject;
80
+ if (subjectOrId === null || subjectOrId === undefined) {
81
+ if (onNoSubject) {
82
+ return onNoSubject({ path, request });
83
+ }
84
+ set.status = 401;
85
+ return { error: 'Unauthorized', message: 'No subject found' };
86
+ }
87
+ if (typeof subjectOrId === 'string') {
88
+ if (!registry) {
89
+ throw new Error('elysiaRights: registry is required when getSubject returns a string identifier');
90
+ }
91
+ subject = registry.get(subjectOrId);
92
+ if (!subject) {
93
+ if (onNoSubject) {
94
+ return onNoSubject({ path, request });
95
+ }
96
+ set.status = 401;
97
+ return { error: 'Unauthorized', message: 'Subject not found' };
98
+ }
99
+ }
100
+ else {
101
+ subject = subjectOrId;
102
+ }
103
+ // Map the path
104
+ const rightsPath = pathMapper
105
+ ? pathMapper({ method, path, request })
106
+ : path;
107
+ // Get the required flags
108
+ const requiredFlags = flagMapper
109
+ ? flagMapper({ method, path, request })
110
+ : (DEFAULT_METHOD_FLAG_MAP[method] ?? 1); // Default to READ
111
+ // Get condition context if provided
112
+ const context = getContext
113
+ ? await getContext({
114
+ headers: request.headers,
115
+ path,
116
+ request,
117
+ subject,
118
+ ...rest
119
+ })
120
+ : undefined;
121
+ // Check permission
122
+ const allowed = subject.has(rightsPath, requiredFlags, context);
123
+ if (!allowed) {
124
+ if (onUnauthorized) {
125
+ return onUnauthorized({
126
+ path: rightsPath,
127
+ request,
128
+ requiredFlags,
129
+ subject
130
+ });
131
+ }
132
+ set.status = 403;
133
+ return { error: 'Forbidden', message: 'Access denied' };
134
+ }
135
+ // Permission granted, continue to handler
136
+ });
137
+ };
138
+ /**
139
+ * Create an Elysia plugin for rights-based authorization using a Rights instance directly.
140
+ * Use this when you don't need subject/role-based access control.
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * import { Elysia } from 'elysia';
145
+ * import { elysiaRightsStandalone } from 'odgn-rights/integrations/elysia';
146
+ * import { Rights, Flags } from 'odgn-rights';
147
+ *
148
+ * const rights = new Rights();
149
+ * rights.allow('/api/**', Flags.READ);
150
+ * rights.allow('/api/admin/**', Flags.ALL);
151
+ *
152
+ * const app = new Elysia()
153
+ * .use(elysiaRightsStandalone({ rights }))
154
+ * .get('/api/users', () => 'list users')
155
+ * .listen(3000);
156
+ * ```
157
+ */
158
+ export const elysiaRightsStandalone = (options) => {
159
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
160
+ const { Elysia } = require('elysia');
161
+ const { flagMapper, getContext, onUnauthorized, pathMapper, rights } = options;
162
+ return new Elysia({ name: 'odgn-rights-standalone' }).onBeforeHandle({ as: 'scoped' }, async ({ path, request, set, ...rest }) => {
163
+ const method = request.method;
164
+ // Map the path
165
+ const rightsPath = pathMapper
166
+ ? pathMapper({ method, path, request })
167
+ : path;
168
+ // Get the required flags
169
+ const requiredFlags = flagMapper
170
+ ? flagMapper({ method, path, request })
171
+ : (DEFAULT_METHOD_FLAG_MAP[method] ?? 1);
172
+ // Get condition context if provided
173
+ const context = getContext
174
+ ? await getContext({
175
+ headers: request.headers,
176
+ path,
177
+ request,
178
+ subject: undefined,
179
+ ...rest
180
+ })
181
+ : undefined;
182
+ // Check permission
183
+ const allowed = rights.has(rightsPath, requiredFlags, context);
184
+ if (!allowed) {
185
+ if (onUnauthorized) {
186
+ return onUnauthorized({
187
+ path: rightsPath,
188
+ request,
189
+ requiredFlags,
190
+ subject: undefined
191
+ });
192
+ }
193
+ set.status = 403;
194
+ return { error: 'Forbidden', message: 'Access denied' };
195
+ }
196
+ });
197
+ };
198
+ /**
199
+ * Create a guard configuration for use with Elysia's .guard() method.
200
+ * This allows more fine-grained control over which routes are protected.
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * import { Elysia } from 'elysia';
205
+ * import { createRightsGuard } from 'odgn-rights/integrations/elysia';
206
+ *
207
+ * const guard = createRightsGuard({
208
+ * registry: subjectRegistry,
209
+ * getSubject: ({ headers }) => headers.get('x-user-id')
210
+ * });
211
+ *
212
+ * const app = new Elysia()
213
+ * .get('/public', () => 'anyone can see this')
214
+ * .guard(guard, (app) =>
215
+ * app
216
+ * .get('/protected', () => 'only authorized users')
217
+ * .post('/protected', () => 'create something')
218
+ * )
219
+ * .listen(3000);
220
+ * ```
221
+ */
222
+ export const createRightsGuard = (options) => {
223
+ const { flagMapper, getContext, getSubject, onNoSubject, onUnauthorized, pathMapper, registry } = options;
224
+ return {
225
+ async beforeHandle({ path, request, set, ...rest }) {
226
+ const method = request.method;
227
+ const subjectOrId = await getSubject({
228
+ headers: request.headers,
229
+ path,
230
+ request,
231
+ ...rest
232
+ });
233
+ let subject;
234
+ if (subjectOrId === null || subjectOrId === undefined) {
235
+ if (onNoSubject) {
236
+ return onNoSubject({ path, request });
237
+ }
238
+ set.status = 401;
239
+ return { error: 'Unauthorized', message: 'No subject found' };
240
+ }
241
+ if (typeof subjectOrId === 'string') {
242
+ if (!registry) {
243
+ throw new Error('createRightsGuard: registry is required when getSubject returns a string identifier');
244
+ }
245
+ subject = registry.get(subjectOrId);
246
+ if (!subject) {
247
+ if (onNoSubject) {
248
+ return onNoSubject({ path, request });
249
+ }
250
+ set.status = 401;
251
+ return { error: 'Unauthorized', message: 'Subject not found' };
252
+ }
253
+ }
254
+ else {
255
+ subject = subjectOrId;
256
+ }
257
+ const rightsPath = pathMapper
258
+ ? pathMapper({ method, path, request })
259
+ : path;
260
+ const requiredFlags = flagMapper
261
+ ? flagMapper({ method, path, request })
262
+ : (DEFAULT_METHOD_FLAG_MAP[method] ?? 1);
263
+ const context = getContext
264
+ ? await getContext({
265
+ headers: request.headers,
266
+ path,
267
+ request,
268
+ subject,
269
+ ...rest
270
+ })
271
+ : undefined;
272
+ const allowed = subject.has(rightsPath, requiredFlags, context);
273
+ if (!allowed) {
274
+ if (onUnauthorized) {
275
+ return onUnauthorized({
276
+ path: rightsPath,
277
+ request,
278
+ requiredFlags,
279
+ subject
280
+ });
281
+ }
282
+ set.status = 403;
283
+ return { error: 'Forbidden', message: 'Access denied' };
284
+ }
285
+ }
286
+ };
287
+ };
288
+ /**
289
+ * Create a macro for declarative per-route authorization.
290
+ * This allows you to specify rights requirements directly on route definitions.
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * import { Elysia } from 'elysia';
295
+ * import { createRightsMacro } from 'odgn-rights/integrations/elysia';
296
+ * import { Flags } from 'odgn-rights';
297
+ *
298
+ * const rightsMacro = createRightsMacro({
299
+ * registry: subjectRegistry,
300
+ * getSubject: ({ headers }) => headers.get('x-user-id')
301
+ * });
302
+ *
303
+ * const app = new Elysia()
304
+ * .use(rightsMacro)
305
+ * .get('/public', () => 'anyone')
306
+ * .get('/users', () => 'list users', {
307
+ * rights: { path: '/users', flags: Flags.READ }
308
+ * })
309
+ * .delete('/users/:id', () => 'delete user', {
310
+ * rights: { path: '/users/*', flags: Flags.DELETE }
311
+ * })
312
+ * .listen(3000);
313
+ * ```
314
+ */
315
+ export const createRightsMacro = (options) => {
316
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
317
+ const { Elysia } = require('elysia');
318
+ const { getContext, getSubject, onNoSubject, onUnauthorized, registry } = options;
319
+ return new Elysia({ name: 'odgn-rights-macro' }).macro({
320
+ rights: (rightsConfig) => ({
321
+ async resolve({ path, request, set, status, ...rest }) {
322
+ const subjectOrId = await getSubject({
323
+ headers: request.headers,
324
+ path,
325
+ request,
326
+ ...rest
327
+ });
328
+ let subject;
329
+ if (subjectOrId === null || subjectOrId === undefined) {
330
+ if (onNoSubject) {
331
+ return onNoSubject({ path, request });
332
+ }
333
+ return status(401);
334
+ }
335
+ if (typeof subjectOrId === 'string') {
336
+ if (!registry) {
337
+ throw new Error('createRightsMacro: registry is required when getSubject returns a string identifier');
338
+ }
339
+ subject = registry.get(subjectOrId);
340
+ if (!subject) {
341
+ if (onNoSubject) {
342
+ return onNoSubject({ path, request });
343
+ }
344
+ return status(401);
345
+ }
346
+ }
347
+ else {
348
+ subject = subjectOrId;
349
+ }
350
+ const context = getContext
351
+ ? await getContext({
352
+ headers: request.headers,
353
+ path,
354
+ request,
355
+ subject,
356
+ ...rest
357
+ })
358
+ : undefined;
359
+ const allowed = subject.has(rightsConfig.path, rightsConfig.flags, context);
360
+ if (!allowed) {
361
+ if (onUnauthorized) {
362
+ return onUnauthorized({
363
+ path: rightsConfig.path,
364
+ request,
365
+ requiredFlags: rightsConfig.flags,
366
+ subject
367
+ });
368
+ }
369
+ return status(403);
370
+ }
371
+ return { subject };
372
+ }
373
+ })
374
+ });
375
+ };