odgn-rights 0.5.1 → 0.6.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/dist/index.d.ts CHANGED
@@ -6,4 +6,3 @@ export * from './role-registry';
6
6
  export * from './subject';
7
7
  export * from './subject-registry';
8
8
  export * from './helpers';
9
- export * from './adapters';
package/dist/index.js CHANGED
@@ -6,4 +6,5 @@ export * from './role-registry';
6
6
  export * from './subject';
7
7
  export * from './subject-registry';
8
8
  export * from './helpers';
9
- export * from './adapters';
9
+ // Note: Adapters are not exported from main entry point to keep it browser-compatible.
10
+ // Use subpath imports: 'odgn-rights/adapters', 'odgn-rights/adapters/sqlite', etc.
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "odgn-rights",
3
- "version": "0.5.1",
4
- "description": "Tiny TypeScript library for expressing and evaluating hierarchical rights with simple glob patterns.",
3
+ "version": "0.6.0",
4
+ "description": "TypeScript library for expressing and evaluating hierarchical rights with simple glob patterns.",
5
5
  "keywords": [
6
6
  "rights",
7
7
  "permissions",
8
- "glob",
9
8
  "typescript"
10
9
  ],
11
10
  "author": "Alex Veenendaal",
@@ -55,35 +54,18 @@
55
54
  ],
56
55
  "type": "module",
57
56
  "scripts": {
58
- "format": "prettier --write .",
59
- "format:check": "prettier --check .",
60
- "lint": "eslint --cache .",
61
- "lint:fix": "eslint --fix .",
62
- "outdated": "bunx npm-check-updates --interactive --format group",
63
- "test": "bun test src",
64
- "test:coverage": "bun test --coverage src",
65
- "test:e2e": "playwright test",
66
- "test:e2e:ui": "playwright test --ui",
67
- "unused": "knip",
68
57
  "clean": "rm -rf dist",
69
58
  "build": "tsc -p tsconfig.build.json",
70
- "playground": "bun ./playground/index.html",
71
- "playground:build": "bun run ./playground/build.ts",
72
- "prepublishOnly": "bun run clean && bun run build && bun run test && bun run lint:fix && bun run format:check"
59
+ "prepublishOnly": "bun run clean && bun run build"
60
+ },
61
+ "dependencies": {
62
+ "commander": "^14.0.2",
63
+ "ioredis": "^5.9.0"
73
64
  },
74
65
  "devDependencies": {
75
- "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
76
- "@nkzw/eslint-config": "^3.3.0",
77
- "@playwright/test": "^1.57.0",
78
66
  "@testcontainers/postgresql": "^11.11.0",
79
67
  "@testcontainers/valkey": "^11.11.0",
80
- "@types/bun": "latest",
81
- "@types/react": "^19.2.7",
82
- "@types/react-dom": "^19.2.3",
83
68
  "elysia": "^1.4.21",
84
- "eslint": "^9",
85
- "knip": "^5.80.0",
86
- "prettier": "^3",
87
69
  "testcontainers": "^11.11.0"
88
70
  },
89
71
  "peerDependencies": {
@@ -98,16 +80,5 @@
98
80
  "publishConfig": {
99
81
  "access": "public"
100
82
  },
101
- "sideEffects": false,
102
- "dependencies": {
103
- "commander": "^14.0.2",
104
- "ioredis": "^5.9.0",
105
- "jotai": "^2.16.1",
106
- "react": "^19.2.3",
107
- "react-dom": "^19.2.3"
108
- },
109
- "trustedDependencies": [
110
- "protobufjs",
111
- "unrs-resolver"
112
- ]
113
- }
83
+ "sideEffects": false
84
+ }
package/README.md DELETED
@@ -1,596 +0,0 @@
1
- # ODGN Rights
2
-
3
- Tiny TypeScript library for expressing and evaluating hierarchical rights with simple glob patterns.
4
-
5
- ## Install & Dev
6
-
7
- - Install: `bun install`
8
- - Test: `bun test` (watch: `bun test --watch`)
9
-
10
- Use in TS:
11
-
12
- ```ts
13
- import { Flags, Right, Rights } from 'odgn-rights'; // when packaged
14
-
15
- // or locally: import { Rights, Right, Flags } from './src/index.ts';
16
- ```
17
-
18
- ## Quick Start
19
-
20
- ```ts
21
- const rights = new Rights();
22
- rights.allow('/', Flags.READ);
23
-
24
- // Deny read, allow create anywhere matching /*/device/**
25
- rights.add(
26
- new Right('/*/device/**', { allow: [Flags.CREATE], deny: [Flags.READ] })
27
- );
28
-
29
- // Grant everything under /system/user/*
30
- rights.add(new Right('/system/user/*', { allow: [Flags.ALL] }));
31
-
32
- rights.read('/system/user/1'); // true
33
- rights.write('/system/user/1'); // true
34
- rights.create('/system/device/a'); // true
35
- rights.read('/system/device/a'); // false (denied by more specific rule)
36
- ```
37
-
38
- ## API Overview
39
-
40
- - `Right(path, {allow?, deny?, description?})`
41
- - Flags: `Flags.READ | WRITE | DELETE | CREATE | EXECUTE | ALL`
42
- - Methods: `allow(flag)`, `deny(flag)`, `clear()`, `has(mask)`
43
- - String form: `-denies+allows:/path` (e.g., `-d+rw:/system`)
44
- - `Right.parse(str)` creates a Right from its string form
45
- - `Rights`
46
- - `add(right)`, `allow(path, ...flags)`, `deny(path, flag)`
47
- - Checks: `read|write|delete|create|execute|all(path)`
48
- - Serialization: `format(separator?)`, `toJSON()`, `Rights.parse(str)`, `Rights.fromJSON(json)`
49
- - `Role(name, rights?)`
50
- - `inheritsFrom(role)`: Inherit rights from another role.
51
- - `Subject`
52
- - `memberOf(role)`: Assign a role to the subject.
53
- - `rights`: Direct `Rights` assigned to the subject.
54
- - `has(path, flag)`: Evaluates permissions across all roles and direct rights.
55
- - `RoleRegistry`
56
- - `define(name, rights?)`: Define or update a role.
57
- - `toJSON() / RoleRegistry.fromJSON(json)`: Serialization with inheritance.
58
-
59
- Matching precedence: the most specific matching rule wins; within a rule, a denied flag blocks that flag even if allowed by a less specific rule.
60
-
61
- ## RBAC Example
62
-
63
- ```ts
64
- const registry = new RoleRegistry();
65
- const viewer = registry.define('viewer', new Rights().allow('/', Flags.READ));
66
- const editor = registry.define(
67
- 'editor',
68
- new Rights().allow('/posts', Flags.WRITE)
69
- );
70
- editor.inheritsFrom(viewer);
71
-
72
- const user = new Subject().memberOf(editor);
73
- user.read('/posts/1'); // true (inherited from viewer)
74
- user.write('/posts/1'); // true (from editor)
75
- user.write('/config'); // false
76
- ```
77
-
78
- ## Contextual Rights (ABAC)
79
-
80
- Rights can include a condition predicate that is evaluated at runtime with a provided context.
81
-
82
- ```ts
83
- rights.add(
84
- new Right('/posts/*', {
85
- allow: [Flags.WRITE],
86
- condition: ctx => ctx.userId === ctx.ownerId
87
- })
88
- );
89
-
90
- // Provide context to the check
91
- rights.write('/posts/1', { userId: 'abc', ownerId: 'abc' }); // true
92
- rights.write('/posts/1', { userId: 'abc', ownerId: 'xyz' }); // false
93
- ```
94
-
95
- ## Glob Patterns
96
-
97
- - `*` matches within a single path segment (`/system/*/id`)
98
- - `**` matches across segments (`/*/device/**`)
99
- - `?` matches a single character (no slash)
100
-
101
- ## Rule Priority
102
-
103
- By default, rules are matched by **specificity** — the most specific matching rule wins. However, you can override this with explicit **priority** values.
104
-
105
- ### How Priority Works
106
-
107
- 1. **Higher priority wins** regardless of specificity
108
- 2. **Equal priorities** fall back to specificity comparison
109
- 3. **Default priority is 0** when not specified
110
- 4. **Negative priorities** can deprioritize rules below the default
111
-
112
- ```ts
113
- const rights = new Rights();
114
-
115
- // Specific path, default priority (0)
116
- rights.add(
117
- new Right('/posts/123', {
118
- allow: [Flags.READ],
119
- deny: [Flags.WRITE]
120
- })
121
- );
122
-
123
- // Wildcard path, but high priority (100) — this rule wins!
124
- rights.add(
125
- new Right('/posts/*', {
126
- allow: [Flags.READ, Flags.WRITE],
127
- priority: 100
128
- })
129
- );
130
-
131
- rights.write('/posts/123'); // true — high-priority wildcard rule wins
132
- ```
133
-
134
- ### Priority in Serialization
135
-
136
- Priority is included in both text and JSON serialization formats.
137
-
138
- **Text format** uses `^` after the path:
139
-
140
- ```ts
141
- const right = new Right('/posts/*', {
142
- allow: [Flags.WRITE],
143
- priority: 100
144
- });
145
- right.toString(); // "+w:/posts/*^100"
146
-
147
- // With tags and time ranges
148
- // Format: [flags]:[path]^[priority]#[tags]@[validFrom]/[validUntil]
149
- Right.parse('+rw:/admin/*^50#secure');
150
- ```
151
-
152
- **JSON format** includes an optional `priority` field:
153
-
154
- ```json
155
- [
156
- { "path": "/posts/*", "allow": "rw", "priority": 100 },
157
- { "path": "/posts/123", "allow": "r", "deny": "w" }
158
- ]
159
- ```
160
-
161
- Priority is omitted from serialization when it equals 0 (the default).
162
-
163
- ### Use Cases
164
-
165
- - **Emergency overrides**: Grant temporary high-priority access that bypasses normal rules
166
- - **Default deny rules**: Use negative priority for fallback deny rules
167
- - **Policy layers**: Implement organizational policies at different priority levels
168
-
169
- ```ts
170
- // Low-priority default: deny all writes
171
- rights.add(
172
- new Right('/**', {
173
- deny: [Flags.WRITE],
174
- priority: -100
175
- })
176
- );
177
-
178
- // Normal priority: department-level permissions
179
- rights.add(
180
- new Right('/dept/engineering/**', {
181
- allow: [Flags.READ, Flags.WRITE]
182
- })
183
- );
184
-
185
- // High priority: emergency maintenance access
186
- rights.add(
187
- new Right('/system/**', {
188
- allow: [Flags.ALL],
189
- priority: 1000,
190
- tags: ['emergency']
191
- })
192
- );
193
- ```
194
-
195
- ## JSON Round‑Trip
196
-
197
- ```ts
198
- const json = rights.toJSON();
199
- // [ { path: '/', allow: 'r' }, { path: '/*/device/**', allow: 'c' }, ... ]
200
- const loaded = Rights.fromJSON(json);
201
- ```
202
-
203
- ## Batch Permission Checks
204
-
205
- Efficiently check multiple permissions at once with `checkMany()`.
206
-
207
- ### Basic Usage
208
-
209
- ```ts
210
- const rights = new Rights();
211
- rights.allow('/users/*', Flags.READ);
212
- rights.allow('/posts/*', Flags.WRITE);
213
- rights.deny('/admin', Flags.ALL);
214
-
215
- const results = rights.checkMany([
216
- { path: '/users/1', flags: Flags.READ },
217
- { path: '/posts/1', flags: Flags.WRITE },
218
- { path: '/admin', flags: Flags.ALL }
219
- ]);
220
- // Returns: [true, true, false]
221
- ```
222
-
223
- ### With Context
224
-
225
- The same context is shared across all checks:
226
-
227
- ```ts
228
- rights.add(
229
- new Right('/posts/*', {
230
- allow: [Flags.WRITE],
231
- condition: ctx => ctx.userId === ctx.ownerId
232
- })
233
- );
234
-
235
- const results = rights.checkMany(
236
- [
237
- { path: '/posts/1', flags: Flags.WRITE },
238
- { path: '/posts/2', flags: Flags.WRITE },
239
- { path: '/posts/3', flags: Flags.WRITE }
240
- ],
241
- { userId: 'user1', ownerId: 'user1' }
242
- );
243
- // Returns: [true, true, true]
244
- ```
245
-
246
- ### With Subjects
247
-
248
- Works with subjects that have multiple roles:
249
-
250
- ```ts
251
- const viewer = new Role('viewer', new Rights().allow('/docs', Flags.READ));
252
- const writer = new Role('writer', new Rights().allow('/docs', Flags.WRITE));
253
-
254
- const subject = new Subject().memberOf(viewer).memberOf(writer);
255
-
256
- const results = subject.checkMany([
257
- { path: '/docs', flags: Flags.READ },
258
- { path: '/docs', flags: Flags.WRITE },
259
- { path: '/docs', flags: Flags.DELETE }
260
- ]);
261
- // Returns: [true, true, false]
262
- ```
263
-
264
- ### Use Cases
265
-
266
- - **Bulk authorization**: Check multiple resource permissions in a single call
267
- - **Feature flags**: Enable/disable multiple features based on permissions
268
- - **API responses**: Include permission information for multiple resources
269
- - **UI rendering**: Determine visibility of multiple UI elements efficiently
270
-
271
- ## CLI Tool
272
-
273
- The CLI tool helps test and debug permission configurations from the command line.
274
-
275
- ### Installation
276
-
277
- ```bash
278
- # Install globally
279
- npm install -g @odgn/rights
280
-
281
- # Or use with npx
282
- npx @odgn/rights --help
283
-
284
- # Or run directly with bun
285
- bun run src/cli/index.ts --help
286
- ```
287
-
288
- ### Commands
289
-
290
- #### check
291
-
292
- Test if a permission is allowed:
293
-
294
- ```bash
295
- # Basic usage
296
- odgn-rights check -c config.json -p /users/123 -f READ
297
-
298
- # With combined flags
299
- odgn-rights check -c config.json -p /users/123 -f RW
300
-
301
- # With comma-separated flags
302
- odgn-rights check -c config.json -p /users/123 -f READ,WRITE
303
-
304
- # Quiet mode for scripting (outputs 'true' or 'false')
305
- odgn-rights check -c config.json -p /users/123 -f READ --quiet
306
-
307
- # With context for conditional rights
308
- odgn-rights check -c config.json -p /posts/1 -f WRITE --context '{"userId":"abc","ownerId":"abc"}'
309
-
310
- # Override time for time-based rights
311
- odgn-rights check -c config.json -p /scheduled -f READ --time 2025-06-15T12:00:00Z
312
- ```
313
-
314
- Exit codes: `0` = allowed, `1` = denied, `2` = error
315
-
316
- #### explain
317
-
318
- Understand why a permission is allowed or denied:
319
-
320
- ```bash
321
- # Basic usage
322
- odgn-rights explain -c config.json -p /users/123 -f WRITE
323
-
324
- # JSON output
325
- odgn-rights explain -c config.json -p /users/123 -f READ --json
326
- ```
327
-
328
- The explain command shows:
329
-
330
- - Decision breakdown per flag
331
- - Matching rules sorted by specificity
332
- - Suggestions for granting denied permissions
333
-
334
- #### validate
335
-
336
- Validate a configuration file:
337
-
338
- ```bash
339
- # Validate JSON config
340
- odgn-rights validate config.json
341
-
342
- # Validate string format config
343
- odgn-rights validate config.txt
344
-
345
- # Strict mode (warns on broad patterns like /**)
346
- odgn-rights validate --strict config.json
347
-
348
- # JSON output
349
- odgn-rights validate --json config.json
350
- ```
351
-
352
- Exit codes: `0` = valid, `1` = validation errors, `2` = file error
353
-
354
- ### Configuration Formats
355
-
356
- The CLI supports two configuration formats:
357
-
358
- **JSON format** (`config.json`):
359
-
360
- ```json
361
- [
362
- { "path": "/", "allow": "r" },
363
- { "path": "/users/*", "allow": "rw" },
364
- { "path": "/admin/**", "allow": "*", "tags": ["admin"] },
365
- { "path": "/scheduled", "allow": "r", "validFrom": "2025-01-01T00:00:00Z" }
366
- ]
367
- ```
368
-
369
- **String format** (`config.txt`):
370
-
371
- ```
372
- # Comments start with #
373
- +r:/
374
- +rw:/users/*
375
- +*:/admin/**
376
- -d+rw:/public
377
- ```
378
-
379
- ### Flag Reference
380
-
381
- | Flag | Letter | Description |
382
- | ------- | ------ | ------------------ |
383
- | READ | R | Read permission |
384
- | WRITE | W | Write permission |
385
- | CREATE | C | Create permission |
386
- | DELETE | D | Delete permission |
387
- | EXECUTE | X | Execute permission |
388
- | ALL | \* | All permissions |
389
-
390
- Flags can be combined: `RW`, `READ,WRITE`, `RWCDX`
391
-
392
- ## Database Adapters
393
-
394
- Database adapters enable persistent storage of rights configurations in SQLite or PostgreSQL databases. This is useful for applications that need to load permissions from a database, share configurations across services, or audit permission changes.
395
-
396
- ### Installation
397
-
398
- The adapters use Bun's built-in database drivers (`bun:sqlite` and `bun` SQL), so no additional dependencies are required.
399
-
400
- ```ts
401
- import { PostgresAdapter, SQLiteAdapter } from 'odgn-rights/adapters';
402
- ```
403
-
404
- ### Table Prefix
405
-
406
- All adapters support a configurable table prefix. The default prefix is `tbl_`.
407
-
408
- ```ts
409
- // Default prefix creates tables: tbl_rights, tbl_roles, etc.
410
- const adapter = new SQLiteAdapter({ filename: './permissions.db' });
411
-
412
- // Custom prefix creates tables: auth_rights, auth_roles, etc.
413
- const adapter = new SQLiteAdapter({
414
- filename: './permissions.db',
415
- tablePrefix: 'auth_'
416
- });
417
-
418
- // No prefix creates tables: rights, roles, etc.
419
- const adapter = new SQLiteAdapter({
420
- filename: './permissions.db',
421
- tablePrefix: ''
422
- });
423
- ```
424
-
425
- ### SQLite Adapter
426
-
427
- SQLite is ideal for single-process applications, embedded systems, or local development.
428
-
429
- ```ts
430
- import { Flags, Right, Rights } from 'odgn-rights';
431
- import { SQLiteAdapter } from 'odgn-rights/adapters';
432
-
433
- // Create adapter and connect
434
- const adapter = new SQLiteAdapter({
435
- filename: './permissions.db', // Use ':memory:' for in-memory
436
- enableWAL: true // Enable WAL mode for better concurrency
437
- });
438
-
439
- await adapter.connect();
440
- await adapter.migrate();
441
-
442
- // Save rights
443
- const rights = new Rights();
444
- rights.allow('/users/*', Flags.READ);
445
- rights.allow('/admin/**', Flags.ALL);
446
- await adapter.saveRights(rights);
447
-
448
- // Load rights
449
- const loaded = await adapter.loadRights();
450
- loaded.has('/users/123', Flags.READ); // true
451
-
452
- // Save and load roles
453
- const { Role, RoleRegistry } = await import('odgn-rights');
454
- const registry = new RoleRegistry();
455
- const admin = registry.define('admin');
456
- admin.rights.allow('/**', Flags.ALL);
457
- await registry.saveTo(adapter);
458
-
459
- // Load registry from database
460
- const loadedRegistry = await RoleRegistry.loadFrom(adapter);
461
-
462
- await adapter.disconnect();
463
- ```
464
-
465
- ### PostgreSQL Adapter
466
-
467
- PostgreSQL is ideal for multi-process applications, microservices, or when you need shared access to permissions.
468
-
469
- ```ts
470
- import { Flags, RoleRegistry, Subject } from 'odgn-rights';
471
- import { PostgresAdapter } from 'odgn-rights/adapters';
472
-
473
- const adapter = new PostgresAdapter({
474
- url: 'postgres://user:pass@localhost:5432/mydb',
475
- // Or use individual options:
476
- // hostname: 'localhost',
477
- // port: 5432,
478
- // database: 'mydb',
479
- // username: 'user',
480
- // password: 'pass',
481
- tablePrefix: 'perms_' // Optional custom prefix
482
- });
483
-
484
- await adapter.connect();
485
- await adapter.migrate();
486
-
487
- // Load registry and make changes
488
- const registry = await adapter.loadRegistry();
489
- const editor = registry.define('editor');
490
- editor.rights.allow('/content/**', Flags.READ, Flags.WRITE);
491
-
492
- // Save back
493
- await adapter.saveRegistry(registry);
494
-
495
- // Save subjects with roles
496
- const user = new Subject();
497
- user.memberOf(editor);
498
- await adapter.saveSubject('user-123', user);
499
-
500
- await adapter.disconnect();
501
- ```
502
-
503
- ### Factory Functions
504
-
505
- Convenience functions for common patterns:
506
-
507
- ```ts
508
- import {
509
- createPostgresRegistry,
510
- createPostgresRights,
511
- createSQLiteRegistry,
512
- createSQLiteRights
513
- } from 'odgn-rights/adapters';
514
-
515
- // Create SQLite adapter with rights
516
- const { adapter, rights } = await createSQLiteRights({
517
- filename: './permissions.db'
518
- });
519
- rights.allow('/public/**', Flags.READ);
520
- await adapter.saveRights(rights);
521
- await adapter.disconnect();
522
-
523
- // Create SQLite adapter with registry
524
- const { adapter: regAdapter, registry } = await createSQLiteRegistry({
525
- filename: ':memory:'
526
- });
527
- const viewer = registry.define('viewer');
528
- viewer.rights.allow('/read/*', Flags.READ);
529
- await registry.saveTo(regAdapter);
530
- await regAdapter.disconnect();
531
- ```
532
-
533
- ### Transactions
534
-
535
- Both adapters support transactions for atomic operations:
536
-
537
- ```ts
538
- await adapter.transaction(async () => {
539
- await adapter.saveRight(new Right('/a', { allow: [Flags.READ] }));
540
- await adapter.saveRight(new Right('/b', { allow: [Flags.WRITE] }));
541
- // If an error is thrown, all changes are rolled back
542
- });
543
- ```
544
-
545
- ### Adapter Interface
546
-
547
- All adapters implement the `DatabaseAdapter` interface:
548
-
549
- | Method | Description |
550
- | ---------------------------------- | --------------------------------- |
551
- | `connect()` | Connect to the database |
552
- | `disconnect()` | Disconnect from the database |
553
- | `migrate()` | Create or update schema |
554
- | `saveRight(right)` | Save a single right |
555
- | `saveRights(rights)` | Save multiple rights |
556
- | `loadRight(id)` | Load a right by ID |
557
- | `loadRights()` | Load all rights |
558
- | `loadRightsByPath(pattern)` | Load rights matching a pattern |
559
- | `deleteRight(id)` | Delete a right |
560
- | `saveRole(role)` | Save a role with its rights |
561
- | `loadRole(name)` | Load a role by name |
562
- | `loadRoles()` | Load all roles |
563
- | `deleteRole(name)` | Delete a role |
564
- | `saveRegistry(registry)` | Save entire RoleRegistry |
565
- | `loadRegistry()` | Load RoleRegistry with all roles |
566
- | `saveSubject(identifier, subject)` | Save a subject |
567
- | `loadSubject(identifier)` | Load a subject |
568
- | `deleteSubject(identifier)` | Delete a subject |
569
- | `clear()` | Clear all data (for testing) |
570
- | `transaction(fn)` | Execute operations in transaction |
571
-
572
- ### Database Schema
573
-
574
- The adapters create the following tables (with the configured prefix):
575
-
576
- | Table | Purpose |
577
- | -------------------------- | ------------------------------------ |
578
- | `{prefix}rights` | Individual rights with paths & flags |
579
- | `{prefix}roles` | Role definitions |
580
- | `{prefix}role_rights` | Role-to-rights mapping |
581
- | `{prefix}role_inheritance` | Role inheritance relationships |
582
- | `{prefix}subjects` | Subject records |
583
- | `{prefix}subject_roles` | Subject-to-roles mapping |
584
- | `{prefix}subject_rights` | Direct subject rights |
585
-
586
- ### Persistence Metadata
587
-
588
- Rights saved to the database receive a `dbId` property:
589
-
590
- ```ts
591
- const right = new Right('/test', { allow: [Flags.READ] });
592
- console.log(right.dbId); // undefined
593
-
594
- await adapter.saveRight(right);
595
- console.log(right.dbId); // 1 (database ID)
596
- ```