@hazeljs/casl 0.7.8 → 0.8.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/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  **Attribute-level (record-level) authorization for HazelJS, powered by [CASL](https://casl.js.org).**
4
4
 
5
5
  Where `RoleGuard` asks "is your role high enough?", `@hazeljs/casl` asks:
6
+
6
7
  > **Can _this_ user perform _this action_ on _this specific record_?**
7
8
 
8
9
  [![npm version](https://img.shields.io/npm/v/@hazeljs/casl.svg)](https://www.npmjs.com/package/@hazeljs/casl)
@@ -43,7 +44,7 @@ import { Injectable } from '@hazeljs/core';
43
44
  // All @casl/ability symbols are re-exported — no separate @casl/ability dep needed.
44
45
  import { AbilityFactory, MongoAbility, AbilityBuilder, createMongoAbility } from '@hazeljs/casl';
45
46
 
46
- type Action = 'create' | 'read' | 'update' | 'delete' | 'manage';
47
+ type Action = 'create' | 'read' | 'update' | 'delete' | 'manage';
47
48
  type Subject = Post | 'Post' | 'all';
48
49
  export type AppAbility = MongoAbility<[Action, Subject]>;
49
50
 
@@ -53,10 +54,10 @@ export class AppAbilityFactory extends AbilityFactory<AppAbility> {
53
54
  const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
54
55
 
55
56
  if (user.role === 'admin') {
56
- can('manage', 'all'); // admin: everything
57
+ can('manage', 'all'); // admin: everything
57
58
  } else {
58
- can('read', 'Post');
59
- can('update', 'Post', { authorId: user.id }); // own posts only
59
+ can('read', 'Post');
60
+ can('update', 'Post', { authorId: user.id }); // own posts only
60
61
  cannot('delete', 'Post');
61
62
  }
62
63
 
@@ -75,9 +76,7 @@ import { CaslModule } from '@hazeljs/casl';
75
76
  import { AppAbilityFactory } from './casl/app-ability.factory';
76
77
 
77
78
  @HazelModule({
78
- imports: [
79
- CaslModule.forRoot({ abilityFactory: AppAbilityFactory }),
80
- ],
79
+ imports: [CaslModule.forRoot({ abilityFactory: AppAbilityFactory })],
81
80
  })
82
81
  export class AppModule {}
83
82
  ```
@@ -114,10 +113,10 @@ export class PostsController {
114
113
 
115
114
  Errors thrown:
116
115
 
117
- | Condition | Status |
118
- |---|---|
119
- | No `req.user` (guard order wrong) | 401 |
120
- | Any handler returns `false` | 403 |
116
+ | Condition | Status |
117
+ | --------------------------------- | ------ |
118
+ | No `req.user` (guard order wrong) | 401 |
119
+ | Any handler returns `false` | 403 |
121
120
 
122
121
  ---
123
122
 
@@ -216,7 +215,7 @@ Request
216
215
 
217
216
  ## `@Ability()` — inject the ability directly
218
217
 
219
- `@Ability()` is a **parameter decorator** that resolves `CaslService.createForUser(req.user)` once per request and injects the result straight into your controller method. Services receive the pre-built ability instead of the raw user, keeping business logic clean.
218
+ `@Ability()` is a **parameter decorator** that resolves `CaslService.createForUser(req.user)` once per request and injects the result straight into your controller method. Services receive the pre-built ability instead of the raw user, keeping business logic clean.
220
219
 
221
220
  ```typescript
222
221
  // posts.controller.ts
@@ -232,18 +231,15 @@ export class PostsController {
232
231
 
233
232
  @Patch('/:id')
234
233
  update(
235
- @Ability() ability: AppAbility, // ← resolved from req.user automatically
234
+ @Ability() ability: AppAbility, // ← resolved from req.user automatically
236
235
  @Param('id') id: string,
237
- @Body() dto: UpdatePostDto,
236
+ @Body() dto: UpdatePostDto
238
237
  ) {
239
238
  return this.postsService.update(ability, id, dto);
240
239
  }
241
240
 
242
241
  @Delete('/:id')
243
- remove(
244
- @Ability() ability: AppAbility,
245
- @Param('id') id: string,
246
- ) {
242
+ remove(@Ability() ability: AppAbility, @Param('id') id: string) {
247
243
  return this.postsService.remove(ability, id);
248
244
  }
249
245
  }
@@ -254,7 +250,7 @@ The service just receives an `AppAbility` — no `CaslService` injection needed:
254
250
  ```typescript
255
251
  // posts.service.ts
256
252
  import { Injectable } from '@hazeljs/core';
257
- import { subject } from '@hazeljs/casl'; // re-exported — no @casl/ability dep needed
253
+ import { subject } from '@hazeljs/casl'; // re-exported — no @casl/ability dep needed
258
254
  import type { AppAbility } from './casl/app-ability.factory';
259
255
 
260
256
  @Injectable()
@@ -283,7 +279,7 @@ export class PostsService {
283
279
  ```
284
280
 
285
281
  > **When to use `@Ability()` vs `CaslService`**
286
- > Use `@Ability()` when the controller passes the ability to a single service. Use `CaslService` directly when a service is called from multiple places (background jobs, other services) and the caller may not hold an ability object.
282
+ > Use `@Ability()` when the controller passes the ability to a single service. Use `CaslService` directly when a service is called from multiple places (background jobs, other services) and the caller may not hold an ability object.
287
283
 
288
284
  ---
289
285
 
@@ -300,11 +296,11 @@ import type { AppAbility } from './casl/app-ability.factory';
300
296
  export class PostsService {
301
297
  constructor(
302
298
  private readonly postsRepo: PostsRepository,
303
- private readonly casl: CaslService<AppAbility>,
299
+ private readonly casl: CaslService<AppAbility>
304
300
  ) {}
305
301
 
306
302
  async update(user: Record<string, unknown>, postId: string, dto: UpdatePostDto) {
307
- const post = await this.postsRepo.findById(postId);
303
+ const post = await this.postsRepo.findById(postId);
308
304
  const ability = this.casl.createForUser(user);
309
305
 
310
306
  if (!ability.can('update', subject('Post', post))) {
@@ -327,9 +323,9 @@ import { CaslService } from '@hazeljs/casl';
327
323
  // Inject and call createForUser to get an ability for the current user
328
324
  const ability = this.casl.createForUser(user);
329
325
 
330
- ability.can('read', 'Post') // true / false
331
- ability.can('update', subject('Post', post)) // checks conditions
332
- ability.cannot('delete', 'Post') // negation check
326
+ ability.can('read', 'Post'); // true / false
327
+ ability.can('update', subject('Post', post)); // checks conditions
328
+ ability.cannot('delete', 'Post'); // negation check
333
329
  ```
334
330
 
335
331
  ---
@@ -349,9 +345,9 @@ abstract class AbilityFactory<A extends AnyAbility> {
349
345
 
350
346
  ## `CaslModule.forRoot()` options
351
347
 
352
- | Option | Type | Required | Description |
353
- |---|---|---|---|
354
- | `abilityFactory` | `new (...args: any[]) => AbilityFactory<A>` | ✓ | Your factory class (must be `@Injectable()`) |
348
+ | Option | Type | Required | Description |
349
+ | ---------------- | ------------------------------------------- | -------- | -------------------------------------------- |
350
+ | `abilityFactory` | `new (...args: any[]) => AbilityFactory<A>` | ✓ | Your factory class (must be `@Injectable()`) |
355
351
 
356
352
  ---
357
353
 
@@ -360,4 +356,4 @@ abstract class AbilityFactory<A extends AnyAbility> {
360
356
  - [Documentation](https://hazeljs.ai/docs/packages/casl)
361
357
  - [GitHub](https://github.com/hazel-js/hazeljs)
362
358
  - [Issues](https://github.com/hazel-js/hazeljs/issues)
363
- - [Discord](https://discord.com/channels/1448263814238965833/1448263814859456575)
359
+ - [Discord](https://discord.gg/PxNBPzvQk7)
@@ -0,0 +1,37 @@
1
+ import type { AnyAbility } from '@casl/ability';
2
+ /**
3
+ * Abstract base class for user-defined ability factories.
4
+ *
5
+ * Extend this class and implement `createForUser` to define what actions a
6
+ * given user can perform. Register your factory with `CaslModule.forRoot()`.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { MongoAbility, createMongoAbility, AbilityBuilder } from '@casl/ability';
11
+ * import { Injectable } from '@hazeljs/core';
12
+ * import { AbilityFactory } from '@hazeljs/casl';
13
+ *
14
+ * type Action = 'create' | 'read' | 'update' | 'delete' | 'manage';
15
+ * type Subject = Post | 'Post' | 'all';
16
+ * export type AppAbility = MongoAbility<[Action, Subject]>;
17
+ *
18
+ * @Injectable()
19
+ * export class AppAbilityFactory extends AbilityFactory<AppAbility> {
20
+ * createForUser(user: AuthUser): AppAbility {
21
+ * const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
22
+ * if (user.role === 'admin') {
23
+ * can('manage', 'all');
24
+ * } else {
25
+ * can('read', 'Post');
26
+ * can('update', 'Post', { authorId: user.id }); // own posts only
27
+ * cannot('delete', 'Post');
28
+ * }
29
+ * return build();
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ export declare abstract class AbilityFactory<A extends AnyAbility> {
35
+ abstract createForUser(user: Record<string, unknown>): A;
36
+ }
37
+ //# sourceMappingURL=ability.factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability.factory.d.ts","sourceRoot":"","sources":["../src/ability.factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,8BAAsB,cAAc,CAAC,CAAC,SAAS,UAAU;IACvD,QAAQ,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC;CACzD"}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AbilityFactory = void 0;
4
+ /**
5
+ * Abstract base class for user-defined ability factories.
6
+ *
7
+ * Extend this class and implement `createForUser` to define what actions a
8
+ * given user can perform. Register your factory with `CaslModule.forRoot()`.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { MongoAbility, createMongoAbility, AbilityBuilder } from '@casl/ability';
13
+ * import { Injectable } from '@hazeljs/core';
14
+ * import { AbilityFactory } from '@hazeljs/casl';
15
+ *
16
+ * type Action = 'create' | 'read' | 'update' | 'delete' | 'manage';
17
+ * type Subject = Post | 'Post' | 'all';
18
+ * export type AppAbility = MongoAbility<[Action, Subject]>;
19
+ *
20
+ * @Injectable()
21
+ * export class AppAbilityFactory extends AbilityFactory<AppAbility> {
22
+ * createForUser(user: AuthUser): AppAbility {
23
+ * const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
24
+ * if (user.role === 'admin') {
25
+ * can('manage', 'all');
26
+ * } else {
27
+ * can('read', 'Post');
28
+ * can('update', 'Post', { authorId: user.id }); // own posts only
29
+ * cannot('delete', 'Post');
30
+ * }
31
+ * return build();
32
+ * }
33
+ * }
34
+ * ```
35
+ */
36
+ class AbilityFactory {
37
+ }
38
+ exports.AbilityFactory = AbilityFactory;
@@ -0,0 +1,35 @@
1
+ import type { AnyAbility } from '@casl/ability';
2
+ import { AbilityFactory } from './ability.factory';
3
+ export interface CaslModuleOptions<A extends AnyAbility = AnyAbility> {
4
+ /**
5
+ * Your application's ability factory class. It must extend `AbilityFactory`
6
+ * and be decorated with `@Injectable()` so the DI container can resolve it.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * CaslModule.forRoot({ abilityFactory: AppAbilityFactory })
11
+ * ```
12
+ */
13
+ abilityFactory: new (...args: any[]) => AbilityFactory<A>;
14
+ }
15
+ /**
16
+ * HazelJS module that wires attribute-level authorization into your app.
17
+ *
18
+ * Register once in your root module:
19
+ *
20
+ * ```ts
21
+ * @HazelModule({
22
+ * imports: [
23
+ * CaslModule.forRoot({ abilityFactory: AppAbilityFactory }),
24
+ * ],
25
+ * })
26
+ * export class AppModule {}
27
+ * ```
28
+ *
29
+ * Then use `PoliciesGuard` / `@CheckPolicies` on your routes and inject
30
+ * `CaslService` in your services for record-level checks.
31
+ */
32
+ export declare class CaslModule {
33
+ static forRoot<A extends AnyAbility>(options: CaslModuleOptions<A>): typeof CaslModule;
34
+ }
35
+ //# sourceMappingURL=casl.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"casl.module.d.ts","sourceRoot":"","sources":["../src/casl.module.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU;IAClE;;;;;;;;OAQG;IAEH,cAAc,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAIa,UAAU;IACrB,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,OAAO,UAAU;CAIvF"}
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var CaslModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.CaslModule = void 0;
11
+ const core_1 = require("@hazeljs/core");
12
+ const casl_service_1 = require("./casl.service");
13
+ /**
14
+ * HazelJS module that wires attribute-level authorization into your app.
15
+ *
16
+ * Register once in your root module:
17
+ *
18
+ * ```ts
19
+ * @HazelModule({
20
+ * imports: [
21
+ * CaslModule.forRoot({ abilityFactory: AppAbilityFactory }),
22
+ * ],
23
+ * })
24
+ * export class AppModule {}
25
+ * ```
26
+ *
27
+ * Then use `PoliciesGuard` / `@CheckPolicies` on your routes and inject
28
+ * `CaslService` in your services for record-level checks.
29
+ */
30
+ let CaslModule = CaslModule_1 = class CaslModule {
31
+ static forRoot(options) {
32
+ casl_service_1.CaslService.configure(options.abilityFactory);
33
+ return CaslModule_1;
34
+ }
35
+ };
36
+ exports.CaslModule = CaslModule;
37
+ exports.CaslModule = CaslModule = CaslModule_1 = __decorate([
38
+ (0, core_1.HazelModule)({
39
+ providers: [casl_service_1.CaslService],
40
+ exports: [casl_service_1.CaslService],
41
+ })
42
+ ], CaslModule);
@@ -0,0 +1,35 @@
1
+ import type { AnyAbility } from '@casl/ability';
2
+ import { AbilityFactory } from './ability.factory';
3
+ /**
4
+ * Core service that builds a CASL ability for a given user.
5
+ *
6
+ * Inject this service anywhere you need record-level authorization:
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * @Injectable()
11
+ * export class PostsService {
12
+ * constructor(private readonly casl: CaslService<AppAbility>) {}
13
+ *
14
+ * async update(user: AuthUser, postId: string, dto: UpdatePostDto) {
15
+ * const post = await this.postsRepo.findById(postId);
16
+ * const ability = this.casl.createForUser(user);
17
+ * if (!ability.can('update', post)) {
18
+ * throw Object.assign(new Error('Forbidden'), { status: 403 });
19
+ * }
20
+ * return this.postsRepo.update(postId, dto);
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+ export declare class CaslService<A extends AnyAbility = AnyAbility> {
26
+ private get factory();
27
+ /**
28
+ * Build an ability instance for the given user object.
29
+ * The user value comes from `req.user` (set by JwtAuthGuard or your own guard).
30
+ */
31
+ createForUser(user: Record<string, unknown>): A;
32
+ static factoryClass: (new (...args: any[]) => AbilityFactory<AnyAbility>) | undefined;
33
+ static configure(factoryClass: new (...args: any[]) => AbilityFactory<AnyAbility>): void;
34
+ }
35
+ //# sourceMappingURL=casl.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"casl.service.d.ts","sourceRoot":"","sources":["../src/casl.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBACa,WAAW,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU;IAExD,OAAO,KAAK,OAAO,GASlB;IAED;;;OAGG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC;IAS/C,MAAM,CAAC,YAAY,EAAE,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,cAAc,CAAC,UAAU,CAAC,CAAC,GAAG,SAAS,CAAC;IAGtF,MAAM,CAAC,SAAS,CAAC,YAAY,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI;CAGzF"}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var CaslService_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.CaslService = void 0;
11
+ const core_1 = require("@hazeljs/core");
12
+ /**
13
+ * Core service that builds a CASL ability for a given user.
14
+ *
15
+ * Inject this service anywhere you need record-level authorization:
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * @Injectable()
20
+ * export class PostsService {
21
+ * constructor(private readonly casl: CaslService<AppAbility>) {}
22
+ *
23
+ * async update(user: AuthUser, postId: string, dto: UpdatePostDto) {
24
+ * const post = await this.postsRepo.findById(postId);
25
+ * const ability = this.casl.createForUser(user);
26
+ * if (!ability.can('update', post)) {
27
+ * throw Object.assign(new Error('Forbidden'), { status: 403 });
28
+ * }
29
+ * return this.postsRepo.update(postId, dto);
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ let CaslService = CaslService_1 = class CaslService {
35
+ // Resolved lazily so the factory class is available after DI wires everything up.
36
+ get factory() {
37
+ const factoryClass = CaslService_1.factoryClass;
38
+ if (!factoryClass) {
39
+ throw new Error('CaslService: no AbilityFactory registered. ' +
40
+ 'Call CaslModule.forRoot({ abilityFactory: YourFactory }) in your app module.');
41
+ }
42
+ return core_1.Container.getInstance().resolve(factoryClass);
43
+ }
44
+ /**
45
+ * Build an ability instance for the given user object.
46
+ * The user value comes from `req.user` (set by JwtAuthGuard or your own guard).
47
+ */
48
+ createForUser(user) {
49
+ return this.factory.createForUser(user);
50
+ }
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ static configure(factoryClass) {
53
+ CaslService_1.factoryClass = factoryClass;
54
+ }
55
+ };
56
+ exports.CaslService = CaslService;
57
+ exports.CaslService = CaslService = CaslService_1 = __decorate([
58
+ (0, core_1.Injectable)()
59
+ ], CaslService);
@@ -0,0 +1,2 @@
1
+ import 'reflect-metadata';
2
+ //# sourceMappingURL=casl.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"casl.test.d.ts","sourceRoot":"","sources":["../src/casl.test.ts"],"names":[],"mappings":"AAEA,OAAO,kBAAkB,CAAC"}
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ /// <reference types="jest" />
3
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
4
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
6
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
7
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
8
+ };
9
+ var __metadata = (this && this.__metadata) || function (k, v) {
10
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
11
+ };
12
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
13
+ return function (target, key) { decorator(target, key, paramIndex); }
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ require("reflect-metadata");
17
+ jest.mock('@hazeljs/core', () => ({
18
+ __esModule: true,
19
+ Service: () => () => undefined,
20
+ Injectable: () => () => undefined,
21
+ HazelModule: () => () => undefined,
22
+ Container: {
23
+ getInstance: jest.fn(),
24
+ },
25
+ logger: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
26
+ default: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
27
+ }));
28
+ const core_1 = require("@hazeljs/core");
29
+ const ability_1 = require("@casl/ability");
30
+ const ability_factory_1 = require("./ability.factory");
31
+ const casl_service_1 = require("./casl.service");
32
+ const casl_module_1 = require("./casl.module");
33
+ const policy_guard_1 = require("./policy.guard");
34
+ const check_policies_decorator_1 = require("./decorators/check-policies.decorator");
35
+ const ability_decorator_1 = require("./decorators/ability.decorator");
36
+ function buildAbility(role, userId) {
37
+ const { can, cannot, build } = new ability_1.AbilityBuilder(ability_1.createMongoAbility);
38
+ if (role === 'admin') {
39
+ can('manage', 'all');
40
+ }
41
+ else {
42
+ can('read', 'Post');
43
+ can('update', 'Post', { authorId: userId });
44
+ cannot('delete', 'Post');
45
+ }
46
+ return build();
47
+ }
48
+ class TestAbilityFactory extends ability_factory_1.AbilityFactory {
49
+ createForUser(user) {
50
+ return buildAbility(user['role'], user['id']);
51
+ }
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Helper: build a mock ExecutionContext
55
+ // ---------------------------------------------------------------------------
56
+ function makeCtx(user) {
57
+ return {
58
+ switchToHttp: () => ({
59
+ getRequest: () => ({ user }),
60
+ getResponse: () => ({}),
61
+ getContext: () => ({}),
62
+ }),
63
+ };
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // CaslService
67
+ // ---------------------------------------------------------------------------
68
+ describe('CaslService', () => {
69
+ const mockContainer = core_1.Container;
70
+ beforeEach(() => {
71
+ casl_service_1.CaslService.factoryClass = undefined;
72
+ jest.clearAllMocks();
73
+ });
74
+ it('throws when no factory is configured', () => {
75
+ const svc = new casl_service_1.CaslService();
76
+ expect(() => svc.createForUser({ id: '1', role: 'user' })).toThrow('CaslService: no AbilityFactory registered');
77
+ });
78
+ it('resolves the factory from the DI container and calls createForUser', () => {
79
+ const factory = new TestAbilityFactory();
80
+ mockContainer.getInstance.mockReturnValue({
81
+ resolve: jest.fn().mockReturnValue(factory),
82
+ });
83
+ casl_service_1.CaslService.configure(TestAbilityFactory);
84
+ const svc = new casl_service_1.CaslService();
85
+ const user = { id: 'user-1', role: 'admin' };
86
+ const ability = svc.createForUser(user);
87
+ expect(ability.can('manage', 'all')).toBe(true);
88
+ });
89
+ it('returns an ability respecting conditions for regular users', () => {
90
+ const factory = new TestAbilityFactory();
91
+ mockContainer.getInstance.mockReturnValue({
92
+ resolve: jest.fn().mockReturnValue(factory),
93
+ });
94
+ casl_service_1.CaslService.configure(TestAbilityFactory);
95
+ const svc = new casl_service_1.CaslService();
96
+ const user = { id: 'user-1', role: 'user' };
97
+ const ability = svc.createForUser(user);
98
+ expect(ability.can('read', 'Post')).toBe(true);
99
+ expect(ability.can('delete', 'Post')).toBe(false);
100
+ // own post — use subject() helper so CASL knows the subject type
101
+ expect(ability.can('update', (0, ability_1.subject)('Post', { authorId: 'user-1' }))).toBe(true);
102
+ // someone else's post
103
+ expect(ability.can('update', (0, ability_1.subject)('Post', { authorId: 'other-user' }))).toBe(false);
104
+ });
105
+ });
106
+ // ---------------------------------------------------------------------------
107
+ // CaslModule.forRoot
108
+ // ---------------------------------------------------------------------------
109
+ describe('CaslModule.forRoot', () => {
110
+ afterEach(() => {
111
+ casl_service_1.CaslService.factoryClass = undefined;
112
+ });
113
+ it('configures the factory class on CaslService', () => {
114
+ casl_module_1.CaslModule.forRoot({ abilityFactory: TestAbilityFactory });
115
+ expect(casl_service_1.CaslService.factoryClass).toBe(TestAbilityFactory);
116
+ });
117
+ it('returns the CaslModule class (for HazelModule imports)', () => {
118
+ const result = casl_module_1.CaslModule.forRoot({ abilityFactory: TestAbilityFactory });
119
+ expect(result).toBe(casl_module_1.CaslModule);
120
+ });
121
+ });
122
+ // ---------------------------------------------------------------------------
123
+ // PoliciesGuard
124
+ // ---------------------------------------------------------------------------
125
+ describe('PoliciesGuard', () => {
126
+ const mockContainer = core_1.Container;
127
+ beforeEach(() => {
128
+ casl_service_1.CaslService.factoryClass = undefined;
129
+ jest.clearAllMocks();
130
+ });
131
+ function setupFactory() {
132
+ const factory = new TestAbilityFactory();
133
+ mockContainer.getInstance.mockReturnValue({
134
+ resolve: jest.fn().mockReturnValue(factory),
135
+ });
136
+ casl_service_1.CaslService.configure(TestAbilityFactory);
137
+ return new casl_service_1.CaslService();
138
+ }
139
+ it('returns true when no handlers are provided', async () => {
140
+ const casl = setupFactory();
141
+ const Guard = (0, policy_guard_1.PoliciesGuard)();
142
+ const guard = new Guard(casl);
143
+ const ctx = makeCtx({ id: '1', role: 'user' });
144
+ await expect(guard.canActivate(ctx)).resolves.toBe(true);
145
+ });
146
+ it('returns true when all function handlers pass', async () => {
147
+ const casl = setupFactory();
148
+ const Guard = (0, policy_guard_1.PoliciesGuard)((ability) => ability.can('read', 'Post'));
149
+ const guard = new Guard(casl);
150
+ const ctx = makeCtx({ id: '1', role: 'user' });
151
+ await expect(guard.canActivate(ctx)).resolves.toBe(true);
152
+ });
153
+ it('throws 403 when a handler returns false', async () => {
154
+ const casl = setupFactory();
155
+ const Guard = (0, policy_guard_1.PoliciesGuard)((ability) => ability.can('delete', 'Post'));
156
+ const guard = new Guard(casl);
157
+ const ctx = makeCtx({ id: '1', role: 'user' });
158
+ await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: 403 });
159
+ });
160
+ it('throws 401 when req.user is missing', async () => {
161
+ const casl = setupFactory();
162
+ const Guard = (0, policy_guard_1.PoliciesGuard)((ability) => ability.can('read', 'Post'));
163
+ const guard = new Guard(casl);
164
+ const ctx = makeCtx(undefined);
165
+ await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: 401 });
166
+ });
167
+ it('works with a class-instance (IPolicyHandler) handler', async () => {
168
+ const casl = setupFactory();
169
+ const classHandler = {
170
+ handle: jest.fn().mockResolvedValue(true),
171
+ };
172
+ const Guard = (0, policy_guard_1.PoliciesGuard)(classHandler);
173
+ const guard = new Guard(casl);
174
+ const ctx = makeCtx({ id: '1', role: 'user' });
175
+ await expect(guard.canActivate(ctx)).resolves.toBe(true);
176
+ expect(classHandler.handle).toHaveBeenCalledTimes(1);
177
+ });
178
+ it('admin passes all checks', async () => {
179
+ const casl = setupFactory();
180
+ const Guard = (0, policy_guard_1.PoliciesGuard)((ability) => ability.can('read', 'Post'), (ability) => ability.can('delete', 'Post'), (ability) => ability.can('create', 'Post'));
181
+ const guard = new Guard(casl);
182
+ const ctx = makeCtx({ id: 'admin-1', role: 'admin' });
183
+ await expect(guard.canActivate(ctx)).resolves.toBe(true);
184
+ });
185
+ });
186
+ // ---------------------------------------------------------------------------
187
+ // @CheckPolicies decorator
188
+ // ---------------------------------------------------------------------------
189
+ describe('@CheckPolicies decorator', () => {
190
+ it('registers a PoliciesGuard on the method metadata under hazel:guards', () => {
191
+ class TestController {
192
+ getAll() {
193
+ return [];
194
+ }
195
+ }
196
+ __decorate([
197
+ (0, check_policies_decorator_1.CheckPolicies)((ability) => ability.can('read', 'Post')),
198
+ __metadata("design:type", Function),
199
+ __metadata("design:paramtypes", []),
200
+ __metadata("design:returntype", void 0)
201
+ ], TestController.prototype, "getAll", null);
202
+ const guards = Reflect.getMetadata('hazel:guards', TestController.prototype, 'getAll');
203
+ expect(Array.isArray(guards)).toBe(true);
204
+ expect(guards).toHaveLength(1);
205
+ });
206
+ it('appends to existing guards without overwriting them', () => {
207
+ // Pre-register a guard manually the way @UseGuards would
208
+ class MockGuard {
209
+ }
210
+ class TestController {
211
+ dummy() { }
212
+ }
213
+ Reflect.defineMetadata('hazel:guards', [MockGuard], TestController.prototype, 'dummy');
214
+ // Now apply CheckPolicies
215
+ const decorator = (0, check_policies_decorator_1.CheckPolicies)((ability) => ability.can('read', 'Post'));
216
+ const descriptor = Object.getOwnPropertyDescriptor(TestController.prototype, 'dummy');
217
+ decorator(TestController.prototype, 'dummy', descriptor);
218
+ const guards = Reflect.getMetadata('hazel:guards', TestController.prototype, 'dummy');
219
+ expect(guards).toHaveLength(2);
220
+ expect(guards[0]).toBe(MockGuard);
221
+ });
222
+ });
223
+ // ---------------------------------------------------------------------------
224
+ // @Ability() decorator
225
+ // ---------------------------------------------------------------------------
226
+ describe('@Ability() decorator', () => {
227
+ const INJECT_KEY = 'hazel:inject';
228
+ it('stores a custom injection on the method metadata', () => {
229
+ class TestController {
230
+ getTask(_ability) {
231
+ return _ability;
232
+ }
233
+ }
234
+ __decorate([
235
+ __param(0, (0, ability_decorator_1.Ability)()),
236
+ __metadata("design:type", Function),
237
+ __metadata("design:paramtypes", [Object]),
238
+ __metadata("design:returntype", void 0)
239
+ ], TestController.prototype, "getTask", null);
240
+ const injections = Reflect.getMetadata(INJECT_KEY, TestController, 'getTask');
241
+ expect(Array.isArray(injections)).toBe(true);
242
+ expect(injections).toHaveLength(1);
243
+ const inj = injections[0];
244
+ expect(inj.type).toBe('custom');
245
+ expect(typeof inj.resolve).toBe('function');
246
+ });
247
+ it('resolve() calls CaslService.createForUser with context.user', () => {
248
+ const mockAbility = { can: jest.fn() };
249
+ const mockCaslSvc = { createForUser: jest.fn().mockReturnValue(mockAbility) };
250
+ const mockContainer = { resolve: jest.fn().mockReturnValue(mockCaslSvc) };
251
+ class TestController {
252
+ getTask(_ability) {
253
+ return _ability;
254
+ }
255
+ }
256
+ __decorate([
257
+ __param(0, (0, ability_decorator_1.Ability)()),
258
+ __metadata("design:type", Function),
259
+ __metadata("design:paramtypes", [Object]),
260
+ __metadata("design:returntype", void 0)
261
+ ], TestController.prototype, "getTask", null);
262
+ const injections = Reflect.getMetadata(INJECT_KEY, TestController, 'getTask');
263
+ const context = { user: { sub: 'u1', role: 'admin' } };
264
+ const result = injections[0].resolve(null, context, mockContainer);
265
+ expect(mockContainer.resolve).toHaveBeenCalledWith(casl_service_1.CaslService);
266
+ expect(mockCaslSvc.createForUser).toHaveBeenCalledWith({ sub: 'u1', role: 'admin' });
267
+ expect(result).toBe(mockAbility);
268
+ });
269
+ it('resolve() falls back to empty user object when context.user is undefined', () => {
270
+ const mockCaslSvc = { createForUser: jest.fn().mockReturnValue({}) };
271
+ const mockContainer = { resolve: jest.fn().mockReturnValue(mockCaslSvc) };
272
+ class TestController {
273
+ getTask(_ability) {
274
+ return _ability;
275
+ }
276
+ }
277
+ __decorate([
278
+ __param(0, (0, ability_decorator_1.Ability)()),
279
+ __metadata("design:type", Function),
280
+ __metadata("design:paramtypes", [Object]),
281
+ __metadata("design:returntype", void 0)
282
+ ], TestController.prototype, "getTask", null);
283
+ const injections = Reflect.getMetadata(INJECT_KEY, TestController, 'getTask');
284
+ injections[0].resolve(null, {}, mockContainer);
285
+ expect(mockCaslSvc.createForUser).toHaveBeenCalledWith({});
286
+ });
287
+ it('throws when used outside a method parameter', () => {
288
+ expect(() => {
289
+ const decorator = (0, ability_decorator_1.Ability)();
290
+ decorator({}, undefined, 0);
291
+ }).toThrow('@Ability() must be used on a method parameter');
292
+ });
293
+ it('does not overwrite existing injections at other parameter indices', () => {
294
+ class TestController {
295
+ getTask(_ability, _id) {
296
+ return _ability;
297
+ }
298
+ }
299
+ __decorate([
300
+ __param(0, (0, ability_decorator_1.Ability)()),
301
+ __metadata("design:type", Function),
302
+ __metadata("design:paramtypes", [Object, String]),
303
+ __metadata("design:returntype", void 0)
304
+ ], TestController.prototype, "getTask", null);
305
+ // Only index 0 should be set; index 1 is unset
306
+ const injections = Reflect.getMetadata(INJECT_KEY, TestController, 'getTask');
307
+ expect(injections[0]).toBeDefined();
308
+ expect(injections[1]).toBeUndefined();
309
+ });
310
+ });
311
+ // ---------------------------------------------------------------------------
312
+ // AbilityFactory abstract class
313
+ // ---------------------------------------------------------------------------
314
+ describe('AbilityFactory', () => {
315
+ it('can be extended and createForUser can be implemented', () => {
316
+ const factory = new TestAbilityFactory();
317
+ const ability = factory.createForUser({ id: 'u1', role: 'admin' });
318
+ expect(ability.can('manage', 'all')).toBe(true);
319
+ });
320
+ });
@@ -0,0 +1,32 @@
1
+ import 'reflect-metadata';
2
+ /**
3
+ * Parameter decorator that injects the current user's CASL ability directly
4
+ * into a controller method parameter.
5
+ *
6
+ * Requires `JwtAuthGuard` (or any guard that sets `req.user`) to run before
7
+ * the route handler, and `CaslModule.forRoot()` to be configured.
8
+ *
9
+ * The ability is resolved once per request by calling
10
+ * `CaslService.createForUser(context.user)`. No need to inject `CaslService`
11
+ * into your services — pass the pre-built ability down instead.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { UseGuards } from '@hazeljs/core';
16
+ * import { JwtAuthGuard } from '@hazeljs/auth';
17
+ * import { Ability } from '@hazeljs/casl';
18
+ * import type { AppAbility } from './casl/app-ability.factory';
19
+ *
20
+ * @UseGuards(JwtAuthGuard, RoleGuard('user'))
21
+ * @Patch('/:id')
22
+ * update(
23
+ * @Ability() ability: AppAbility,
24
+ * @Param('id') id: string,
25
+ * @Body() dto: UpdateTaskDto,
26
+ * ) {
27
+ * return this.tasksService.update(ability, id, dto);
28
+ * }
29
+ * ```
30
+ */
31
+ export declare function Ability(): ParameterDecorator;
32
+ //# sourceMappingURL=ability.decorator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/ability.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAM1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,OAAO,IAAI,kBAAkB,CA6B5C"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Ability = Ability;
4
+ require("reflect-metadata");
5
+ const casl_service_1 = require("../casl.service");
6
+ const INJECT_METADATA_KEY = 'hazel:inject';
7
+ /**
8
+ * Parameter decorator that injects the current user's CASL ability directly
9
+ * into a controller method parameter.
10
+ *
11
+ * Requires `JwtAuthGuard` (or any guard that sets `req.user`) to run before
12
+ * the route handler, and `CaslModule.forRoot()` to be configured.
13
+ *
14
+ * The ability is resolved once per request by calling
15
+ * `CaslService.createForUser(context.user)`. No need to inject `CaslService`
16
+ * into your services — pass the pre-built ability down instead.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { UseGuards } from '@hazeljs/core';
21
+ * import { JwtAuthGuard } from '@hazeljs/auth';
22
+ * import { Ability } from '@hazeljs/casl';
23
+ * import type { AppAbility } from './casl/app-ability.factory';
24
+ *
25
+ * @UseGuards(JwtAuthGuard, RoleGuard('user'))
26
+ * @Patch('/:id')
27
+ * update(
28
+ * @Ability() ability: AppAbility,
29
+ * @Param('id') id: string,
30
+ * @Body() dto: UpdateTaskDto,
31
+ * ) {
32
+ * return this.tasksService.update(ability, id, dto);
33
+ * }
34
+ * ```
35
+ */
36
+ function Ability() {
37
+ return (target, propertyKey, parameterIndex) => {
38
+ if (!propertyKey) {
39
+ throw new Error('@Ability() must be used on a method parameter');
40
+ }
41
+ const constructor = target.constructor;
42
+ const injections = Reflect.getMetadata(INJECT_METADATA_KEY, constructor, propertyKey) ?? [];
43
+ injections[parameterIndex] = {
44
+ type: 'custom',
45
+ // Called by the router with (req, context, container) after guards have run.
46
+ resolve: (_req, context, container) => {
47
+ const user = (context.user ?? {});
48
+ const caslSvc = container.resolve(casl_service_1.CaslService);
49
+ return caslSvc.createForUser(user);
50
+ },
51
+ };
52
+ Reflect.defineMetadata(INJECT_METADATA_KEY, injections, constructor, propertyKey);
53
+ };
54
+ }
@@ -0,0 +1,31 @@
1
+ import 'reflect-metadata';
2
+ import type { AnyAbility } from '@casl/ability';
3
+ import type { AnyPolicyHandler } from '../types';
4
+ /**
5
+ * Shorthand method decorator that registers one or more CASL policy handlers
6
+ * directly on the route — equivalent to `@UseGuards(PoliciesGuard(...handlers))`.
7
+ *
8
+ * Accepts both **function** handlers and **class-instance** handlers that
9
+ * implement `IPolicyHandler`.
10
+ *
11
+ * > Requires `JwtAuthGuard` (or any guard that sets `req.user`) to run first,
12
+ * > and `CaslModule.forRoot()` to be configured.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * // Function handler (inline, no DI needed)
17
+ * @CheckPolicies((ability: AppAbility) => ability.can('read', 'Post'))
18
+ * @Get('/')
19
+ * list() { ... }
20
+ *
21
+ * // Multiple handlers — all must pass
22
+ * @CheckPolicies(
23
+ * (ability: AppAbility) => ability.can('read', 'Post'),
24
+ * (ability: AppAbility) => ability.can('update', 'Post'),
25
+ * )
26
+ * @Get('/:id/edit')
27
+ * editForm() { ... }
28
+ * ```
29
+ */
30
+ export declare function CheckPolicies<A extends AnyAbility>(...handlers: AnyPolicyHandler<A>[]): MethodDecorator;
31
+ //# sourceMappingURL=check-policies.decorator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-policies.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/check-policies.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,UAAU,EAChD,GAAG,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,GACjC,eAAe,CAWjB"}
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CheckPolicies = CheckPolicies;
4
+ require("reflect-metadata");
5
+ const policy_guard_1 = require("../policy.guard");
6
+ /**
7
+ * Shorthand method decorator that registers one or more CASL policy handlers
8
+ * directly on the route — equivalent to `@UseGuards(PoliciesGuard(...handlers))`.
9
+ *
10
+ * Accepts both **function** handlers and **class-instance** handlers that
11
+ * implement `IPolicyHandler`.
12
+ *
13
+ * > Requires `JwtAuthGuard` (or any guard that sets `req.user`) to run first,
14
+ * > and `CaslModule.forRoot()` to be configured.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // Function handler (inline, no DI needed)
19
+ * @CheckPolicies((ability: AppAbility) => ability.can('read', 'Post'))
20
+ * @Get('/')
21
+ * list() { ... }
22
+ *
23
+ * // Multiple handlers — all must pass
24
+ * @CheckPolicies(
25
+ * (ability: AppAbility) => ability.can('read', 'Post'),
26
+ * (ability: AppAbility) => ability.can('update', 'Post'),
27
+ * )
28
+ * @Get('/:id/edit')
29
+ * editForm() { ... }
30
+ * ```
31
+ */
32
+ function CheckPolicies(...handlers) {
33
+ const guardClass = (0, policy_guard_1.PoliciesGuard)(...handlers);
34
+ // `target` is the class prototype; `propertyKey` is the method name.
35
+ // This mirrors the exact metadata key/target pair that @UseGuards uses for
36
+ // method-level guards, and that the HazelJS router reads.
37
+ return (target, propertyKey, descriptor) => {
38
+ const existing = Reflect.getMetadata('hazel:guards', target, propertyKey) ?? [];
39
+ Reflect.defineMetadata('hazel:guards', [...existing, guardClass], target, propertyKey);
40
+ return descriptor;
41
+ };
42
+ }
@@ -0,0 +1,11 @@
1
+ export { AbilityFactory } from './ability.factory';
2
+ export { CaslService } from './casl.service';
3
+ export { CaslModule } from './casl.module';
4
+ export type { CaslModuleOptions } from './casl.module';
5
+ export { PoliciesGuard } from './policy.guard';
6
+ export { CheckPolicies } from './decorators/check-policies.decorator';
7
+ export { Ability } from './decorators/ability.decorator';
8
+ export type { PolicyHandler, IPolicyHandler, AnyPolicyHandler } from './types';
9
+ export { AbilityBuilder, createMongoAbility, subject } from '@casl/ability';
10
+ export type { MongoAbility, AnyAbility } from '@casl/ability';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAK/E,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC5E,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.subject = exports.createMongoAbility = exports.AbilityBuilder = exports.Ability = exports.CheckPolicies = exports.PoliciesGuard = exports.CaslModule = exports.CaslService = exports.AbilityFactory = void 0;
4
+ var ability_factory_1 = require("./ability.factory");
5
+ Object.defineProperty(exports, "AbilityFactory", { enumerable: true, get: function () { return ability_factory_1.AbilityFactory; } });
6
+ var casl_service_1 = require("./casl.service");
7
+ Object.defineProperty(exports, "CaslService", { enumerable: true, get: function () { return casl_service_1.CaslService; } });
8
+ var casl_module_1 = require("./casl.module");
9
+ Object.defineProperty(exports, "CaslModule", { enumerable: true, get: function () { return casl_module_1.CaslModule; } });
10
+ var policy_guard_1 = require("./policy.guard");
11
+ Object.defineProperty(exports, "PoliciesGuard", { enumerable: true, get: function () { return policy_guard_1.PoliciesGuard; } });
12
+ var check_policies_decorator_1 = require("./decorators/check-policies.decorator");
13
+ Object.defineProperty(exports, "CheckPolicies", { enumerable: true, get: function () { return check_policies_decorator_1.CheckPolicies; } });
14
+ var ability_decorator_1 = require("./decorators/ability.decorator");
15
+ Object.defineProperty(exports, "Ability", { enumerable: true, get: function () { return ability_decorator_1.Ability; } });
16
+ // Re-export the most commonly used @casl/ability symbols so applications
17
+ // that depend only on @hazeljs/casl do not need @casl/ability as a direct
18
+ // dependency in their own package.json.
19
+ var ability_1 = require("@casl/ability");
20
+ Object.defineProperty(exports, "AbilityBuilder", { enumerable: true, get: function () { return ability_1.AbilityBuilder; } });
21
+ Object.defineProperty(exports, "createMongoAbility", { enumerable: true, get: function () { return ability_1.createMongoAbility; } });
22
+ Object.defineProperty(exports, "subject", { enumerable: true, get: function () { return ability_1.subject; } });
@@ -0,0 +1,36 @@
1
+ import { CanActivate, Type } from '@hazeljs/core';
2
+ import type { AnyAbility } from '@casl/ability';
3
+ import type { AnyPolicyHandler } from './types';
4
+ /**
5
+ * Factory that returns a guard running all provided policy handlers against
6
+ * the current user's CASL ability. Every handler must return `true`; if any
7
+ * returns `false` the guard throws a 403.
8
+ *
9
+ * Must be placed **after** `JwtAuthGuard` (or any guard that sets `req.user`).
10
+ * Requires `CaslModule.forRoot()` to be configured.
11
+ *
12
+ * Accepts both function handlers and class-instance handlers:
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * // Inline function handler
17
+ * @UseGuards(JwtAuthGuard, PoliciesGuard((ability: AppAbility) => ability.can('read', 'Post')))
18
+ * @Get('/')
19
+ * list() { ... }
20
+ *
21
+ * // Class-instance handler (dependencies resolved manually or via new)
22
+ * @UseGuards(JwtAuthGuard, PoliciesGuard(new CanCreatePost()))
23
+ * @Post('/')
24
+ * create() { ... }
25
+ * ```
26
+ *
27
+ * Prefer the `@CheckPolicies()` decorator for cleaner syntax:
28
+ *
29
+ * ```ts
30
+ * @CheckPolicies((ability: AppAbility) => ability.can('read', 'Post'))
31
+ * @Get('/')
32
+ * list() { ... }
33
+ * ```
34
+ */
35
+ export declare function PoliciesGuard<A extends AnyAbility>(...handlers: AnyPolicyHandler<A>[]): Type<CanActivate>;
36
+ //# sourceMappingURL=policy.guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.guard.d.ts","sourceRoot":"","sources":["../src/policy.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,WAAW,EAAoB,IAAI,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,EAAE,gBAAgB,EAAiC,MAAM,SAAS,CAAC;AAY/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,UAAU,EAChD,GAAG,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,GACjC,IAAI,CAAC,WAAW,CAAC,CAqCnB"}
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.PoliciesGuard = PoliciesGuard;
13
+ const core_1 = require("@hazeljs/core");
14
+ const casl_service_1 = require("./casl.service");
15
+ function isClassHandler(handler) {
16
+ return (typeof handler === 'object' &&
17
+ handler !== null &&
18
+ typeof handler.handle === 'function');
19
+ }
20
+ /**
21
+ * Factory that returns a guard running all provided policy handlers against
22
+ * the current user's CASL ability. Every handler must return `true`; if any
23
+ * returns `false` the guard throws a 403.
24
+ *
25
+ * Must be placed **after** `JwtAuthGuard` (or any guard that sets `req.user`).
26
+ * Requires `CaslModule.forRoot()` to be configured.
27
+ *
28
+ * Accepts both function handlers and class-instance handlers:
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // Inline function handler
33
+ * @UseGuards(JwtAuthGuard, PoliciesGuard((ability: AppAbility) => ability.can('read', 'Post')))
34
+ * @Get('/')
35
+ * list() { ... }
36
+ *
37
+ * // Class-instance handler (dependencies resolved manually or via new)
38
+ * @UseGuards(JwtAuthGuard, PoliciesGuard(new CanCreatePost()))
39
+ * @Post('/')
40
+ * create() { ... }
41
+ * ```
42
+ *
43
+ * Prefer the `@CheckPolicies()` decorator for cleaner syntax:
44
+ *
45
+ * ```ts
46
+ * @CheckPolicies((ability: AppAbility) => ability.can('read', 'Post'))
47
+ * @Get('/')
48
+ * list() { ... }
49
+ * ```
50
+ */
51
+ function PoliciesGuard(...handlers) {
52
+ let PoliciesGuardMixin = class PoliciesGuardMixin {
53
+ constructor(casl) {
54
+ this.casl = casl;
55
+ }
56
+ async canActivate(context) {
57
+ if (handlers.length === 0)
58
+ return true;
59
+ const req = context.switchToHttp().getRequest();
60
+ const user = req.user;
61
+ if (!user) {
62
+ throw Object.assign(new Error('Unauthorized'), { status: 401 });
63
+ }
64
+ const ability = this.casl.createForUser(user);
65
+ for (const handler of handlers) {
66
+ let result;
67
+ if (isClassHandler(handler)) {
68
+ result = await handler.handle(ability);
69
+ }
70
+ else {
71
+ result = handler(ability);
72
+ }
73
+ if (!result) {
74
+ throw Object.assign(new Error('Insufficient permissions for this action'), {
75
+ status: 403,
76
+ });
77
+ }
78
+ }
79
+ return true;
80
+ }
81
+ };
82
+ PoliciesGuardMixin = __decorate([
83
+ (0, core_1.Injectable)(),
84
+ __metadata("design:paramtypes", [casl_service_1.CaslService])
85
+ ], PoliciesGuardMixin);
86
+ return PoliciesGuardMixin;
87
+ }
@@ -0,0 +1,32 @@
1
+ import type { AnyAbility } from '@casl/ability';
2
+ /**
3
+ * A function-style policy handler. Receives the current user's ability and
4
+ * returns `true` if the action is allowed.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const canReadPost: PolicyHandler<AppAbility> =
9
+ * (ability) => ability.can('read', 'Post');
10
+ * ```
11
+ */
12
+ export type PolicyHandler<A extends AnyAbility = AnyAbility> = (ability: A) => boolean;
13
+ /**
14
+ * Class-style policy handler. Implement this interface and pass the class to
15
+ * `@CheckPolicies()` when you need constructor-injected dependencies.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * @Injectable()
20
+ * export class CanCreatePost implements IPolicyHandler<AppAbility> {
21
+ * handle(ability: AppAbility) {
22
+ * return ability.can('create', 'Post');
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ export interface IPolicyHandler<A extends AnyAbility = AnyAbility> {
28
+ handle(ability: A): boolean | Promise<boolean>;
29
+ }
30
+ /** Union of the two supported handler shapes. */
31
+ export type AnyPolicyHandler<A extends AnyAbility = AnyAbility> = PolicyHandler<A> | IPolicyHandler<A>;
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC;AAEvF;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU;IAC/D,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAChD;AAED,iDAAiD;AACjD,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,IAC1D,aAAa,CAAC,CAAC,CAAC,GAChB,cAAc,CAAC,CAAC,CAAC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hazeljs/casl",
3
- "version": "0.7.8",
3
+ "version": "0.8.0",
4
4
  "description": "Attribute-level (resource-level) authorization for HazelJS via CASL",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -52,5 +52,5 @@
52
52
  "peerDependencies": {
53
53
  "@hazeljs/core": ">=0.2.0-beta.0"
54
54
  },
55
- "gitHead": "906cacdc08d52c4616831b888748f1d1887edb80"
55
+ "gitHead": "e0ed98ca074dd4f7472816d3c32ef576900dcca6"
56
56
  }