@hazeljs/auth 0.2.0-beta.58 → 0.2.0-beta.59

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.
@@ -0,0 +1,54 @@
1
+ import { CanActivate, Type } from '@hazeljs/core';
2
+ export interface TenantGuardOptions {
3
+ /**
4
+ * Where to read the expected tenant ID from the incoming request.
5
+ *
6
+ * - `'param'` — URL segment, e.g. `/orgs/:tenantId/products` (default)
7
+ * - `'header'` — HTTP header, e.g. `X-Tenant-ID`
8
+ * - `'query'` — Query string, e.g. `?tenantId=acme`
9
+ *
10
+ * @default 'param'
11
+ */
12
+ source?: 'param' | 'header' | 'query';
13
+ /**
14
+ * Name of the param / header / query key that carries the tenant ID.
15
+ * @default 'tenantId'
16
+ */
17
+ key?: string;
18
+ /**
19
+ * Field on the authenticated user object that holds the user's tenant ID.
20
+ * @default 'tenantId'
21
+ */
22
+ userField?: string;
23
+ }
24
+ /**
25
+ * Factory that returns a guard enforcing tenant-level isolation.
26
+ *
27
+ * Compares the tenant ID carried by the request (from a URL param, header,
28
+ * or query param) against the tenant ID stored on the authenticated user
29
+ * (from the JWT payload). Returns 403 if they do not match.
30
+ *
31
+ * Must be used after JwtAuthGuard so that `req.user` is populated.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * // URL param: GET /orgs/:tenantId/invoices
36
+ * @UseGuards(JwtAuthGuard, TenantGuard())
37
+ * @Controller('/orgs/:tenantId/invoices')
38
+ * export class InvoicesController {}
39
+ *
40
+ * // Header: X-Org-ID
41
+ * @UseGuards(JwtAuthGuard, TenantGuard({ source: 'header', key: 'x-org-id' }))
42
+ * @Controller('/invoices')
43
+ * export class InvoicesController {}
44
+ *
45
+ * // Superadmins bypass tenant check:
46
+ * @UseGuards(JwtAuthGuard, TenantGuard({ bypassRoles: ['superadmin'] }))
47
+ * @Controller('/orgs/:tenantId/invoices')
48
+ * export class InvoicesController {}
49
+ * ```
50
+ */
51
+ export declare function TenantGuard(options?: TenantGuardOptions & {
52
+ bypassRoles?: string[];
53
+ }): Type<CanActivate>;
54
+ //# sourceMappingURL=tenant.guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.guard.d.ts","sourceRoot":"","sources":["../../src/guards/tenant.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,WAAW,EAAoB,IAAI,EAAkB,MAAM,eAAe,CAAC;AAIhG,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IAEtC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAkB,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAC5D,IAAI,CAAC,WAAW,CAAC,CAsEnB"}
@@ -0,0 +1,96 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.TenantGuard = TenantGuard;
10
+ const core_1 = require("@hazeljs/core");
11
+ const tenant_context_1 = require("../tenant/tenant-context");
12
+ /**
13
+ * Factory that returns a guard enforcing tenant-level isolation.
14
+ *
15
+ * Compares the tenant ID carried by the request (from a URL param, header,
16
+ * or query param) against the tenant ID stored on the authenticated user
17
+ * (from the JWT payload). Returns 403 if they do not match.
18
+ *
19
+ * Must be used after JwtAuthGuard so that `req.user` is populated.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * // URL param: GET /orgs/:tenantId/invoices
24
+ * @UseGuards(JwtAuthGuard, TenantGuard())
25
+ * @Controller('/orgs/:tenantId/invoices')
26
+ * export class InvoicesController {}
27
+ *
28
+ * // Header: X-Org-ID
29
+ * @UseGuards(JwtAuthGuard, TenantGuard({ source: 'header', key: 'x-org-id' }))
30
+ * @Controller('/invoices')
31
+ * export class InvoicesController {}
32
+ *
33
+ * // Superadmins bypass tenant check:
34
+ * @UseGuards(JwtAuthGuard, TenantGuard({ bypassRoles: ['superadmin'] }))
35
+ * @Controller('/orgs/:tenantId/invoices')
36
+ * export class InvoicesController {}
37
+ * ```
38
+ */
39
+ function TenantGuard(options = {}) {
40
+ const { source = 'param', key = 'tenantId', userField = 'tenantId', bypassRoles = [] } = options;
41
+ let TenantGuardMixin = class TenantGuardMixin {
42
+ canActivate(context) {
43
+ const req = context.switchToHttp().getRequest();
44
+ const ctx = context.switchToHttp().getContext();
45
+ const user = req.user;
46
+ if (!user) {
47
+ const err = Object.assign(new Error('Unauthorized'), { status: 401 });
48
+ throw err;
49
+ }
50
+ // Privileged roles can bypass the tenant check entirely.
51
+ // Still seed the context so their own queries are naturally scoped.
52
+ if (bypassRoles.includes(user.role)) {
53
+ const bypassTenantId = user[userField];
54
+ if (bypassTenantId)
55
+ tenant_context_1.TenantContext.enterWith(bypassTenantId);
56
+ return true;
57
+ }
58
+ const userTenantId = user[userField];
59
+ if (!userTenantId) {
60
+ const err = Object.assign(new Error(`User is not associated with any tenant (missing "${userField}")`), { status: 403 });
61
+ throw err;
62
+ }
63
+ // Extract the request-level tenant ID from the configured source.
64
+ let requestTenantId;
65
+ switch (source) {
66
+ case 'param':
67
+ requestTenantId = ctx.params?.[key];
68
+ break;
69
+ case 'header':
70
+ requestTenantId = ctx.headers?.[key.toLowerCase()];
71
+ break;
72
+ case 'query':
73
+ requestTenantId = ctx.query?.[key];
74
+ break;
75
+ }
76
+ if (!requestTenantId) {
77
+ const err = Object.assign(new Error(`Tenant ID not found in request ${source} "${key}"`), {
78
+ status: 400,
79
+ });
80
+ throw err;
81
+ }
82
+ if (userTenantId !== requestTenantId) {
83
+ const err = Object.assign(new Error('Access denied: resource belongs to a different tenant'), { status: 403 });
84
+ throw err;
85
+ }
86
+ // Seed AsyncLocalStorage so repositories can call tenantCtx.requireId()
87
+ // without needing tenantId passed through every function parameter.
88
+ tenant_context_1.TenantContext.enterWith(userTenantId);
89
+ return true;
90
+ }
91
+ };
92
+ TenantGuardMixin = __decorate([
93
+ (0, core_1.Injectable)()
94
+ ], TenantGuardMixin);
95
+ return TenantGuardMixin;
96
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,18 @@
1
1
  /**
2
2
  * @hazeljs/auth - Authentication module for HazelJS
3
3
  */
4
- export { AuthGuard } from './auth.guard';
4
+ export { AuthGuard, Auth } from './auth.guard';
5
5
  export { AuthService } from './auth.service';
6
+ export type { AuthUser } from './auth.service';
6
7
  export { JwtModule } from './jwt/jwt.module';
7
8
  export { JwtService } from './jwt/jwt.service';
9
+ export { JwtAuthGuard } from './guards/jwt-auth.guard';
10
+ export { RoleGuard } from './guards/role.guard';
11
+ export type { RoleGuardOptions } from './guards/role.guard';
12
+ export { TenantGuard } from './guards/tenant.guard';
13
+ export type { TenantGuardOptions } from './guards/tenant.guard';
14
+ export { TenantContext } from './tenant/tenant-context';
15
+ export { RoleHierarchy, DEFAULT_ROLE_HIERARCHY } from './utils/role-hierarchy';
16
+ export type { RoleHierarchyMap } from './utils/role-hierarchy';
17
+ export { CurrentUser } from './decorators/current-user.decorator';
8
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,YAAY,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC"}
package/dist/index.js CHANGED
@@ -3,12 +3,26 @@
3
3
  * @hazeljs/auth - Authentication module for HazelJS
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.JwtService = exports.JwtModule = exports.AuthService = exports.AuthGuard = void 0;
6
+ exports.CurrentUser = exports.DEFAULT_ROLE_HIERARCHY = exports.RoleHierarchy = exports.TenantContext = exports.TenantGuard = exports.RoleGuard = exports.JwtAuthGuard = exports.JwtService = exports.JwtModule = exports.AuthService = exports.Auth = exports.AuthGuard = void 0;
7
7
  var auth_guard_1 = require("./auth.guard");
8
8
  Object.defineProperty(exports, "AuthGuard", { enumerable: true, get: function () { return auth_guard_1.AuthGuard; } });
9
+ Object.defineProperty(exports, "Auth", { enumerable: true, get: function () { return auth_guard_1.Auth; } });
9
10
  var auth_service_1 = require("./auth.service");
10
11
  Object.defineProperty(exports, "AuthService", { enumerable: true, get: function () { return auth_service_1.AuthService; } });
11
12
  var jwt_module_1 = require("./jwt/jwt.module");
12
13
  Object.defineProperty(exports, "JwtModule", { enumerable: true, get: function () { return jwt_module_1.JwtModule; } });
13
14
  var jwt_service_1 = require("./jwt/jwt.service");
14
15
  Object.defineProperty(exports, "JwtService", { enumerable: true, get: function () { return jwt_service_1.JwtService; } });
16
+ var jwt_auth_guard_1 = require("./guards/jwt-auth.guard");
17
+ Object.defineProperty(exports, "JwtAuthGuard", { enumerable: true, get: function () { return jwt_auth_guard_1.JwtAuthGuard; } });
18
+ var role_guard_1 = require("./guards/role.guard");
19
+ Object.defineProperty(exports, "RoleGuard", { enumerable: true, get: function () { return role_guard_1.RoleGuard; } });
20
+ var tenant_guard_1 = require("./guards/tenant.guard");
21
+ Object.defineProperty(exports, "TenantGuard", { enumerable: true, get: function () { return tenant_guard_1.TenantGuard; } });
22
+ var tenant_context_1 = require("./tenant/tenant-context");
23
+ Object.defineProperty(exports, "TenantContext", { enumerable: true, get: function () { return tenant_context_1.TenantContext; } });
24
+ var role_hierarchy_1 = require("./utils/role-hierarchy");
25
+ Object.defineProperty(exports, "RoleHierarchy", { enumerable: true, get: function () { return role_hierarchy_1.RoleHierarchy; } });
26
+ Object.defineProperty(exports, "DEFAULT_ROLE_HIERARCHY", { enumerable: true, get: function () { return role_hierarchy_1.DEFAULT_ROLE_HIERARCHY; } });
27
+ var current_user_decorator_1 = require("./decorators/current-user.decorator");
28
+ Object.defineProperty(exports, "CurrentUser", { enumerable: true, get: function () { return current_user_decorator_1.CurrentUser; } });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Provides request-scoped tenant context via AsyncLocalStorage.
3
+ *
4
+ * ─── Why two layers? ──────────────────────────────────────────────────────────
5
+ * TenantGuard enforces isolation at the HTTP layer — it rejects requests where
6
+ * the JWT tenant doesn't match the route tenant. That's necessary but not
7
+ * sufficient: a bug in service code could still query another tenant's rows if
8
+ * the guard is misconfigured or skipped.
9
+ *
10
+ * TenantContext closes that gap at the DATA layer. After the guard validates
11
+ * the request, it calls TenantContext.enterWith(tenantId) which seeds an
12
+ * AsyncLocalStorage store for the remainder of the request's async call chain.
13
+ * Every repository/service that injects TenantContext can then call
14
+ * requireId() without receiving tenantId as a function parameter.
15
+ *
16
+ * ─── Usage in a repository ───────────────────────────────────────────────────
17
+ * ```ts
18
+ * @Service()
19
+ * export class OrdersRepository {
20
+ * constructor(private readonly tenantCtx: TenantContext) {}
21
+ *
22
+ * findAll() {
23
+ * const tenantId = this.tenantCtx.requireId();
24
+ * return db.query('SELECT * FROM orders WHERE tenant_id = $1', [tenantId]);
25
+ * }
26
+ *
27
+ * findById(id: string) {
28
+ * const tenantId = this.tenantCtx.requireId();
29
+ * // Even a direct ID lookup is scoped — prevents horizontal privilege escalation
30
+ * return db.query(
31
+ * 'SELECT * FROM orders WHERE id = $1 AND tenant_id = $2',
32
+ * [id, tenantId]
33
+ * );
34
+ * }
35
+ * }
36
+ * ```
37
+ *
38
+ * ─── How it propagates ────────────────────────────────────────────────────────
39
+ * Node.js AsyncLocalStorage propagates through the async call graph.
40
+ * TenantGuard calls enterWith() during guard execution; because the route
41
+ * handler is called afterwards in the same async chain, it (and everything
42
+ * it awaits) automatically has access to the stored tenant ID.
43
+ */
44
+ export declare class TenantContext {
45
+ /**
46
+ * Returns the current tenant ID, or `undefined` if called outside a
47
+ * request context (e.g. during startup or in a background job).
48
+ */
49
+ getId(): string | undefined;
50
+ /**
51
+ * Returns the current tenant ID and throws if it is not set.
52
+ *
53
+ * Use this in any repository or service method that must never run without
54
+ * a tenant — it acts as a last-resort safety net even if the guard was
55
+ * accidentally omitted on a route.
56
+ */
57
+ requireId(): string;
58
+ /**
59
+ * Seeds the current async context with the given tenant ID.
60
+ *
61
+ * Called internally by TenantGuard after it validates the request.
62
+ * Unlike `run()`, `enterWith` propagates through the rest of the current
63
+ * async execution chain without requiring a wrapping callback — which
64
+ * makes it the right tool for seeding from within a guard.
65
+ */
66
+ static enterWith(tenantId: string): void;
67
+ /**
68
+ * Wraps a callback so that everything inside it (and all async operations
69
+ * it spawns) runs with the given tenant ID in context.
70
+ *
71
+ * Use this when you need explicit scoping, e.g. in background jobs or tests:
72
+ *
73
+ * ```ts
74
+ * await TenantContext.run('acme', async () => {
75
+ * await ordersService.processPendingOrders();
76
+ * });
77
+ * ```
78
+ */
79
+ static run<T>(tenantId: string, fn: () => T): T;
80
+ }
81
+ //# sourceMappingURL=tenant-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-context.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant-context.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,qBACa,aAAa;IACxB;;;OAGG;IACH,KAAK,IAAI,MAAM,GAAG,SAAS;IAI3B;;;;;;OAMG;IACH,SAAS,IAAI,MAAM;IAanB;;;;;;;OAOG;IACH,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIxC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC;CAGhD"}
@@ -0,0 +1,108 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.TenantContext = void 0;
10
+ const async_hooks_1 = require("async_hooks");
11
+ const core_1 = require("@hazeljs/core");
12
+ const storage = new async_hooks_1.AsyncLocalStorage();
13
+ /**
14
+ * Provides request-scoped tenant context via AsyncLocalStorage.
15
+ *
16
+ * ─── Why two layers? ──────────────────────────────────────────────────────────
17
+ * TenantGuard enforces isolation at the HTTP layer — it rejects requests where
18
+ * the JWT tenant doesn't match the route tenant. That's necessary but not
19
+ * sufficient: a bug in service code could still query another tenant's rows if
20
+ * the guard is misconfigured or skipped.
21
+ *
22
+ * TenantContext closes that gap at the DATA layer. After the guard validates
23
+ * the request, it calls TenantContext.enterWith(tenantId) which seeds an
24
+ * AsyncLocalStorage store for the remainder of the request's async call chain.
25
+ * Every repository/service that injects TenantContext can then call
26
+ * requireId() without receiving tenantId as a function parameter.
27
+ *
28
+ * ─── Usage in a repository ───────────────────────────────────────────────────
29
+ * ```ts
30
+ * @Service()
31
+ * export class OrdersRepository {
32
+ * constructor(private readonly tenantCtx: TenantContext) {}
33
+ *
34
+ * findAll() {
35
+ * const tenantId = this.tenantCtx.requireId();
36
+ * return db.query('SELECT * FROM orders WHERE tenant_id = $1', [tenantId]);
37
+ * }
38
+ *
39
+ * findById(id: string) {
40
+ * const tenantId = this.tenantCtx.requireId();
41
+ * // Even a direct ID lookup is scoped — prevents horizontal privilege escalation
42
+ * return db.query(
43
+ * 'SELECT * FROM orders WHERE id = $1 AND tenant_id = $2',
44
+ * [id, tenantId]
45
+ * );
46
+ * }
47
+ * }
48
+ * ```
49
+ *
50
+ * ─── How it propagates ────────────────────────────────────────────────────────
51
+ * Node.js AsyncLocalStorage propagates through the async call graph.
52
+ * TenantGuard calls enterWith() during guard execution; because the route
53
+ * handler is called afterwards in the same async chain, it (and everything
54
+ * it awaits) automatically has access to the stored tenant ID.
55
+ */
56
+ let TenantContext = class TenantContext {
57
+ /**
58
+ * Returns the current tenant ID, or `undefined` if called outside a
59
+ * request context (e.g. during startup or in a background job).
60
+ */
61
+ getId() {
62
+ return storage.getStore()?.tenantId;
63
+ }
64
+ /**
65
+ * Returns the current tenant ID and throws if it is not set.
66
+ *
67
+ * Use this in any repository or service method that must never run without
68
+ * a tenant — it acts as a last-resort safety net even if the guard was
69
+ * accidentally omitted on a route.
70
+ */
71
+ requireId() {
72
+ const id = this.getId();
73
+ if (!id) {
74
+ throw Object.assign(new Error('TenantContext: no tenant ID found. ' + 'Ensure TenantGuard is applied to the route.'), { status: 500 });
75
+ }
76
+ return id;
77
+ }
78
+ /**
79
+ * Seeds the current async context with the given tenant ID.
80
+ *
81
+ * Called internally by TenantGuard after it validates the request.
82
+ * Unlike `run()`, `enterWith` propagates through the rest of the current
83
+ * async execution chain without requiring a wrapping callback — which
84
+ * makes it the right tool for seeding from within a guard.
85
+ */
86
+ static enterWith(tenantId) {
87
+ storage.enterWith({ tenantId });
88
+ }
89
+ /**
90
+ * Wraps a callback so that everything inside it (and all async operations
91
+ * it spawns) runs with the given tenant ID in context.
92
+ *
93
+ * Use this when you need explicit scoping, e.g. in background jobs or tests:
94
+ *
95
+ * ```ts
96
+ * await TenantContext.run('acme', async () => {
97
+ * await ordersService.processPendingOrders();
98
+ * });
99
+ * ```
100
+ */
101
+ static run(tenantId, fn) {
102
+ return storage.run({ tenantId }, fn);
103
+ }
104
+ };
105
+ exports.TenantContext = TenantContext;
106
+ exports.TenantContext = TenantContext = __decorate([
107
+ (0, core_1.Injectable)()
108
+ ], TenantContext);
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Describes which roles a given role "inherits".
3
+ *
4
+ * Example: `{ superadmin: ['admin'], admin: ['manager'], manager: ['user'] }`
5
+ * means superadmin implicitly satisfies any check for admin, manager, or user.
6
+ */
7
+ export type RoleHierarchyMap = Record<string, string[]>;
8
+ /**
9
+ * A sensible default hierarchy for most applications.
10
+ * Override by passing your own map to RoleHierarchy or to RoleGuard.
11
+ *
12
+ * superadmin → admin → manager → user
13
+ */
14
+ export declare const DEFAULT_ROLE_HIERARCHY: RoleHierarchyMap;
15
+ /**
16
+ * Resolves inherited roles so that a higher-level role implicitly satisfies
17
+ * requirements for any role it inherits.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const h = new RoleHierarchy({ admin: ['editor'], editor: ['viewer'] });
22
+ *
23
+ * h.satisfies('admin', 'viewer') // true — admin → editor → viewer
24
+ * h.satisfies('editor', 'admin') // false — editor does not inherit admin
25
+ * h.resolve('admin') // Set { 'admin', 'editor', 'viewer' }
26
+ * ```
27
+ */
28
+ export declare class RoleHierarchy {
29
+ private readonly map;
30
+ constructor(map?: RoleHierarchyMap);
31
+ /**
32
+ * Returns true if `userRole` satisfies the `requiredRole` check, either
33
+ * directly (same role) or via inheritance.
34
+ */
35
+ satisfies(userRole: string, requiredRole: string): boolean;
36
+ /**
37
+ * Returns the complete set of roles that `role` covers (itself plus all
38
+ * transitively inherited roles).
39
+ */
40
+ resolve(role: string): Set<string>;
41
+ }
42
+ //# sourceMappingURL=role-hierarchy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-hierarchy.d.ts","sourceRoot":"","sources":["../../src/utils/role-hierarchy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,EAAE,gBAKpC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,GAAE,gBAAyC;IAE3E;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO;IAI1D;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;CAcnC"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RoleHierarchy = exports.DEFAULT_ROLE_HIERARCHY = void 0;
4
+ /**
5
+ * A sensible default hierarchy for most applications.
6
+ * Override by passing your own map to RoleHierarchy or to RoleGuard.
7
+ *
8
+ * superadmin → admin → manager → user
9
+ */
10
+ exports.DEFAULT_ROLE_HIERARCHY = {
11
+ superadmin: ['admin'],
12
+ admin: ['manager'],
13
+ manager: ['user'],
14
+ user: [],
15
+ };
16
+ /**
17
+ * Resolves inherited roles so that a higher-level role implicitly satisfies
18
+ * requirements for any role it inherits.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const h = new RoleHierarchy({ admin: ['editor'], editor: ['viewer'] });
23
+ *
24
+ * h.satisfies('admin', 'viewer') // true — admin → editor → viewer
25
+ * h.satisfies('editor', 'admin') // false — editor does not inherit admin
26
+ * h.resolve('admin') // Set { 'admin', 'editor', 'viewer' }
27
+ * ```
28
+ */
29
+ class RoleHierarchy {
30
+ constructor(map = exports.DEFAULT_ROLE_HIERARCHY) {
31
+ this.map = map;
32
+ }
33
+ /**
34
+ * Returns true if `userRole` satisfies the `requiredRole` check, either
35
+ * directly (same role) or via inheritance.
36
+ */
37
+ satisfies(userRole, requiredRole) {
38
+ return this.resolve(userRole).has(requiredRole);
39
+ }
40
+ /**
41
+ * Returns the complete set of roles that `role` covers (itself plus all
42
+ * transitively inherited roles).
43
+ */
44
+ resolve(role) {
45
+ const visited = new Set();
46
+ const queue = [role];
47
+ while (queue.length > 0) {
48
+ const current = queue.shift();
49
+ if (!visited.has(current)) {
50
+ visited.add(current);
51
+ (this.map[current] ?? []).forEach((child) => queue.push(child));
52
+ }
53
+ }
54
+ return visited;
55
+ }
56
+ }
57
+ exports.RoleHierarchy = RoleHierarchy;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hazeljs/auth",
3
- "version": "0.2.0-beta.58",
3
+ "version": "0.2.0-beta.59",
4
4
  "description": "Authentication and JWT module for HazelJS framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -51,5 +51,5 @@
51
51
  "peerDependencies": {
52
52
  "@hazeljs/core": ">=0.2.0-beta.0"
53
53
  },
54
- "gitHead": "ce3821b373043284ac4847315371efb137dc860d"
54
+ "gitHead": "f6d8ee8162a40e2298ccce46d843269838bbe6ff"
55
55
  }