@pcg/auth 1.0.0-alpha.2 → 1.0.0-alpha.3

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
@@ -1,84 +1,602 @@
1
- # @deep/auth
1
+ # @pcg/auth
2
2
 
3
- ### Quick Start
3
+ Authorization library for checking user access rights. Supports a flexible permission system with scopes, wildcard access, and NestJS integration.
4
4
 
5
- Install npm package
5
+ ## Installation
6
6
 
7
- ```tsx
8
- npm i @deep/auth --save
7
+ ```bash
8
+ npm i @pcg/auth
9
9
  ```
10
10
 
11
- Use `isGranted` helper to check access rights.
11
+ ## Quick Start
12
12
 
13
- ```jsx
14
- import { isGranted } from '@deep/auth'
15
- isGranted(user, 'js:episodes:create'); // boolean
13
+ ```typescript
14
+ import { isGranted, resolvePermissions } from '@pcg/auth';
15
+ import * as s from '@pcg/auth/scopes';
16
+
17
+ // Prepare user object
18
+ const user = {
19
+ id: 'hcu:2x6g7l8n9op',
20
+ permissions: ['js:core:episodes[org#hcorg:company1]:get'],
21
+ resolvedPermissions: resolvePermissions(['js:core:episodes[org#hcorg:company1]:get']),
22
+ };
23
+
24
+ // Check access
25
+ isGranted(user, 'js:core:episodes:get', s.org('hcorg:company1')); // true
26
+ isGranted(user, 'js:core:episodes:get', s.org('hcorg:other')); // false
27
+ ```
28
+
29
+ ## Exports
30
+
31
+ The library provides two entry points:
32
+
33
+ | Import | Description |
34
+ |--------|-------------|
35
+ | `@pcg/auth` | Core functions: `isGranted`, `resolvePermissions`, `ActionScopes`, `ScopesBulder`, types |
36
+ | `@pcg/auth/scopes` | Functional scope builders: `org`, `id`, `user`, `and`, `anyScope`, etc. |
37
+
38
+ ---
39
+
40
+ ## Permission String Format
41
+
42
+ **Pattern**: `<service>:<module>:<resource>[scopes]:<action>`
43
+
44
+ | Component | Description | Examples |
45
+ |-----------|-------------|----------|
46
+ | service | Application identifier | `js`, `tl`, `bo`, `*` |
47
+ | module | Module within the service | `core`, `mam`, `*` |
48
+ | resource | Entity type | `episodes`, `users`, `roles` |
49
+ | scopes | Constraints (optional) | `[org]`, `[org,published]`, `[org+published]` |
50
+ | action | Operation | `get`, `list`, `create`, `update`, `delete`, `*` |
51
+
52
+ ### Permission String Examples
53
+
54
+ ```typescript
55
+ 'js:core:episodes:get' // No scopes — full access to the action
56
+ 'js:core:episodes[org]:get' // Restricted to organization
57
+ 'js:core:episodes[org,published]:get' // OR — needs ANY of the scopes
58
+ 'js:core:episodes[org+published]:get' // AND — needs ALL scopes
59
+ 'js:core:episodes[org#hcorg:company1]:get' // Bound to a specific organization
60
+ 'js:*:*:*' // Wildcard — all actions in the service
61
+ '*:*:*:*' // Super admin — everything
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Scope Logic
67
+
68
+ ### OR Logic (comma `,`)
69
+
70
+ User is granted access if the action scope matches **any** of their scopes.
71
+
72
+ ```typescript
73
+ // Permission: 'js:core:episodes[org,published]:get'
74
+ // Resolved scopes: ['org', 'published']
75
+
76
+ isGranted(user, 'js:core:episodes:get', ['org']); // true
77
+ isGranted(user, 'js:core:episodes:get', ['published']); // true
78
+ isGranted(user, 'js:core:episodes:get', ['draft']); // false
79
+ ```
80
+
81
+ ### AND Logic (plus `+`)
82
+
83
+ User is granted access **only** if the action scope contains **all** specified scopes.
84
+
85
+ ```typescript
86
+ // Permission: 'js:core:episodes[org+published]:get'
87
+ // Resolved scopes: [['org', 'published']] ← array within array
88
+
89
+ isGranted(user, 'js:core:episodes:get', ['org']); // false
90
+ isGranted(user, 'js:core:episodes:get', [['org', 'published']]); // true
91
+ ```
92
+
93
+ ### Mixed Logic
94
+
95
+ ```typescript
96
+ // Permission: 'js:core:episodes[published,org+draft]:get'
97
+ // Resolved scopes: ['published', ['org', 'draft']]
98
+
99
+ // Access granted if:
100
+ // - scope is 'published', OR
101
+ // - both 'org' AND 'draft' scopes
102
+
103
+ isGranted(user, 'js:core:episodes:get', ['published']); // true
104
+ isGranted(user, 'js:core:episodes:get', [['org', 'draft']]); // true
105
+ isGranted(user, 'js:core:episodes:get', ['org']); // false
106
+ ```
107
+
108
+ ### Scope ID (hash `#`)
109
+
110
+ Scopes can include a binding to a specific entity ID.
111
+
112
+ ```typescript
113
+ // Permission: 'js:core:episodes[org#hcorg:company1]:get'
114
+
115
+ isGranted(user, 'js:core:episodes:get', ['org#hcorg:company1']); // true
116
+ isGranted(user, 'js:core:episodes:get', ['org#hcorg:company2']); // false
117
+ ```
118
+
119
+ ### Wildcard (`*`)
120
+
121
+ The `['*']` action scope bypasses scope checking.
122
+
123
+ ```typescript
124
+ isGranted(user, 'js:core:episodes:get', ['*']); // true — if user has the permission with any scope
125
+ ```
126
+
127
+ ### Results Matrix
128
+
129
+ | User Permission | Action Scopes | Result |
130
+ |-----------------|---------------|--------|
131
+ | `episodes:get` (no scopes) | any | `true` |
132
+ | `episodes[org]:get` | `['org']` | `true` |
133
+ | `episodes[org]:get` | `['published']` | `false` |
134
+ | `episodes[org]:get` | `[]` (empty) | `false` |
135
+ | `episodes[org]:get` | `['*']` | `true` |
136
+ | `episodes[org,published]:get` | `['org']` | `true` |
137
+ | `episodes[org+published]:get` | `['org']` | `false` |
138
+ | `episodes[org+published]:get` | `[['org', 'published']]` | `true` |
139
+ | `episodes[org#hcorg:A]:get` | `['org#hcorg:A']` | `true` |
140
+ | `episodes[org#hcorg:A]:get` | `['org#hcorg:B']` | `false` |
141
+ | `js:*:*:*` | any | `true` |
142
+
143
+ ---
144
+
145
+ ## Types
146
+
147
+ ### IUser
148
+
149
+ The user object must contain `resolvedPermissions`:
150
+
151
+ ```typescript
152
+ interface IUser {
153
+ id: string; // 'hcu:2x6g7l8n9op'
154
+ permissions: readonly string[]; // Raw permission strings
155
+ resolvedPermissions: ReadonlyResolvedPermission[]; // Parsed permissions
156
+ }
157
+ ```
158
+
159
+ ### ResolvedPermission
160
+
161
+ ```typescript
162
+ interface ResolvedPermission {
163
+ id: string; // Permission without scope brackets
164
+ scopes: (string | string[])[]; // Parsed scopes
165
+ }
166
+
167
+ // Examples:
168
+ // 'js:core:episodes[org,published]:get'
169
+ // → { id: 'js:core:episodes:get', scopes: ['org', 'published'] }
170
+ //
171
+ // 'js:core:episodes[org+published]:get'
172
+ // → { id: 'js:core:episodes:get', scopes: [['org', 'published']] }
173
+ ```
174
+
175
+ ---
176
+
177
+ ## API
178
+
179
+ ### `isGranted(user, permission, actionScopes?)`
180
+
181
+ Main function for checking access. Returns `boolean`.
182
+
183
+ ```typescript
184
+ import { isGranted } from '@pcg/auth';
185
+ import * as s from '@pcg/auth/scopes';
186
+
187
+ // Without scopes
188
+ isGranted(user, 'js:core:episodes:get');
189
+
190
+ // With single scope (no array needed)
191
+ isGranted(user, 'js:core:episodes:get', s.org('hcorg:company1'));
192
+
193
+ // With wildcard
194
+ isGranted(user, 'js:core:episodes:list', s.anyScope());
195
+ ```
196
+
197
+ ### `resolvePermission(permission)`
198
+
199
+ Parses a single permission string into an object.
200
+
201
+ ```typescript
202
+ import { resolvePermission } from '@pcg/auth';
203
+
204
+ resolvePermission('js:core:episodes[org,published]:get');
205
+ // { id: 'js:core:episodes:get', scopes: ['org', 'published'] }
206
+
207
+ resolvePermission('js:core:episodes[org+published]:get');
208
+ // { id: 'js:core:episodes:get', scopes: [['org', 'published']] }
209
+ ```
210
+
211
+ ### `resolvePermissions(permissions)`
212
+
213
+ Parses an array of permission strings. Matching IDs are merged — their scopes are combined.
214
+
215
+ ```typescript
216
+ import { resolvePermissions } from '@pcg/auth';
217
+
218
+ resolvePermissions([
219
+ 'js:core:episodes[org]:get',
220
+ 'js:core:episodes[published]:get',
221
+ ]);
222
+ // [{ id: 'js:core:episodes:get', scopes: ['org', 'published'] }]
223
+ ```
224
+
225
+ ### `mergeResolvedPermissions(array1, array2)`
226
+
227
+ Merges two arrays of resolved permissions. Empty scopes (`[]`) mean full access and take precedence when merging.
228
+
229
+ ```typescript
230
+ import { mergeResolvedPermissions } from '@pcg/auth';
231
+
232
+ mergeResolvedPermissions(
233
+ [{ id: 'js:core:episodes:get', scopes: ['org#hci'] }],
234
+ [{ id: 'js:core:episodes:get', scopes: ['org#dv'] }],
235
+ );
236
+ // [{ id: 'js:core:episodes:get', scopes: ['org#hci', 'org#dv'] }]
237
+
238
+ // Empty scopes = full access
239
+ mergeResolvedPermissions(
240
+ [{ id: 'js:core:episodes:get', scopes: ['org#hci'] }],
241
+ [{ id: 'js:core:episodes:get', scopes: [] }],
242
+ );
243
+ // [{ id: 'js:core:episodes:get', scopes: [] }]
244
+ ```
245
+
246
+ ### `encodeScopes(scopes)`
247
+
248
+ Converts a scopes array back to string format.
249
+
250
+ ```typescript
251
+ import { encodeScopes } from '@pcg/auth';
252
+
253
+ encodeScopes(['org#xxx', 'user#xxx']);
254
+ // '[org#xxx,user#xxx]'
255
+
256
+ encodeScopes([['org#xxx', 'published']]);
257
+ // '[org#xxx+published]'
258
+ ```
259
+
260
+ ### `injectScopesIntoPermission(permission, scopes)`
261
+
262
+ Adds scopes to a permission string.
263
+
264
+ ```typescript
265
+ import { injectScopesIntoPermission } from '@pcg/auth';
266
+
267
+ injectScopesIntoPermission('js:core:episodes:create', ['org']);
268
+ // 'js:core:episodes[org]:create'
269
+
270
+ injectScopesIntoPermission('js:core:episodes[org]:create', ['shared']);
271
+ // 'js:core:episodes[org,shared]:create'
272
+ ```
273
+
274
+ ### `replaceScope(scopes, from, to)`
275
+
276
+ Replaces specific scope values in an array.
277
+
278
+ ```typescript
279
+ import { replaceScope } from '@pcg/auth';
280
+
281
+ replaceScope(['assigned'], 'assigned', 'brand#brd:xxx');
282
+ // ['brand#brd:xxx']
283
+
284
+ // Works with AND scopes
285
+ replaceScope([['assigned', 'lang']], 'assigned', 'brand#brd:xxx');
286
+ // [['brand#brd:xxx', 'lang']]
287
+
288
+ // Sequential replacement
289
+ let scopes = [['assigned', 'lang']];
290
+ scopes = replaceScope(scopes, 'assigned', 'brand#brd:xxx');
291
+ scopes = replaceScope(scopes, 'lang', 'lang#en');
292
+ // [['brand#brd:xxx', 'lang#en']]
16
293
  ```
17
294
 
18
- ### Resolved Permissions
295
+ ---
296
+
297
+ ## Functional Scope Builders (recommended)
19
298
 
20
- User object `user` must contain `resolvedPemissions` array with array of objects:
299
+ Imported from `@pcg/auth/scopes`. Pure functions with no classes convenient for inline use.
21
300
 
22
- ```tsx
23
- user.resolvedPermission = [
24
- {
25
- id: "js:episodes:get",
26
- scopes: []
27
- },
28
- {
29
- id: "js:episodes:list",
30
- scopes: ["published"]
31
- }
32
- ],
301
+ `isGranted` accepts a single scope string directly or an array for multiple scopes.
33
302
 
303
+ ```typescript
304
+ import * as s from '@pcg/auth/scopes';
34
305
  ```
35
306
 
36
- But, is real life we store permissions in database as array of string, like this:
307
+ ### Available Functions
308
+
309
+ | Function | Returns | Example |
310
+ |----------|---------|---------|
311
+ | `s.anyScope()` | `ActionScopesArray` | `s.anyScope()` → `['*']` |
312
+ | `s.org(id)` | `string` | `s.org('jsorg:hci')` → `'org#jsorg:hci'` |
313
+ | `s.id(entityId)` | `string` | `s.id('ep:123')` → `'id#ep:123'` |
314
+ | `s.user(id)` | `string` | `s.user('hcu:xxx')` → `'user#hcu:xxx'` |
315
+ | `s.form(id)` | `string` | `s.form('contact')` → `'form#contact'` |
316
+ | `s.group(id)` | `string` | `s.group('hcgrp:ZT9')` → `'grp#hcgrp:ZT9'` |
317
+ | `s.scope(name, id?)` | `string` | `s.scope('orggroup', 'hcgrp:ZT9')` → `'orggroup#hcgrp:ZT9'` |
318
+ | `s.and(...items)` | `string[]` | `s.and(s.org('hci'), 'published')` → `['org#hci', 'published']` |
319
+
320
+ ### Usage Examples
321
+
322
+ ```typescript
323
+ import * as s from '@pcg/auth/scopes';
324
+ import { isGranted } from '@pcg/auth';
325
+
326
+ // Single scope — no array needed
327
+ isGranted(user, 'js:core:brands:update', s.org(brand.orgId));
37
328
 
38
- ```tsx
39
- [
40
- "js:episodes:get",
41
- "js:episodes[@published]:list"
42
- ]
329
+ // Multiple scopes (OR) — use array
330
+ isGranted(user, 'js:core:episodes:get', [s.org(episode.orgId), s.id(episode.id)]);
331
+
332
+ // AND scopes (reuse variable)
333
+ const org = s.org(episode.orgId);
334
+ isGranted(user, 'js:core:episodes:get', [org, s.and(org, 'published')]);
335
+
336
+ // Wildcard
337
+ isGranted(user, 'js:core:episodes:list', s.anyScope());
338
+
339
+ // Custom scope
340
+ isGranted(user, 'js:mam:episodes:create', s.scope('action', 'approve'));
43
341
  ```
44
342
 
45
- Library has special helper, to resolve permissions:
343
+ ---
344
+
345
+ ## ActionScopes (legacy)
346
+
347
+ The `ActionScopes` class is still supported for backward compatibility. Use functional scope builders for new code.
46
348
 
47
- ```tsx
48
- import { resolvePermissions } from '@deep/auth';
349
+ ```typescript
350
+ import { ActionScopes, isGranted } from '@pcg/auth';
49
351
 
50
- const permissions = [
51
- "js:episodes:get",
52
- "js:episodes[@published]:list"
53
- ]
352
+ const scopes = new ActionScopes();
353
+ scopes.set('org', 'hcorg:company1'); // org#hcorg:company1
354
+ scopes.set('org+published'); // AND: ['org#hcorg:company1', 'published']
355
+ scopes.setEntityId('jse:episode123'); // id#jse:episode123
54
356
 
55
- user.resolvedPermission = resolvePermissions(permissions)
357
+ isGranted(user, 'js:core:episodes:get', scopes);
56
358
  ```
57
359
 
58
- ### Permission Context
360
+ ---
59
361
 
60
- Some users don't have full access to action, but have access in some execution context. For example, user dont have permission to all episodes, but have permission only to published episodes.
362
+ ## ScopesBulder
61
363
 
62
- In this case we determinate execution context. If user fetch only published episodes, we set scope to `published`. If user has special permission `js:episodes[@published]:list` he will obtain access to this action.
364
+ Utility for building complex scope combinations.
63
365
 
64
- ```tsx
65
- import { isGranted, PermissionContext } from '@deep/auth';
66
- const ctx = new PermissionContext();
366
+ ```typescript
367
+ import { ScopesBulder } from '@pcg/auth';
67
368
 
68
- if (filter.published) {
69
- ctx.addScope('published');
369
+ const builder = new ScopesBulder();
370
+
371
+ // Add scopes
372
+ builder.append('org#hci');
373
+ builder.extend(['org#hci', 'org#dv']);
374
+
375
+ // Join — creates AND combinations (cartesian product)
376
+ builder.extend(['published', 'draft']);
377
+ builder.join('org#hcc', 'before');
378
+ // [['org#hcc', 'published'], ['org#hcc', 'draft']]
379
+
380
+ // Replace prefix
381
+ builder.replacePrefix('org', 'id');
382
+ // ['org#hcorg:hci'] → ['id#hcorg:hci']
383
+
384
+ // Clone and build
385
+ const clone = builder.clone();
386
+ const scopes = builder.build();
387
+ ```
388
+
389
+ ### Example: Multi-language, multi-org access
390
+
391
+ ```typescript
392
+ const builder = new ScopesBulder();
393
+ builder.extend(['org#hci', 'org#dv']);
394
+ builder.join(['lang#en', 'lang#de']);
395
+ builder.build();
396
+ // [
397
+ // ['org#hci', 'lang#en'],
398
+ // ['org#hci', 'lang#de'],
399
+ // ['org#dv', 'lang#en'],
400
+ // ['org#dv', 'lang#de'],
401
+ // ]
402
+ ```
403
+
404
+ ---
405
+
406
+ ## NestJS Integration
407
+
408
+ ### Basic CRUD
409
+
410
+ ```typescript
411
+ import * as s from '@pcg/auth/scopes';
412
+
413
+ @Resolver(() => Episode)
414
+ @UseGuards(GraphQLJwtAuthGuard, GraphQLPermissionsGuard)
415
+ export class EpisodesResolver {
416
+
417
+ @Query(() => Episode)
418
+ @UsePermission('js:core:episodes:get')
419
+ async episode(
420
+ @Args('id') id: string,
421
+ @ActionContextParam() ctx: ActionContext,
422
+ ) {
423
+ const episode = await this.episodesService.findOne(id);
424
+
425
+ ctx.validateAccess(
426
+ 'js:core:episodes:get',
427
+ [s.org(episode.organizationId), s.id(episode.id)],
428
+ );
429
+
430
+ return episode;
431
+ }
70
432
  }
433
+ ```
434
+
435
+ ### Two-Level Permission Check
436
+
437
+ 1. **`@UsePermission()`** — guard checks whether the user has access to the action with **any** scope (uses `['*']` wildcard)
438
+ 2. **`ctx.validateAccess()`** — inside the resolver, checks **specific scopes** for the given entity
439
+
440
+ ```typescript
441
+ import * as s from '@pcg/auth/scopes';
442
+
443
+ @Mutation(() => Episode)
444
+ @UsePermission('js:mam:episodes:update') // Step 1: does the user have this permission at all?
445
+ async updateEpisode(
446
+ @Args('input') input: UpdateEpisodeInput,
447
+ @ActionContextParam() ctx: ActionContext,
448
+ ) {
449
+ const episode = await this.episodeService.findOne(input.id);
71
450
 
72
- isGranted(user, 'js:episodes:list'); // boolean
451
+ // Step 2: check the specific scope
452
+ ctx.validateAccess(
453
+ 'js:mam:episodes:update',
454
+ s.org(episode.organizationId),
455
+ );
456
+ // If the episode belongs to company2 but the user only has access to company1 — throws an error
457
+
458
+ return this.episodeService.update(input, ctx);
459
+ }
460
+ ```
461
+
462
+ ### `ctx.validateAccess()` vs `ctx.isGranted()`
463
+
464
+ | Method | Behavior | When to use |
465
+ |--------|----------|-------------|
466
+ | `ctx.validateAccess()` | Throws an error if access is denied | When access is mandatory |
467
+ | `ctx.isGranted()` | Returns `boolean` | When branching logic is needed |
468
+
469
+ ```typescript
470
+ // validateAccess — throws if access is denied
471
+ ctx.validateAccess('js:core:episodes:get', s.org(episode.orgId));
472
+
473
+ // isGranted — for conditional logic
474
+ if (ctx.isGranted('js:core:episodes:list', s.anyScope())) {
475
+ return this.episodeService.findAll();
476
+ }
477
+ ```
478
+
479
+ ### Conditional Access: Full vs Scoped
480
+
481
+ ```typescript
482
+ import * as s from '@pcg/auth/scopes';
483
+
484
+ @Query(() => [Episode])
485
+ @UsePermission('js:mam:episodes:list')
486
+ async episodes(@ActionContextParam() ctx: ActionContext) {
487
+ // Admin: full access (permission without scopes)
488
+ if (ctx.isGranted('js:mam:episodes:list', s.anyScope())) {
489
+ return this.episodeService.findAll();
490
+ }
491
+
492
+ // User: organization-scoped access
493
+ if (ctx.isGranted(
494
+ 'js:mam:episodes:list',
495
+ s.org(ctx.user.organizationId),
496
+ )) {
497
+ return this.episodeService.findByOrg(ctx.user.organizationId);
498
+ }
499
+
500
+ return [];
501
+ }
502
+ ```
503
+
504
+ ### Status-Based Access (published/draft)
505
+
506
+ ```typescript
507
+ import * as s from '@pcg/auth/scopes';
508
+
509
+ // Permission: 'js:mam:episodes[org+published]:get'
510
+ // Can only view published episodes within their organization
511
+
512
+ async getEpisode(id: string, ctx: ActionContext) {
513
+ const episode = await this.repository.findOne(id);
514
+
515
+ const org = s.org(episode.organizationId);
516
+ const items: s.ScopeItem[] = [org];
517
+
518
+ if (episode.status === 'published') {
519
+ items.push('published', s.and(org, 'published'));
520
+ } else if (episode.status === 'draft') {
521
+ items.push('draft', s.and(org, 'draft'));
522
+ }
523
+
524
+ ctx.validateAccess('js:mam:episodes:get', items);
525
+
526
+ return episode;
527
+ }
73
528
  ```
74
529
 
75
- Otherwise, we can build query based on user rights. In this case, we add system roles if user have special permission.
530
+ ### Public Resources
531
+
532
+ ```typescript
533
+ import * as s from '@pcg/auth/scopes';
534
+
535
+ // AUTHORIZED: 'tl:core:forms[public]:get'
536
+ // ORGANIZATION_ADMIN: 'tl:core:forms[assigned-org]:get' → resolved to org#tlorg:xxx
76
537
 
77
- ```tsx
78
- import { isGranted } from '@deep/auth'
79
- if(isGranted(user, 'js:roles:get', { scopes: ['system'] })) {
80
- where.or.push({
81
- type: 'system'
82
- })
538
+ @Query(() => Form)
539
+ @UsePermission('tl:core:forms:get')
540
+ async form(
541
+ @Args() input: FetchFormInput,
542
+ @ActionContextParam() ctx: ActionContext,
543
+ ) {
544
+ const form = await this.formsService.getOneByOrFail(input, ctx);
545
+
546
+ const items: s.ScopeItem[] = [
547
+ s.org(form.organizationId),
548
+ s.id(form.id),
549
+ ];
550
+
551
+ if (form.audienceType === FormAudienceType.PUBLIC) {
552
+ items.push('public');
553
+ }
554
+
555
+ ctx.validateAccess('tl:core:forms:get', items);
556
+
557
+ return form;
83
558
  }
84
559
  ```
560
+
561
+ ---
562
+
563
+ ## Preparing the User Object
564
+
565
+ Permission strings are stored in the database as an array of strings. Before checking access, they must be parsed into `resolvedPermissions`:
566
+
567
+ ```typescript
568
+ import { resolvePermissions } from '@pcg/auth';
569
+
570
+ const rawPermissions = [
571
+ 'js:core:episodes[org]:get',
572
+ 'js:core:episodes[published]:get',
573
+ 'js:core:episodes[org]:create',
574
+ 'js:mam:*[org]:*',
575
+ ];
576
+
577
+ const user = {
578
+ id: 'hcu:2x6g7l8n9op',
579
+ permissions: rawPermissions,
580
+ resolvedPermissions: resolvePermissions(rawPermissions),
581
+ };
582
+
583
+ // resolvedPermissions:
584
+ // [
585
+ // { id: 'js:core:episodes:get', scopes: ['org', 'published'] },
586
+ // { id: 'js:core:episodes:create', scopes: ['org'] },
587
+ // { id: 'js:mam:*:*', scopes: ['org'] },
588
+ // ]
589
+ ```
590
+
591
+ ---
592
+
593
+ ## Best Practices
594
+
595
+ 1. **Permission naming**: lowercase with colons — `js:core:episodes:get`
596
+ 2. **Standard actions**: `get`, `list`, `create`, `update`, `delete`
597
+ 3. **Scopes**: use `org` for organization, `id#` for entity
598
+ 4. **Functional builders**: prefer `@pcg/auth/scopes` over the `ActionScopes` class
599
+ 5. **Checks in resolvers**: all scope checks should be done in the resolver, NOT in the service layer
600
+ 6. **`@UsePermission`**: for basic access control at the guard level
601
+ 7. **`ctx.validateAccess()`**: for scope-specific checks inside the resolver
602
+ 8. **Do not use** `assigned-org`, `assigned-form`, `assigned-group` in action scopes — these are meant for role definitions and are automatically resolved to `org#xxx` / `id#xxx`
@@ -1,22 +1,5 @@
1
1
  //#region src/permissions/functional-scopes.ts
2
2
  /**
3
- * Create an ActionScopesArray from individual scope items.
4
- *
5
- * @example
6
- * scopes(org('jsorg:hci'))
7
- * // => ['org#jsorg:hci']
8
- *
9
- * scopes(org('jsorg:hci'), entityId('ep:123'))
10
- * // => ['org#jsorg:hci', 'id#ep:123']
11
- *
12
- * const o = org('jsorg:hci');
13
- * scopes(o, and(o, 'published'))
14
- * // => ['org#jsorg:hci', ['org#jsorg:hci', 'published']]
15
- */
16
- function scopes(...items) {
17
- return items;
18
- }
19
- /**
20
3
  * Create a wildcard scope that matches any permission scope.
21
4
  *
22
5
  * @example
@@ -92,5 +75,5 @@ function and(...items) {
92
75
  }
93
76
 
94
77
  //#endregion
95
- export { id as a, scopes as c, group as i, user as l, anyScope as n, org as o, form as r, scope as s, and as t };
96
- //# sourceMappingURL=functional-scopes-CsZNJMXW.js.map
78
+ export { id as a, user as c, group as i, anyScope as n, org as o, form as r, scope as s, and as t };
79
+ //# sourceMappingURL=functional-scopes-COiTBSy_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"functional-scopes-COiTBSy_.js","names":["id"],"sources":["../src/permissions/functional-scopes.ts"],"sourcesContent":["import { type ActionScopesArray } from './action-scopes.js';\n\nexport type ScopeItem = string | string[];\n\n/**\n * Create a wildcard scope that matches any permission scope.\n *\n * @example\n * isGranted(user, 'js:core:episodes:list', anyScope())\n */\nexport function anyScope(): ActionScopesArray {\n return ['*'];\n}\n\n/**\n * Create an organization scope.\n *\n * @example\n * org('jsorg:hci') // => 'org#jsorg:hci'\n */\nexport function org(id: string): string {\n return `org#${id}`;\n}\n\n/**\n * Create an entity id scope.\n *\n * @example\n * id('ep:123') // => 'id#ep:123'\n */\nexport function id(entityId: string): string {\n return `id#${entityId}`;\n}\n\n/**\n * Create a user scope.\n *\n * @example\n * user('hcu:xxx') // => 'user#hcu:xxx'\n */\nexport function user(id: string): string {\n return `user#${id}`;\n}\n\n/**\n * Create a form scope.\n *\n * @example\n * form('contact') // => 'form#contact'\n */\nexport function form(id: string): string {\n return `form#${id}`;\n}\n\n/**\n * Create a group scope.\n *\n * @example\n * group('hcgrp:ZT9') // => 'grp#hcgrp:ZT9'\n */\nexport function group(id: string): string {\n return `grp#${id}`;\n}\n\n/**\n * Create a named scope with an optional ID.\n *\n * @example\n * scope('published') // => 'published'\n * scope('orggroup', 'hcgrp:ZT9mt8j9lSR') // => 'orggroup#hcgrp:ZT9mt8j9lSR'\n */\nexport function scope(name: string, id?: string): string {\n return id ? `${name}#${id}` : name;\n}\n\n/**\n * Combine multiple scopes with AND logic (all must match).\n *\n * @example\n * and(org('jsorg:hci'), 'published')\n * // => ['org#jsorg:hci', 'published']\n */\nexport function and(...items: string[]): string[] {\n return items;\n}\n"],"mappings":";;;;;;;AAUA,SAAgB,WAA8B;AAC5C,QAAO,CAAC,IAAI;;;;;;;;AASd,SAAgB,IAAI,MAAoB;AACtC,QAAO,OAAOA;;;;;;;;AAShB,SAAgB,GAAG,UAA0B;AAC3C,QAAO,MAAM;;;;;;;;AASf,SAAgB,KAAK,MAAoB;AACvC,QAAO,QAAQA;;;;;;;;AASjB,SAAgB,KAAK,MAAoB;AACvC,QAAO,QAAQA;;;;;;;;AASjB,SAAgB,MAAM,MAAoB;AACxC,QAAO,OAAOA;;;;;;;;;AAUhB,SAAgB,MAAM,MAAc,MAAqB;AACvD,QAAOA,OAAK,GAAG,KAAK,GAAGA,SAAO;;;;;;;;;AAUhC,SAAgB,IAAI,GAAG,OAA2B;AAChD,QAAO"}
@@ -55,21 +55,6 @@ declare class ActionScopes {
55
55
  //#endregion
56
56
  //#region src/permissions/functional-scopes.d.ts
57
57
  type ScopeItem = string | string[];
58
- /**
59
- * Create an ActionScopesArray from individual scope items.
60
- *
61
- * @example
62
- * scopes(org('jsorg:hci'))
63
- * // => ['org#jsorg:hci']
64
- *
65
- * scopes(org('jsorg:hci'), entityId('ep:123'))
66
- * // => ['org#jsorg:hci', 'id#ep:123']
67
- *
68
- * const o = org('jsorg:hci');
69
- * scopes(o, and(o, 'published'))
70
- * // => ['org#jsorg:hci', ['org#jsorg:hci', 'published']]
71
- */
72
- declare function scopes(...items: ScopeItem[]): ActionScopesArray;
73
58
  /**
74
59
  * Create a wildcard scope that matches any permission scope.
75
60
  *
@@ -129,5 +114,5 @@ declare function scope(name: string, id?: string): string;
129
114
  */
130
115
  declare function and(...items: string[]): string[];
131
116
  //#endregion
132
- export { group as a, scope as c, ActionScopes as d, ActionScopesArray as f, form as i, scopes as l, and as n, id as o, anyScope as r, org as s, ScopeItem as t, user as u };
133
- //# sourceMappingURL=functional-scopes-DygK315q.d.ts.map
117
+ export { group as a, scope as c, ActionScopesArray as d, form as i, user as l, and as n, id as o, anyScope as r, org as s, ScopeItem as t, ActionScopes as u };
118
+ //# sourceMappingURL=functional-scopes-ut8Z6MmG.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"functional-scopes-ut8Z6MmG.d.ts","names":[],"sources":["../src/permissions/action-scopes.ts","../src/permissions/functional-scopes.ts"],"sourcesContent":[],"mappings":";KAAY,iBAAA;AAAZ;AAMA;;;AA6Dc,cA7DD,YAAA,CA6DC;EAAiB,KAAA,EA5Df,iBA4De;;;uBAxDR;ECTX;AAQZ;AAUA;AAUA;AAUA;AAUA;AAUA;AAWA;AAWA;;;;;;;;;;;;;;;;;;cDfc;;;;;;;;;;;;;;;;;;;AAnEF,KCEA,SAAA,GDFiB,MAAA,GAAA,MAAA,EAAA;AAM7B;;;;;;iBCIgB,QAAA,CAAA,GAAY;;AAR5B;AAQA;AAUA;AAUA;AAUA;AAUgB,iBA9BA,GAAA,CA8BI,EAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAUpB;AAWA;AAWA;;;;iBApDgB,EAAA;;;;;;;iBAUA,IAAA;;;;;;;iBAUA,IAAA;;;;;;;iBAUA,KAAA;;;;;;;;iBAWA,KAAA;;;;;;;;iBAWA,GAAA"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as group, c as scope, d as ActionScopes, f as ActionScopesArray, i as form, l as scopes, n as and, o as id, r as anyScope, s as org, t as ScopeItem, u as user } from "./functional-scopes-DygK315q.js";
1
+ import { a as group, c as scope, d as ActionScopesArray, i as form, l as user, n as and, o as id, r as anyScope, s as org, t as ScopeItem, u as ActionScopes } from "./functional-scopes-ut8Z6MmG.js";
2
2
 
3
3
  //#region src/types/permissions.d.ts
4
4
 
@@ -72,7 +72,7 @@ interface IUser {
72
72
  * @param actionScopes - action scopes (e.g. ['org#hcorg:hci'] or ActionScopes instance)
73
73
  * @returns - true if user has access to permission
74
74
  */
75
- declare const isGranted: (user: IUser, permission: string, actionScopes?: ActionScopesArray | ActionScopes) => boolean;
75
+ declare const isGranted: (user: IUser, permission: string, actionScopes?: ActionScopesArray | ActionScopes | string) => boolean;
76
76
  declare const encodeScopes: (scopes: (string | string[])[]) => string;
77
77
  /**
78
78
  * Inject scopes into permission
@@ -190,5 +190,5 @@ declare class ScopesBulder {
190
190
  replacePrefix(from: string, to: string): void;
191
191
  }
192
192
  //#endregion
193
- export { ActionScopes, ActionScopesArray, IUser, ReadonlyResolvedPermission, ResolvedPermission, ResolvedPermissionGroup, ScopeItem, ScopesBulder, and, anyScope, concatScopes, encodeScopes, form, group, id, injectScopesIntoPermission, isGranted, mergeResolvedPermissions, org, replaceScope, resolvePermission, resolvePermissionGroup, resolvePermissionGroups, resolvePermissions, scope, scopes, user };
193
+ export { ActionScopes, ActionScopesArray, IUser, ReadonlyResolvedPermission, ResolvedPermission, ResolvedPermissionGroup, ScopeItem, ScopesBulder, and, anyScope, concatScopes, encodeScopes, form, group, id, injectScopesIntoPermission, isGranted, mergeResolvedPermissions, org, replaceScope, resolvePermission, resolvePermissionGroup, resolvePermissionGroups, resolvePermissions, scope, user };
194
194
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types/permissions.ts","../src/types/user.ts","../src/permissions/helpers.ts","../src/permissions/scopes-bulder.ts"],"sourcesContent":[],"mappings":";;;;;;;AAKA;AAUA;AAKiB,UAfA,kBAAA,CAe0B;;;;AClB3C;;;;ACoCA;AACQ,UFxBS,uBAAA,CEwBT;EAEQ,EAAA,EAAA,MAAA;EAAoB,MAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA;;AAgCvB,UFrDI,0BAAA,CE2DhB;EAWY,SAAA,EAAA,EAAA,MAAA;EAqBA,SAAA,MAqBZ,EAAA,SAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA;AAUD;;;UD5IiB,KAAA;;ADGjB;AAUA;AAKA;;;;EClBiB,EAAA,EAAA,MAAK;;;;ACoCtB;;;;EAGqD,WAAA,EAAA,SAAA,MAAA,EAAA;EAgCxC;AAiBb;AAqBA;AA+BA;AAkCA;AAiCA;AAaA;AAOA;;;EAE8B,mBAAA,EDxMP,0BCwMO,EAAA;;;;AFlO9B;AAUA;AAKA;;;;AClBA;;;;ACoCA;;;;;AAmCa,cAnCA,SAyCZ,EAAA,CAAA,IAAA,EAxCO,KAwCP,EAAA,UAAA,EAAA,MAAA,EAAA,YAAA,CAAA,EAtCe,iBAsCf,GAtCmC,YAsCnC,EAAA,GAAA,OAAA;AAWY,cAjBA,YA4BZ,EAAA,CAAA,MAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,EAAA,GAAA,MAAA;AAUD;AA+BA;AAkCA;AAiCA;AAaA;AAOA;;;;AAE8B,cA7IjB,0BA6IiB,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,cAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,EAAA,GAAA,MAAA;AAmD9B;;;;ACxRA;;;;AAmCyC,cD0E5B,YC1E4B,EAAA,CAAA,GAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,EAAA,KAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,EAAA,GAAA,IAAA;;;;;;;;;cDyG5B,2CAA0C;;;;;;;;;cAkC1C,+CAEV;;;;;;;;;cA+BU,qDAAiD;;;;;;;;;cAajD,yDAEV;cAKU,mCACH,8BACA,yBAAoB;;;;;;;;;;;cAmDjB;;;;;cCxRA,YAAA;;;EHGI,OAAA,UAAA,CAAA,OAAkB,EGAN,YHAM,CAAA,EGAM,YHAN;EAUlB,KAAA,CAAA,CAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAuB,CAAA,EAAA;EAKvB,KAAA,CAAA,CAAA,EGPV,YHOU;;;;AClBjB;;;;ACoCA;;EAGgB,MAAA,CAAA,KAAA,EAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,IAAA;EAAoB;;AAgCpC;AAiBA;AAqBA;AA+BA;AAkCA;EAiCa,MAAA,CAAA,MAAA,EAAA,CAAA,MAAA,GAGZ,MAAA,EAAA,CAAA,EAAA,GC/KwC,YD4KqB,CAAA,EAAA,IAG7D;EAUY;AAOb;;;;;AAqDA;;;;ECxRa"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types/permissions.ts","../src/types/user.ts","../src/permissions/helpers.ts","../src/permissions/scopes-bulder.ts"],"sourcesContent":[],"mappings":";;;;;;;AAKA;AAUA;AAKiB,UAfA,kBAAA,CAe0B;;;;AClB3C;;;;ACwCA;AACQ,UF5BS,uBAAA,CE4BT;EAEQ,EAAA,EAAA,MAAA;EAAoB,MAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA;;AAgCvB,UFzDI,0BAAA,CE+DhB;EAWY,SAAA,EAAA,EAAA,MAAA;EAqBA,SAAA,MAqBZ,EAAA,SAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA;AAUD;;;UDhJiB,KAAA;;ADGjB;AAUA;AAKA;;;;EClBiB,EAAA,EAAA,MAAK;;;;ACwCtB;;;;EAGgD,WAAA,EAAA,SAAA,MAAA,EAAA;EAgCnC;AAiBb;AAqBA;AA+BA;AAkCA;AAiCA;AAaA;AAOA;;;EAE8B,mBAAA,ED5MP,0BC4MO,EAAA;;;;AFtO9B;AAUA;AAKA;;;;AClBA;;;;ACwCA;;;;;AAmCa,cAnCA,SAyCZ,EAAA,CAAA,IAAA,EAxCO,KAwCP,EAAA,UAAA,EAAA,MAAA,EAAA,YAAA,CAAA,EAtCe,iBAsCf,GAtCmC,YAsCnC,GAAA,MAAA,EAAA,GAAA,OAAA;AAWY,cAjBA,YA4BZ,EAAA,CAAA,MAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,EAAA,GAAA,MAAA;AAUD;AA+BA;AAkCA;AAiCA;AAaA;AAOA;;;;AAE8B,cA7IjB,0BA6IiB,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,cAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,EAAA,GAAA,MAAA;AAmD9B;;;;AC5RA;;;;AAmCyC,cD8E5B,YC9E4B,EAAA,CAAA,GAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,EAAA,KAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,EAAA,GAAA,IAAA;;;;;;;;;cD6G5B,2CAA0C;;;;;;;;;cAkC1C,+CAEV;;;;;;;;;cA+BU,qDAAiD;;;;;;;;;cAajD,yDAEV;cAKU,mCACH,8BACA,yBAAoB;;;;;;;;;;;cAmDjB;;;;;cC5RA,YAAA;;;EHGI,OAAA,UAAA,CAAA,OAAkB,EGAN,YHAM,CAAA,EGAM,YHAN;EAUlB,KAAA,CAAA,CAAA,EAAA,CAAA,MAAA,GAAA,MAAA,EAAuB,CAAA,EAAA;EAKvB,KAAA,CAAA,CAAA,EGPV,YHOU;;;;AClBjB;;;;ACwCA;;EAGgB,MAAA,CAAA,KAAA,EAAA,MAAA,GAAA,MAAA,EAAA,CAAA,EAAA,IAAA;EAAoB;;AAgCpC;AAiBA;AAqBA;AA+BA;AAkCA;EAiCa,MAAA,CAAA,MAAA,EAAA,CAAA,MAAA,GAGZ,MAAA,EAAA,CAAA,EAAA,GCnLwC,YDgLqB,CAAA,EAAA,IAG7D;EAUY;AAOb;;;;;AAqDA;;;;EC5Ra"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as id, c as scopes, i as group, l as user, n as anyScope, o as org, r as form, s as scope, t as and } from "./functional-scopes-CsZNJMXW.js";
1
+ import { a as id, c as user, i as group, n as anyScope, o as org, r as form, s as scope, t as and } from "./functional-scopes-COiTBSy_.js";
2
2
 
3
3
  //#region src/permissions/action-scopes.ts
4
4
  /**
@@ -9,8 +9,8 @@ var ActionScopes = class {
9
9
  array = [];
10
10
  anyScope = false;
11
11
  idsMap = /* @__PURE__ */ new Map();
12
- constructor(scopes$1) {
13
- if (scopes$1) this.array = scopes$1;
12
+ constructor(scopes) {
13
+ if (scopes) this.array = scopes;
14
14
  }
15
15
  /**
16
16
  * Set id scope (e.g. 'id#jsu:123')
@@ -103,9 +103,10 @@ var ActionScopes = class {
103
103
  *
104
104
  * scopes.set('my')
105
105
  */
106
- const normalizeScopes = (scopes$1) => {
107
- if (!(scopes$1 instanceof ActionScopes)) return new ActionScopes(scopes$1);
108
- return scopes$1;
106
+ const normalizeScopes = (scopes) => {
107
+ if (scopes instanceof ActionScopes) return scopes;
108
+ if (typeof scopes === "string") return new ActionScopes([scopes]);
109
+ return new ActionScopes(scopes);
109
110
  };
110
111
  /**
111
112
  * Check if user has access to permission
@@ -126,14 +127,14 @@ const isGranted = (user$1, permission, actionScopes = []) => {
126
127
  const [service, module, resource, action] = permission.split(":");
127
128
  if (user$1.resolvedPermissions.length === 0) return false;
128
129
  const userPermissions = user$1.resolvedPermissions;
129
- const scopes$1 = normalizeScopes(actionScopes);
130
- const permissionCanBeExecutedInScopes = (perm, scopes$2) => perm.scopes.length === 0 || scopes$2.canBeExecutedInScopes(perm.scopes);
130
+ const scopes = normalizeScopes(actionScopes);
131
+ const permissionCanBeExecutedInScopes = (perm, scopes$1) => perm.scopes.length === 0 || scopes$1.canBeExecutedInScopes(perm.scopes);
131
132
  const regexp = /* @__PURE__ */ new RegExp(`^(${service}|\\*):(${module}|\\*):(${resource}|\\*):(${action}|\\*)$`);
132
- if (userPermissions.some((perm) => regexp.test(perm.id) && permissionCanBeExecutedInScopes(perm, scopes$1))) return true;
133
+ if (userPermissions.some((perm) => regexp.test(perm.id) && permissionCanBeExecutedInScopes(perm, scopes))) return true;
133
134
  return false;
134
135
  };
135
- const encodeScopes = (scopes$1) => {
136
- const scopesString = scopes$1.map((s) => Array.isArray(s) ? s.join("+") : s).join(",");
136
+ const encodeScopes = (scopes) => {
137
+ const scopesString = scopes.map((s) => Array.isArray(s) ? s.join("+") : s).join(",");
137
138
  return scopesString.length > 0 ? `[${scopesString}]` : "";
138
139
  };
139
140
  /**
@@ -146,10 +147,10 @@ const encodeScopes = (scopes$1) => {
146
147
  * // js:core:episodes[org,published]:get
147
148
  */
148
149
  const injectScopesIntoPermission = (permission, scopesToInject) => {
149
- const { id: id$1, scopes: scopes$1 } = resolvePermission(permission);
150
+ const { id: id$1, scopes } = resolvePermission(permission);
150
151
  const [service, module, resource, action] = id$1.split(":");
151
- concatScopes(scopes$1, scopesToInject);
152
- return `${service}:${module}:${resource}${encodeScopes(scopes$1)}:${action}`;
152
+ concatScopes(scopes, scopesToInject);
153
+ return `${service}:${module}:${resource}${encodeScopes(scopes)}:${action}`;
153
154
  };
154
155
  /**
155
156
  * Concat scopes
@@ -175,13 +176,13 @@ const concatScopes = (set, items) => {
175
176
  const resolvePermission = (permission) => {
176
177
  const matches = /\[(?<scopes>.*?)\]/u.exec(permission);
177
178
  if (matches?.[0] && matches.groups?.scopes) {
178
- const scopes$1 = matches.groups.scopes.split(",").map((s) => {
179
+ const scopes = matches.groups.scopes.split(",").map((s) => {
179
180
  if (s.includes("+")) return s.split("+");
180
181
  return s;
181
182
  });
182
183
  return {
183
184
  id: permission.replace(matches[0], ""),
184
- scopes: scopes$1
185
+ scopes
185
186
  };
186
187
  }
187
188
  return {
@@ -200,12 +201,12 @@ const resolvePermission = (permission) => {
200
201
  const resolvePermissions = (permissions) => {
201
202
  const resolvedPermissions = [];
202
203
  for (const permission of permissions) {
203
- const { id: id$1, scopes: scopes$1 } = resolvePermission(permission);
204
+ const { id: id$1, scopes } = resolvePermission(permission);
204
205
  const currentPermission = resolvedPermissions.find((p) => p.id === id$1);
205
- if (currentPermission) concatScopes(currentPermission.scopes, scopes$1);
206
+ if (currentPermission) concatScopes(currentPermission.scopes, scopes);
206
207
  else resolvedPermissions.push({
207
208
  id: id$1,
208
- scopes: scopes$1
209
+ scopes
209
210
  });
210
211
  }
211
212
  return resolvedPermissions;
@@ -243,11 +244,11 @@ const mergeResolvedPermissions = (array1, array2) => {
243
244
  });
244
245
  continue;
245
246
  }
246
- const scopes$1 = [...rp1.scopes];
247
- for (const rp2 of rps2) concatScopes(scopes$1, rp2.scopes);
247
+ const scopes = [...rp1.scopes];
248
+ for (const rp2 of rps2) concatScopes(scopes, rp2.scopes);
248
249
  result.push({
249
250
  id: rp1.id,
250
- scopes: scopes$1
251
+ scopes
251
252
  });
252
253
  }
253
254
  for (const rp2 of array2) if (!result.some((rp) => rp.id === rp2.id)) result.push(rp2);
@@ -263,9 +264,9 @@ const mergeResolvedPermissions = (array1, array2) => {
263
264
  * replaceScope(['org#jsorg:xxx', ['org#jsorg:xxx', 'published']], 'org#jsorg:xxx', 'org#jsorg:hci')
264
265
  * // ['org#jsorg:hci', ['org#jsorg:hci', 'published']]
265
266
  */
266
- const replaceScope = (scopes$1, from, to) => {
267
+ const replaceScope = (scopes, from, to) => {
267
268
  const result = [];
268
- for (const scope$1 of scopes$1) if (Array.isArray(scope$1)) result.push(replaceScope(scope$1, from, to));
269
+ for (const scope$1 of scopes) if (Array.isArray(scope$1)) result.push(replaceScope(scope$1, from, to));
269
270
  else if (scope$1 === from) if (Array.isArray(to)) result.push(...to);
270
271
  else result.push(to);
271
272
  else result.push(scope$1);
@@ -275,8 +276,8 @@ const replaceScope = (scopes$1, from, to) => {
275
276
  //#endregion
276
277
  //#region src/permissions/scopes-bulder.ts
277
278
  var ScopesBulder = class ScopesBulder {
278
- constructor(scopes$1 = []) {
279
- this.scopes = scopes$1;
279
+ constructor(scopes = []) {
280
+ this.scopes = scopes;
280
281
  }
281
282
  static fromBulder(builder) {
282
283
  return new ScopesBulder(builder.scopes.slice());
@@ -306,9 +307,9 @@ var ScopesBulder = class ScopesBulder {
306
307
  * extend(['lang#en', 'lang#de'])
307
308
  * // ['published', 'draft'] => ['published', 'draft', 'lang#en', 'lang#de']
308
309
  */
309
- extend(scopes$1) {
310
- if (Array.isArray(scopes$1)) concatScopes(this.scopes, scopes$1);
311
- else if (scopes$1 instanceof ScopesBulder) concatScopes(this.scopes, scopes$1.scopes);
310
+ extend(scopes) {
311
+ if (Array.isArray(scopes)) concatScopes(this.scopes, scopes);
312
+ else if (scopes instanceof ScopesBulder) concatScopes(this.scopes, scopes.scopes);
312
313
  }
313
314
  /**
314
315
  * Join all scopes with the given scope
@@ -348,5 +349,5 @@ var ScopesBulder = class ScopesBulder {
348
349
  };
349
350
 
350
351
  //#endregion
351
- export { ActionScopes, ScopesBulder, and, anyScope, concatScopes, encodeScopes, form, group, id, injectScopesIntoPermission, isGranted, mergeResolvedPermissions, org, replaceScope, resolvePermission, resolvePermissionGroup, resolvePermissionGroups, resolvePermissions, scope, scopes, user };
352
+ export { ActionScopes, ScopesBulder, and, anyScope, concatScopes, encodeScopes, form, group, id, injectScopesIntoPermission, isGranted, mergeResolvedPermissions, org, replaceScope, resolvePermission, resolvePermissionGroup, resolvePermissionGroups, resolvePermissions, scope, user };
352
353
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["scopes","id","scope","scopes","user","id","scope","scopes: (string | string[])[]","resolvedPermissions: ResolvedPermission[]","result: ResolvedPermission[]","result: (string | string[])[]","scopes: (string | string[])[]","scope","scopes","result: (string | string[])[]"],"sources":["../src/permissions/action-scopes.ts","../src/permissions/helpers.ts","../src/permissions/scopes-bulder.ts"],"sourcesContent":["export type ActionScopesArray = (string | string[])[];\n\n/**\n * Action scopes are used to describe\n * what scopes are required to execute an action.\n */\nexport class ActionScopes {\n public array: ActionScopesArray = [];\n public anyScope = false;\n private idsMap = new Map<string, string>();\n\n constructor(scopes?: ActionScopesArray) {\n if (scopes) {\n this.array = scopes;\n }\n }\n\n /**\n * Set id scope (e.g. 'id#jsu:123')\n * @param id - entity id\n * @returns - this\n * @example\n * scopes.setEntityId('jsu:123')\n *\n * // scopes.getArray() = ['id#jsu:123']\n */\n setEntityId(id: string): this {\n this.set(`id#${id}`);\n\n return this;\n }\n\n /**\n * Set scope (e.g. 'org', 'org+review')\n * @param scope - scope name (e.g. 'org', 'org+review')\n * @param id - entity id (e.g. 'jsorg:hci')\n * @returns - this\n * @example\n * scopes.set('my')\n *\n * // scopes.getArray() = ['my']\n *\n * scopes.set('org', 'jsorg:hci')\n *\n * // scopes.getArray() = ['org#jsorg:hci']\n */\n set(scope: string, id?: string): this {\n if (id && /^[A-Za-z_]+$/.test(scope)) {\n this.idsMap.set(scope, id);\n }\n\n if (scope.includes('+')) {\n const subscopes = scope.split('+');\n this.array.push(\n subscopes.map((subscope) => this.resolveScopeId(subscope)),\n );\n } else {\n this.array.push(this.resolveScopeId(scope));\n }\n\n return this;\n }\n\n has(scope: string): boolean {\n return this.array.includes(scope);\n }\n\n getArray(): ActionScopesArray {\n return [...this.array];\n }\n\n /**\n * Permission scopes must cover auth scopes determined in auth context.\n * @param permissionScopes\n */\n canBeExecutedInScopes(\n permissionScopes: readonly (string | string[])[],\n ): boolean {\n // if scopes has * then it can be executed in any scope\n if (this.array.includes('*')) {\n return true;\n }\n\n /* action scopes = ['user#jsu:123', 'org#hci', ['org#hci', 'draft']]\n /* permission scopes: ['user#jsu:123', ['org#hci', 'draft']]\n\n /**\n * empty user permission scopes = full access to action\n */\n if (permissionScopes.length === 0) {\n return true;\n }\n\n /**\n * Empty auth scopes but user has permission scopes = no access to action\n *\n * action scopes = []\n * permission scopes = ['org#hci']\n *\n * Example:\n * User try to get list of all entities\n * But user only has permission to entity from org#hci\n */\n if (this.array.length === 0) {\n return false;\n }\n\n return this.array.some((authScope) => {\n if (Array.isArray(authScope)) {\n // scopes ['org#hci', 'draft'] == permissionScope ['org#hci', 'draft']\n\n return permissionScopes.some((permissionScope) => {\n if (Array.isArray(permissionScope)) {\n return permissionScope.every(\n (sub) => authScope.includes(sub),\n );\n }\n\n return authScope.includes(permissionScope);\n });\n }\n\n return permissionScopes.includes(authScope); // 'user#jsu:123' == 'user#jsu:123'\n });\n }\n\n /**\n * Resolve presaved scopes\n * @example\n * authCtx.setScope('org', 'jsorg:hci')\n * [org#hci]\n *\n * authCtx.setScope('org+review');\n * [org#hci, [org#hci, review]]\n **/\n private resolveScopeId(scope: string) {\n const id = this.idsMap.get(scope);\n return id !== undefined\n ? `${scope}#${id}`\n : scope;\n }\n}\n","import {\n type ReadonlyResolvedPermission, type ResolvedPermission, type ResolvedPermissionGroup,\n} from '../types/permissions.js';\nimport { type IUser } from '../types/user.js';\nimport { ActionScopes, type ActionScopesArray } from './action-scopes.js';\n\n/**\n * Normilize scopes to ActionScopes\n * @example\n * const scopes = normalizeScopes(['org#hci', 'user#hcu:xxxxx') // ActionScopes\n *\n * scopes.set('my')\n */\nconst normalizeScopes = (\n scopes: ActionScopesArray | ActionScopes,\n): ActionScopes => {\n if (!(scopes instanceof ActionScopes)) {\n return new ActionScopes(scopes);\n }\n\n return scopes;\n};\n\n/**\n * Check if user has access to permission\n * @example\n *\n * // Check episode access\n * const scopes = new ActionScopes();\n * scopes.setEntityId(episode.id); // id#js:core:episodes:xxxxx\n * scopes.set('org', episode.organizationId); // org#hcorg:hci\n *\n * isGranted(user, 'js:core:episodes:get', scopes)\n * @param user - user object with resolvedPermissions\n * @param permission - permission string (e.g. 'js:core:episodes:get')\n * @param actionScopes - action scopes (e.g. ['org#hcorg:hci'] or ActionScopes instance)\n * @returns - true if user has access to permission\n */\nexport const isGranted = (\n user: IUser,\n permission: string,\n actionScopes: ActionScopesArray | ActionScopes = [],\n): boolean => {\n const [service, module, resource, action] = permission.split(':');\n\n if (user.resolvedPermissions.length === 0) {\n return false;\n }\n\n const userPermissions = user.resolvedPermissions;\n\n const scopes = normalizeScopes(actionScopes);\n\n const permissionCanBeExecutedInScopes = (perm: ReadonlyResolvedPermission, scopes: ActionScopes) =>\n (perm.scopes.length === 0 || scopes.canBeExecutedInScopes(perm.scopes));\n\n const regexp = new RegExp(`^(${service}|\\\\*):(${module}|\\\\*):(${resource}|\\\\*):(${action}|\\\\*)$`);\n\n // novajs:*:*:* or novajs:*:*[org]:*\n // novajs:module:*:* or novajs:module:*[org]:*\n // novajs:module:resource:* or novajs:module:resource:[org]:*\n // novajs:modules:resource:create\n // *:modules:resource:create\n if (userPermissions.some(\n (perm) => regexp.test(perm.id)\n && permissionCanBeExecutedInScopes(perm, scopes))\n ) {\n return true;\n }\n\n return false;\n};\n\nexport const encodeScopes = (scopes: (string | string[])[]): string => {\n const scopesString = scopes\n .map((s) => (Array.isArray(s) ? s.join('+') : s))\n .join(',');\n\n return scopesString.length > 0 ? `[${scopesString}]` : '';\n};\n\n/**\n * Inject scopes into permission\n * @param permission - permission string (e.g. 'js:core:episodes:get')\n * @param scopesToInject - scopes to inject (e.g. ['org', 'published'])\n * @returns - permission string with injected scopes (e.g. 'js:core:episodes[org,published]:get')\n * @example\n * injectScopesIntoPermission('js:core:episodes:get', ['org', 'published'])\n * // js:core:episodes[org,published]:get\n */\nexport const injectScopesIntoPermission = (\n permission: string,\n scopesToInject: (string | string[])[],\n) => {\n const { id, scopes } = resolvePermission(permission);\n\n const [service, module, resource, action] = id.split(':');\n\n concatScopes(scopes, scopesToInject);\n\n return `${service}:${module}:${resource}${encodeScopes(scopes)}:${action}`;\n};\n\n/**\n * Concat scopes\n * @param set - set of scopes\n * @param items - items to concat\n * @example\n * concatScopes(['org#hci'], ['org#dv'])\n * // ['org#hci', 'org#dv']\n */\nexport const concatScopes = (\n set: (string | string[])[],\n items: (string | string[])[],\n): void => {\n for (const item of items) {\n if (Array.isArray(item)) {\n // check if scopes not includes array with subscopes rp2Scope\n if (\n !set.some(\n (scope) =>\n Array.isArray(scope)\n && scope.length === item.length\n && scope.every((s) => item.includes(s)),\n )\n ) {\n set.push(item);\n }\n } else if (!set.includes(item)) {\n set.push(item);\n }\n }\n};\n\n/**\n * Resolve permission string to object with scopes\n * @param permission - permission string\n * @returns - resolved permission object\n * @example\n * resolvePermission('js:core:episodes[org,published]:get')\n * // { id: 'js:core:episodes:get', scopes: ['org', 'published'] }\n */\nexport const resolvePermission = (permission: string): ResolvedPermission => {\n const matches = /\\[(?<scopes>.*?)\\]/u.exec(permission);\n\n if (matches?.[0] && matches.groups?.scopes) {\n const scopes: (string | string[])[] = matches.groups.scopes\n .split(',') // scopes with OR logic\n .map((s) => {\n if (s.includes('+')) {\n return s.split('+'); // scopes with AND logic\n }\n\n return s;\n });\n\n return {\n id: permission.replace(matches[0], ''),\n scopes,\n };\n }\n\n return {\n id: permission,\n scopes: [],\n };\n};\n\n/**\n * Resolve permissions to array of resolved permissions\n * @param permissions - array of permissions\n * @returns - array of resolved permissions\n * @example\n * resolvePermissions(['js:core:episodes[org]:get', 'js:core:episodes[published]:get'])\n * // [{ id: 'js:core:episodes:get', scopes: ['org', 'published'] }]\n */\nexport const resolvePermissions = (\n permissions: string[],\n): ResolvedPermission[] => {\n const resolvedPermissions: ResolvedPermission[] = [];\n\n for (const permission of permissions) {\n const { id, scopes } = resolvePermission(permission);\n\n const currentPermission = resolvedPermissions.find(\n (p) => p.id === id,\n );\n\n if (currentPermission) {\n concatScopes(currentPermission.scopes, scopes);\n } else {\n resolvedPermissions.push({\n id,\n scopes,\n });\n }\n }\n\n return resolvedPermissions;\n};\n\n/**\n * Resolve permission group string to object with scopes\n * @param permissionGroup - permission group string\n * @returns - resolved permission group object\n * @example\n * resolvePermissionGroup('g:js:core:episodes[org]:read')\n * // { id: 'g:js:core:episodes:read', scopes: ['org'] }\n */\nexport const resolvePermissionGroup = (permissionGroup: string) => {\n // Currently it has the same algorithm\n return resolvePermission(permissionGroup);\n};\n\n/**\n * Resolve permission groups to array of resolved permission groups\n * @param permissionGroups - array of permission groups\n * @returns - array of resolved permission groups\n * @example\n * resolvePermissionGroups(['g:js:core:episodes[org]:read', 'g:js:core:episodes[published]:read'])\n * // [{ id: 'g:js:core:episodes:read', scopes: ['org', 'published'] }]\n */\nexport const resolvePermissionGroups = (\n permissionGroups: string[],\n): ResolvedPermissionGroup[] => {\n // Currently it has the same algorithm\n return resolvePermissions(permissionGroups);\n};\n\nexport const mergeResolvedPermissions = (\n array1: ResolvedPermission[],\n array2: ResolvedPermission[],\n) => {\n const result: ResolvedPermission[] = [];\n\n for (const rp1 of array1) {\n const rps2 = array2.filter((rp2) => rp2.id === rp1.id);\n\n /* Empty scopes = has full access */\n if (\n rp1.scopes.length === 0\n || rps2.some((rp2) => rp2.id === rp1.id && rp2.scopes.length === 0)\n ) {\n result.push({\n id: rp1.id,\n scopes: [],\n });\n\n continue;\n }\n\n // [org#hci, user#hcu:xxxxx, [org#hci, review]]\n const scopes = [...rp1.scopes];\n for (const rp2 of rps2) {\n concatScopes(scopes, rp2.scopes);\n }\n\n result.push({\n id: rp1.id,\n scopes,\n });\n }\n\n for (const rp2 of array2) {\n if (!result.some((rp) => rp.id === rp2.id)) {\n result.push(rp2);\n }\n }\n\n return result;\n};\n\n/**\n * Replace scope in array of scopes\n * @param scopes - array of scopes\n * @param from - scope to replace\n * @param to - scope to replace with\n * @returns - array of scopes with replaced scopes\n * @example\n * replaceScope(['org#jsorg:xxx', ['org#jsorg:xxx', 'published']], 'org#jsorg:xxx', 'org#jsorg:hci')\n * // ['org#jsorg:hci', ['org#jsorg:hci', 'published']]\n */\nexport const replaceScope = (\n scopes: (string | string[])[], // [org#jsorg:xxx, [org#jsorg:xxx, published]]\n from: string,\n to: string | (string | string[])[],\n) => {\n const result: (string | string[])[] = [];\n for (const scope of scopes) {\n if (Array.isArray(scope)) {\n result.push(replaceScope(scope, from, to) as string[]);\n } else if (scope === from) {\n if (Array.isArray(to)) {\n result.push(...to);\n } else {\n result.push(to);\n }\n } else {\n result.push(scope);\n }\n }\n\n return result;\n};\n","import { concatScopes } from './helpers.js';\n\nexport class ScopesBulder {\n constructor(private scopes: (string | string[])[] = []) {}\n\n static fromBulder(builder: ScopesBulder) {\n return new ScopesBulder(builder.scopes.slice());\n }\n\n build() {\n return JSON.parse(JSON.stringify(this.scopes)) as (string | string[])[];\n }\n\n clone() {\n return ScopesBulder.fromBulder(this);\n }\n\n /**\n * Append scope or scopes to the current scopes\n * @param scope - scope or array of scopes to append\n * @example\n * append('org#jsorg:hci')\n * // ['org#xxx'] => ['org#xxx', 'org#jsorg:hci']\n * append(['org#hci', 'lang#en])\n * // ['org#xxx'] => ['org#xxx', ['org#hci', 'lang#en']]\n */\n append(scope: string | string[]) {\n this.scopes.push(scope);\n }\n\n /**\n * Extend current scopes with the given scopes\n * @param scopes - scope or array of scopes to extend with\n * @example\n * extend(['lang#en', 'lang#de'])\n * // ['published', 'draft'] => ['published', 'draft', 'lang#en', 'lang#de']\n */\n extend(scopes: (string | string[])[] | ScopesBulder) {\n if (Array.isArray(scopes)) {\n concatScopes(this.scopes, scopes);\n } else if (scopes instanceof ScopesBulder) {\n concatScopes(this.scopes, scopes.scopes);\n }\n }\n\n /**\n * Join all scopes with the given scope\n * @param scope - scope to join with\n * @example\n * join('org#jsorg:hci', 'before')\n * // ['published', 'draft'] => [['org#jsorg:hci', 'published'], ['org#jsorg:hci', 'draft']]\n * join(['lang#en', 'lang#de'])\n * // ['published', 'draft'] => [['published', 'lang#en'], ['published', 'lang#de'], ['draft', 'lang#en'], ['draft', 'lang#de']]\n */\n join(scopeOrScopes: string | (string | string[])[], pos: 'before' | 'after' = 'after') {\n const result: (string | string[])[] = [];\n\n const scopesToJoin = Array.isArray(scopeOrScopes) ? scopeOrScopes : [scopeOrScopes];\n\n if (scopesToJoin.length === 0) {\n return;\n }\n\n const currentScopesCopy = this.build();\n for (const scope of scopesToJoin) {\n const scopeToJoin = Array.isArray(scope) ? scope : [scope];\n\n for (const s of currentScopesCopy) {\n if (Array.isArray(s)) {\n result.push(pos === 'before' ? [...scopeToJoin, ...s] : [...s, ...scopeToJoin]);\n } else {\n result.push(pos === 'before' ? [...scopeToJoin, s] : [s, ...scopeToJoin]);\n }\n }\n }\n\n this.scopes = result;\n }\n\n /**\n * Replace prefix in all scopes\n * @param from - prefix to replace\n * @param to - prefix to replace with\n * @example\n * replacePrefix('org', 'id')\n * ['org#jsorg:hci'] => ['id#jsorg:hci']\n */\n replacePrefix(from: string, to: string) {\n const result: (string | string[])[] = [];\n for (const scope of this.scopes) {\n if (Array.isArray(scope)) {\n result.push(scope.map((s) => s.replace(`${from}#`, `${to}#`)));\n } else {\n result.push(scope.replace(`${from}#`, `${to}#`));\n }\n }\n this.scopes = result;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["id","scope","user","scopes","id","scope","scopes: (string | string[])[]","resolvedPermissions: ResolvedPermission[]","result: ResolvedPermission[]","result: (string | string[])[]","scopes: (string | string[])[]","scope","result: (string | string[])[]"],"sources":["../src/permissions/action-scopes.ts","../src/permissions/helpers.ts","../src/permissions/scopes-bulder.ts"],"sourcesContent":["export type ActionScopesArray = (string | string[])[];\n\n/**\n * Action scopes are used to describe\n * what scopes are required to execute an action.\n */\nexport class ActionScopes {\n public array: ActionScopesArray = [];\n public anyScope = false;\n private idsMap = new Map<string, string>();\n\n constructor(scopes?: ActionScopesArray) {\n if (scopes) {\n this.array = scopes;\n }\n }\n\n /**\n * Set id scope (e.g. 'id#jsu:123')\n * @param id - entity id\n * @returns - this\n * @example\n * scopes.setEntityId('jsu:123')\n *\n * // scopes.getArray() = ['id#jsu:123']\n */\n setEntityId(id: string): this {\n this.set(`id#${id}`);\n\n return this;\n }\n\n /**\n * Set scope (e.g. 'org', 'org+review')\n * @param scope - scope name (e.g. 'org', 'org+review')\n * @param id - entity id (e.g. 'jsorg:hci')\n * @returns - this\n * @example\n * scopes.set('my')\n *\n * // scopes.getArray() = ['my']\n *\n * scopes.set('org', 'jsorg:hci')\n *\n * // scopes.getArray() = ['org#jsorg:hci']\n */\n set(scope: string, id?: string): this {\n if (id && /^[A-Za-z_]+$/.test(scope)) {\n this.idsMap.set(scope, id);\n }\n\n if (scope.includes('+')) {\n const subscopes = scope.split('+');\n this.array.push(\n subscopes.map((subscope) => this.resolveScopeId(subscope)),\n );\n } else {\n this.array.push(this.resolveScopeId(scope));\n }\n\n return this;\n }\n\n has(scope: string): boolean {\n return this.array.includes(scope);\n }\n\n getArray(): ActionScopesArray {\n return [...this.array];\n }\n\n /**\n * Permission scopes must cover auth scopes determined in auth context.\n * @param permissionScopes\n */\n canBeExecutedInScopes(\n permissionScopes: readonly (string | string[])[],\n ): boolean {\n // if scopes has * then it can be executed in any scope\n if (this.array.includes('*')) {\n return true;\n }\n\n /* action scopes = ['user#jsu:123', 'org#hci', ['org#hci', 'draft']]\n /* permission scopes: ['user#jsu:123', ['org#hci', 'draft']]\n\n /**\n * empty user permission scopes = full access to action\n */\n if (permissionScopes.length === 0) {\n return true;\n }\n\n /**\n * Empty auth scopes but user has permission scopes = no access to action\n *\n * action scopes = []\n * permission scopes = ['org#hci']\n *\n * Example:\n * User try to get list of all entities\n * But user only has permission to entity from org#hci\n */\n if (this.array.length === 0) {\n return false;\n }\n\n return this.array.some((authScope) => {\n if (Array.isArray(authScope)) {\n // scopes ['org#hci', 'draft'] == permissionScope ['org#hci', 'draft']\n\n return permissionScopes.some((permissionScope) => {\n if (Array.isArray(permissionScope)) {\n return permissionScope.every(\n (sub) => authScope.includes(sub),\n );\n }\n\n return authScope.includes(permissionScope);\n });\n }\n\n return permissionScopes.includes(authScope); // 'user#jsu:123' == 'user#jsu:123'\n });\n }\n\n /**\n * Resolve presaved scopes\n * @example\n * authCtx.setScope('org', 'jsorg:hci')\n * [org#hci]\n *\n * authCtx.setScope('org+review');\n * [org#hci, [org#hci, review]]\n **/\n private resolveScopeId(scope: string) {\n const id = this.idsMap.get(scope);\n return id !== undefined\n ? `${scope}#${id}`\n : scope;\n }\n}\n","import {\n type ReadonlyResolvedPermission, type ResolvedPermission, type ResolvedPermissionGroup,\n} from '../types/permissions.js';\nimport { type IUser } from '../types/user.js';\nimport { ActionScopes, type ActionScopesArray } from './action-scopes.js';\n\n/**\n * Normilize scopes to ActionScopes\n * @example\n * const scopes = normalizeScopes(['org#hci', 'user#hcu:xxxxx') // ActionScopes\n *\n * scopes.set('my')\n */\nconst normalizeScopes = (\n scopes: ActionScopesArray | ActionScopes | string,\n): ActionScopes => {\n if (scopes instanceof ActionScopes) {\n return scopes;\n }\n\n if (typeof scopes === 'string') {\n return new ActionScopes([scopes]);\n }\n\n return new ActionScopes(scopes);\n};\n\n/**\n * Check if user has access to permission\n * @example\n *\n * // Check episode access\n * const scopes = new ActionScopes();\n * scopes.setEntityId(episode.id); // id#js:core:episodes:xxxxx\n * scopes.set('org', episode.organizationId); // org#hcorg:hci\n *\n * isGranted(user, 'js:core:episodes:get', scopes)\n * @param user - user object with resolvedPermissions\n * @param permission - permission string (e.g. 'js:core:episodes:get')\n * @param actionScopes - action scopes (e.g. ['org#hcorg:hci'] or ActionScopes instance)\n * @returns - true if user has access to permission\n */\nexport const isGranted = (\n user: IUser,\n permission: string,\n actionScopes: ActionScopesArray | ActionScopes | string = [],\n): boolean => {\n const [service, module, resource, action] = permission.split(':');\n\n if (user.resolvedPermissions.length === 0) {\n return false;\n }\n\n const userPermissions = user.resolvedPermissions;\n\n const scopes = normalizeScopes(actionScopes);\n\n const permissionCanBeExecutedInScopes = (perm: ReadonlyResolvedPermission, scopes: ActionScopes) =>\n (perm.scopes.length === 0 || scopes.canBeExecutedInScopes(perm.scopes));\n\n const regexp = new RegExp(`^(${service}|\\\\*):(${module}|\\\\*):(${resource}|\\\\*):(${action}|\\\\*)$`);\n\n // novajs:*:*:* or novajs:*:*[org]:*\n // novajs:module:*:* or novajs:module:*[org]:*\n // novajs:module:resource:* or novajs:module:resource:[org]:*\n // novajs:modules:resource:create\n // *:modules:resource:create\n if (userPermissions.some(\n (perm) => regexp.test(perm.id)\n && permissionCanBeExecutedInScopes(perm, scopes))\n ) {\n return true;\n }\n\n return false;\n};\n\nexport const encodeScopes = (scopes: (string | string[])[]): string => {\n const scopesString = scopes\n .map((s) => (Array.isArray(s) ? s.join('+') : s))\n .join(',');\n\n return scopesString.length > 0 ? `[${scopesString}]` : '';\n};\n\n/**\n * Inject scopes into permission\n * @param permission - permission string (e.g. 'js:core:episodes:get')\n * @param scopesToInject - scopes to inject (e.g. ['org', 'published'])\n * @returns - permission string with injected scopes (e.g. 'js:core:episodes[org,published]:get')\n * @example\n * injectScopesIntoPermission('js:core:episodes:get', ['org', 'published'])\n * // js:core:episodes[org,published]:get\n */\nexport const injectScopesIntoPermission = (\n permission: string,\n scopesToInject: (string | string[])[],\n) => {\n const { id, scopes } = resolvePermission(permission);\n\n const [service, module, resource, action] = id.split(':');\n\n concatScopes(scopes, scopesToInject);\n\n return `${service}:${module}:${resource}${encodeScopes(scopes)}:${action}`;\n};\n\n/**\n * Concat scopes\n * @param set - set of scopes\n * @param items - items to concat\n * @example\n * concatScopes(['org#hci'], ['org#dv'])\n * // ['org#hci', 'org#dv']\n */\nexport const concatScopes = (\n set: (string | string[])[],\n items: (string | string[])[],\n): void => {\n for (const item of items) {\n if (Array.isArray(item)) {\n // check if scopes not includes array with subscopes rp2Scope\n if (\n !set.some(\n (scope) =>\n Array.isArray(scope)\n && scope.length === item.length\n && scope.every((s) => item.includes(s)),\n )\n ) {\n set.push(item);\n }\n } else if (!set.includes(item)) {\n set.push(item);\n }\n }\n};\n\n/**\n * Resolve permission string to object with scopes\n * @param permission - permission string\n * @returns - resolved permission object\n * @example\n * resolvePermission('js:core:episodes[org,published]:get')\n * // { id: 'js:core:episodes:get', scopes: ['org', 'published'] }\n */\nexport const resolvePermission = (permission: string): ResolvedPermission => {\n const matches = /\\[(?<scopes>.*?)\\]/u.exec(permission);\n\n if (matches?.[0] && matches.groups?.scopes) {\n const scopes: (string | string[])[] = matches.groups.scopes\n .split(',') // scopes with OR logic\n .map((s) => {\n if (s.includes('+')) {\n return s.split('+'); // scopes with AND logic\n }\n\n return s;\n });\n\n return {\n id: permission.replace(matches[0], ''),\n scopes,\n };\n }\n\n return {\n id: permission,\n scopes: [],\n };\n};\n\n/**\n * Resolve permissions to array of resolved permissions\n * @param permissions - array of permissions\n * @returns - array of resolved permissions\n * @example\n * resolvePermissions(['js:core:episodes[org]:get', 'js:core:episodes[published]:get'])\n * // [{ id: 'js:core:episodes:get', scopes: ['org', 'published'] }]\n */\nexport const resolvePermissions = (\n permissions: string[],\n): ResolvedPermission[] => {\n const resolvedPermissions: ResolvedPermission[] = [];\n\n for (const permission of permissions) {\n const { id, scopes } = resolvePermission(permission);\n\n const currentPermission = resolvedPermissions.find(\n (p) => p.id === id,\n );\n\n if (currentPermission) {\n concatScopes(currentPermission.scopes, scopes);\n } else {\n resolvedPermissions.push({\n id,\n scopes,\n });\n }\n }\n\n return resolvedPermissions;\n};\n\n/**\n * Resolve permission group string to object with scopes\n * @param permissionGroup - permission group string\n * @returns - resolved permission group object\n * @example\n * resolvePermissionGroup('g:js:core:episodes[org]:read')\n * // { id: 'g:js:core:episodes:read', scopes: ['org'] }\n */\nexport const resolvePermissionGroup = (permissionGroup: string) => {\n // Currently it has the same algorithm\n return resolvePermission(permissionGroup);\n};\n\n/**\n * Resolve permission groups to array of resolved permission groups\n * @param permissionGroups - array of permission groups\n * @returns - array of resolved permission groups\n * @example\n * resolvePermissionGroups(['g:js:core:episodes[org]:read', 'g:js:core:episodes[published]:read'])\n * // [{ id: 'g:js:core:episodes:read', scopes: ['org', 'published'] }]\n */\nexport const resolvePermissionGroups = (\n permissionGroups: string[],\n): ResolvedPermissionGroup[] => {\n // Currently it has the same algorithm\n return resolvePermissions(permissionGroups);\n};\n\nexport const mergeResolvedPermissions = (\n array1: ResolvedPermission[],\n array2: ResolvedPermission[],\n) => {\n const result: ResolvedPermission[] = [];\n\n for (const rp1 of array1) {\n const rps2 = array2.filter((rp2) => rp2.id === rp1.id);\n\n /* Empty scopes = has full access */\n if (\n rp1.scopes.length === 0\n || rps2.some((rp2) => rp2.id === rp1.id && rp2.scopes.length === 0)\n ) {\n result.push({\n id: rp1.id,\n scopes: [],\n });\n\n continue;\n }\n\n // [org#hci, user#hcu:xxxxx, [org#hci, review]]\n const scopes = [...rp1.scopes];\n for (const rp2 of rps2) {\n concatScopes(scopes, rp2.scopes);\n }\n\n result.push({\n id: rp1.id,\n scopes,\n });\n }\n\n for (const rp2 of array2) {\n if (!result.some((rp) => rp.id === rp2.id)) {\n result.push(rp2);\n }\n }\n\n return result;\n};\n\n/**\n * Replace scope in array of scopes\n * @param scopes - array of scopes\n * @param from - scope to replace\n * @param to - scope to replace with\n * @returns - array of scopes with replaced scopes\n * @example\n * replaceScope(['org#jsorg:xxx', ['org#jsorg:xxx', 'published']], 'org#jsorg:xxx', 'org#jsorg:hci')\n * // ['org#jsorg:hci', ['org#jsorg:hci', 'published']]\n */\nexport const replaceScope = (\n scopes: (string | string[])[], // [org#jsorg:xxx, [org#jsorg:xxx, published]]\n from: string,\n to: string | (string | string[])[],\n) => {\n const result: (string | string[])[] = [];\n for (const scope of scopes) {\n if (Array.isArray(scope)) {\n result.push(replaceScope(scope, from, to) as string[]);\n } else if (scope === from) {\n if (Array.isArray(to)) {\n result.push(...to);\n } else {\n result.push(to);\n }\n } else {\n result.push(scope);\n }\n }\n\n return result;\n};\n","import { concatScopes } from './helpers.js';\n\nexport class ScopesBulder {\n constructor(private scopes: (string | string[])[] = []) {}\n\n static fromBulder(builder: ScopesBulder) {\n return new ScopesBulder(builder.scopes.slice());\n }\n\n build() {\n return JSON.parse(JSON.stringify(this.scopes)) as (string | string[])[];\n }\n\n clone() {\n return ScopesBulder.fromBulder(this);\n }\n\n /**\n * Append scope or scopes to the current scopes\n * @param scope - scope or array of scopes to append\n * @example\n * append('org#jsorg:hci')\n * // ['org#xxx'] => ['org#xxx', 'org#jsorg:hci']\n * append(['org#hci', 'lang#en])\n * // ['org#xxx'] => ['org#xxx', ['org#hci', 'lang#en']]\n */\n append(scope: string | string[]) {\n this.scopes.push(scope);\n }\n\n /**\n * Extend current scopes with the given scopes\n * @param scopes - scope or array of scopes to extend with\n * @example\n * extend(['lang#en', 'lang#de'])\n * // ['published', 'draft'] => ['published', 'draft', 'lang#en', 'lang#de']\n */\n extend(scopes: (string | string[])[] | ScopesBulder) {\n if (Array.isArray(scopes)) {\n concatScopes(this.scopes, scopes);\n } else if (scopes instanceof ScopesBulder) {\n concatScopes(this.scopes, scopes.scopes);\n }\n }\n\n /**\n * Join all scopes with the given scope\n * @param scope - scope to join with\n * @example\n * join('org#jsorg:hci', 'before')\n * // ['published', 'draft'] => [['org#jsorg:hci', 'published'], ['org#jsorg:hci', 'draft']]\n * join(['lang#en', 'lang#de'])\n * // ['published', 'draft'] => [['published', 'lang#en'], ['published', 'lang#de'], ['draft', 'lang#en'], ['draft', 'lang#de']]\n */\n join(scopeOrScopes: string | (string | string[])[], pos: 'before' | 'after' = 'after') {\n const result: (string | string[])[] = [];\n\n const scopesToJoin = Array.isArray(scopeOrScopes) ? scopeOrScopes : [scopeOrScopes];\n\n if (scopesToJoin.length === 0) {\n return;\n }\n\n const currentScopesCopy = this.build();\n for (const scope of scopesToJoin) {\n const scopeToJoin = Array.isArray(scope) ? scope : [scope];\n\n for (const s of currentScopesCopy) {\n if (Array.isArray(s)) {\n result.push(pos === 'before' ? [...scopeToJoin, ...s] : [...s, ...scopeToJoin]);\n } else {\n result.push(pos === 'before' ? [...scopeToJoin, s] : [s, ...scopeToJoin]);\n }\n }\n }\n\n this.scopes = result;\n }\n\n /**\n * Replace prefix in all scopes\n * @param from - prefix to replace\n * @param to - prefix to replace with\n * @example\n * replacePrefix('org', 'id')\n * ['org#jsorg:hci'] => ['id#jsorg:hci']\n */\n replacePrefix(from: string, to: string) {\n const result: (string | string[])[] = [];\n for (const scope of this.scopes) {\n if (Array.isArray(scope)) {\n result.push(scope.map((s) => s.replace(`${from}#`, `${to}#`)));\n } else {\n result.push(scope.replace(`${from}#`, `${to}#`));\n }\n }\n this.scopes = result;\n }\n}\n"],"mappings":"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"}
package/dist/scopes.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as group, c as scope, i as form, l as scopes, n as and, o as id, r as anyScope, s as org, t as ScopeItem, u as user } from "./functional-scopes-DygK315q.js";
2
- export { ScopeItem, and, anyScope, form, group, id, org, scope, scopes, user };
1
+ import { a as group, c as scope, i as form, l as user, n as and, o as id, r as anyScope, s as org, t as ScopeItem } from "./functional-scopes-ut8Z6MmG.js";
2
+ export { ScopeItem, and, anyScope, form, group, id, org, scope, user };
package/dist/scopes.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as id, c as scopes, i as group, l as user, n as anyScope, o as org, r as form, s as scope, t as and } from "./functional-scopes-CsZNJMXW.js";
1
+ import { a as id, c as user, i as group, n as anyScope, o as org, r as form, s as scope, t as and } from "./functional-scopes-COiTBSy_.js";
2
2
 
3
- export { and, anyScope, form, group, id, org, scope, scopes, user };
3
+ export { and, anyScope, form, group, id, org, scope, user };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pcg/auth",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.3",
4
4
  "description": "Authorization library",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -27,15 +27,15 @@
27
27
  "files": [
28
28
  "dist"
29
29
  ],
30
- "devDependencies": {
31
- "@vitest/ui": "^4.0.8",
32
- "vitest": "^4.0.8"
33
- },
34
30
  "scripts": {
35
31
  "dev": "tsdown --watch",
36
32
  "build": "tsdown",
37
33
  "test": "vitest run",
38
34
  "test:watch": "vitest",
39
35
  "lint": "eslint \"src/**/*.ts\" --fix"
36
+ },
37
+ "devDependencies": {
38
+ "@vitest/ui": "^4.0.8",
39
+ "vitest": "^4.0.8"
40
40
  }
41
- }
41
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"functional-scopes-CsZNJMXW.js","names":["id"],"sources":["../src/permissions/functional-scopes.ts"],"sourcesContent":["import { type ActionScopesArray } from './action-scopes.js';\n\nexport type ScopeItem = string | string[];\n\n/**\n * Create an ActionScopesArray from individual scope items.\n *\n * @example\n * scopes(org('jsorg:hci'))\n * // => ['org#jsorg:hci']\n *\n * scopes(org('jsorg:hci'), entityId('ep:123'))\n * // => ['org#jsorg:hci', 'id#ep:123']\n *\n * const o = org('jsorg:hci');\n * scopes(o, and(o, 'published'))\n * // => ['org#jsorg:hci', ['org#jsorg:hci', 'published']]\n */\nexport function scopes(...items: ScopeItem[]): ActionScopesArray {\n return items;\n}\n\n/**\n * Create a wildcard scope that matches any permission scope.\n *\n * @example\n * isGranted(user, 'js:core:episodes:list', anyScope())\n */\nexport function anyScope(): ActionScopesArray {\n return ['*'];\n}\n\n/**\n * Create an organization scope.\n *\n * @example\n * org('jsorg:hci') // => 'org#jsorg:hci'\n */\nexport function org(id: string): string {\n return `org#${id}`;\n}\n\n/**\n * Create an entity id scope.\n *\n * @example\n * id('ep:123') // => 'id#ep:123'\n */\nexport function id(entityId: string): string {\n return `id#${entityId}`;\n}\n\n/**\n * Create a user scope.\n *\n * @example\n * user('hcu:xxx') // => 'user#hcu:xxx'\n */\nexport function user(id: string): string {\n return `user#${id}`;\n}\n\n/**\n * Create a form scope.\n *\n * @example\n * form('contact') // => 'form#contact'\n */\nexport function form(id: string): string {\n return `form#${id}`;\n}\n\n/**\n * Create a group scope.\n *\n * @example\n * group('hcgrp:ZT9') // => 'grp#hcgrp:ZT9'\n */\nexport function group(id: string): string {\n return `grp#${id}`;\n}\n\n/**\n * Create a named scope with an optional ID.\n *\n * @example\n * scope('published') // => 'published'\n * scope('orggroup', 'hcgrp:ZT9mt8j9lSR') // => 'orggroup#hcgrp:ZT9mt8j9lSR'\n */\nexport function scope(name: string, id?: string): string {\n return id ? `${name}#${id}` : name;\n}\n\n/**\n * Combine multiple scopes with AND logic (all must match).\n *\n * @example\n * and(org('jsorg:hci'), 'published')\n * // => ['org#jsorg:hci', 'published']\n */\nexport function and(...items: string[]): string[] {\n return items;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,SAAgB,OAAO,GAAG,OAAuC;AAC/D,QAAO;;;;;;;;AAST,SAAgB,WAA8B;AAC5C,QAAO,CAAC,IAAI;;;;;;;;AASd,SAAgB,IAAI,MAAoB;AACtC,QAAO,OAAOA;;;;;;;;AAShB,SAAgB,GAAG,UAA0B;AAC3C,QAAO,MAAM;;;;;;;;AASf,SAAgB,KAAK,MAAoB;AACvC,QAAO,QAAQA;;;;;;;;AASjB,SAAgB,KAAK,MAAoB;AACvC,QAAO,QAAQA;;;;;;;;AASjB,SAAgB,MAAM,MAAoB;AACxC,QAAO,OAAOA;;;;;;;;;AAUhB,SAAgB,MAAM,MAAc,MAAqB;AACvD,QAAOA,OAAK,GAAG,KAAK,GAAGA,SAAO;;;;;;;;;AAUhC,SAAgB,IAAI,GAAG,OAA2B;AAChD,QAAO"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"functional-scopes-DygK315q.d.ts","names":[],"sources":["../src/permissions/action-scopes.ts","../src/permissions/functional-scopes.ts"],"sourcesContent":[],"mappings":";KAAY,iBAAA;AAAZ;AAMA;;;AA6Dc,cA7DD,YAAA,CA6DC;EAAiB,KAAA,EA5Df,iBA4De;;;uBAxDR;ECTX;AAgBZ;AAUA;AAUA;AAUA;AAUA;AAUA;AAUA;AAWA;EAWgB,WAAG,CAAA,EAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;;;;;;;;;;;;;;cDjCL;;;;;;;;;;;;;;;;;;;AAnEF,KCEA,SAAA,GDFiB,MAAA,GAAA,MAAA,EAAA;AAM7B;;;;;;;;ACJA;AAgBA;AAUA;AAUA;AAUA;AAUA;AAUgB,iBAlDA,MAAA,CAkDI,GAAA,KAAA,EAlDa,SAkDb,EAAA,CAAA,EAlD2B,iBAkD3B;AAUpB;AAWA;AAWA;;;;iBAxEgB,QAAA,CAAA,GAAY;;;;;;;iBAUZ,GAAA;;;;;;;iBAUA,EAAA;;;;;;;iBAUA,IAAA;;;;;;;iBAUA,IAAA;;;;;;;iBAUA,KAAA;;;;;;;;iBAWA,KAAA;;;;;;;;iBAWA,GAAA"}