@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.
- package/LICENSE +192 -21
- package/README.md +348 -332
- package/dist/auth.service.js +1 -1
- package/dist/auth.test.d.ts +2 -0
- package/dist/auth.test.d.ts.map +1 -0
- package/dist/auth.test.js +682 -0
- package/dist/decorators/current-user.decorator.d.ts +26 -0
- package/dist/decorators/current-user.decorator.d.ts.map +1 -0
- package/dist/decorators/current-user.decorator.js +39 -0
- package/dist/guards/jwt-auth.guard.d.ts +24 -0
- package/dist/guards/jwt-auth.guard.d.ts.map +1 -0
- package/dist/guards/jwt-auth.guard.js +61 -0
- package/dist/guards/role.guard.d.ts +36 -0
- package/dist/guards/role.guard.d.ts.map +1 -0
- package/dist/guards/role.guard.js +66 -0
- package/dist/guards/tenant.guard.d.ts +54 -0
- package/dist/guards/tenant.guard.d.ts.map +1 -0
- package/dist/guards/tenant.guard.js +96 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/jwt/jwt.service.js +1 -1
- package/dist/tenant/tenant-context.d.ts +81 -0
- package/dist/tenant/tenant-context.d.ts.map +1 -0
- package/dist/tenant/tenant-context.js +108 -0
- package/dist/utils/role-hierarchy.d.ts +42 -0
- package/dist/utils/role-hierarchy.d.ts.map +1 -0
- package/dist/utils/role-hierarchy.js +57 -0
- package/package.json +11 -5
|
@@ -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.
|
|
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 <
|
|
47
|
-
"license": "
|
|
48
|
-
"
|
|
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
|
}
|