@hazeljs/auth 0.2.0-beta.8 → 0.2.0-beta.81

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,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.8",
3
+ "version": "0.2.0-beta.81",
4
4
  "description": "Authentication and JWT module for HazelJS framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,7 +15,6 @@
15
15
  "clean": "rm -rf dist"
16
16
  },
17
17
  "dependencies": {
18
- "@hazeljs/core": "^0.2.0-beta.8",
19
18
  "jsonwebtoken": "^9.0.2"
20
19
  },
21
20
  "devDependencies": {
@@ -43,7 +42,14 @@
43
42
  "jwt",
44
43
  "security"
45
44
  ],
46
- "author": "Muhammad Arslan <marslan@hazeljs.com>",
47
- "license": "MIT",
48
- "gitHead": "b5cc0bf7d43739cad25220a611490b1d175acd62"
45
+ "author": "Muhammad Arslan <muhammad.arslan@hazeljs.com>",
46
+ "license": "Apache-2.0",
47
+ "bugs": {
48
+ "url": "https://github.com/hazeljs/hazel-js/issues"
49
+ },
50
+ "homepage": "https://hazeljs.com",
51
+ "peerDependencies": {
52
+ "@hazeljs/core": ">=0.2.0-beta.0"
53
+ },
54
+ "gitHead": "1054f957451360f973469d436a1d58e57cc9231a"
49
55
  }