@meridianjs/auth 0.1.2 → 0.1.4

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/dist/index.d.mts CHANGED
@@ -27,6 +27,8 @@ interface JwtPayload {
27
27
  sub: string;
28
28
  workspaceId: string | null;
29
29
  roles: string[];
30
+ permissions: string[];
31
+ jti?: string;
30
32
  iat?: number;
31
33
  exp?: number;
32
34
  }
@@ -40,17 +42,17 @@ declare class AuthModuleService extends AuthModuleService_base {
40
42
  login(input: LoginInput): Promise<AuthResult>;
41
43
  /** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
42
44
  verifyToken(token: string, secret: string): JwtPayload;
45
+ /** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
46
+ private resolvePermissions;
43
47
  private signToken;
44
48
  }
45
49
 
46
50
  /**
47
51
  * Express middleware that validates a Bearer JWT on every request.
48
52
  *
49
- * On success, populates req.user = { id, workspaceId, roles } and calls next().
53
+ * On success, populates req.user = { id, workspaceId, roles, permissions, jti } and calls next().
54
+ * Also validates the session against the DB (stateful revocation support).
50
55
  * On failure, responds 401 Unauthorized.
51
- *
52
- * Reads jwtSecret from req.scope (the request-scoped DI container that is
53
- * attached by the framework before any middleware runs).
54
56
  */
55
57
  declare function authenticateJWT(req: any, res: Response, next: NextFunction): void;
56
58
 
@@ -64,6 +66,19 @@ declare function authenticateJWT(req: any, res: Response, next: NextFunction): v
64
66
  * { matcher: "/admin/settings", middlewares: [authenticateJWT, requireRoles("admin")] }
65
67
  */
66
68
  declare function requireRoles(...roles: string[]): (req: any, res: Response, next: NextFunction) => void;
69
+ /**
70
+ * Permission guard — allows the request if `req.user.roles` includes "super-admin"
71
+ * (full bypass) or if `req.user.permissions` contains at least one of the listed
72
+ * permissions.
73
+ *
74
+ * Must be used after `authenticateJWT` so that `req.user` is populated.
75
+ *
76
+ * @example
77
+ * export const POST = async (req, res) => {
78
+ * requirePermission("project:create")(req, res, async () => { ... })
79
+ * }
80
+ */
81
+ declare function requirePermission(...permissions: string[]): (req: any, res: Response, next: NextFunction) => void;
67
82
  /**
68
83
  * Workspace isolation guard — rejects requests where the `workspace_id` query
69
84
  * param or body field does not match the authenticated user's workspace.
@@ -78,4 +93,4 @@ declare function requireWorkspace(req: any, res: Response, next: NextFunction):
78
93
  declare const AUTH_MODULE = "authModuleService";
79
94
  declare const _default: _meridianjs_types.ModuleDefinition;
80
95
 
81
- export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requireRoles, requireWorkspace };
96
+ export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requirePermission, requireRoles, requireWorkspace };
package/dist/index.d.ts CHANGED
@@ -27,6 +27,8 @@ interface JwtPayload {
27
27
  sub: string;
28
28
  workspaceId: string | null;
29
29
  roles: string[];
30
+ permissions: string[];
31
+ jti?: string;
30
32
  iat?: number;
31
33
  exp?: number;
32
34
  }
@@ -40,17 +42,17 @@ declare class AuthModuleService extends AuthModuleService_base {
40
42
  login(input: LoginInput): Promise<AuthResult>;
41
43
  /** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
42
44
  verifyToken(token: string, secret: string): JwtPayload;
45
+ /** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
46
+ private resolvePermissions;
43
47
  private signToken;
44
48
  }
45
49
 
46
50
  /**
47
51
  * Express middleware that validates a Bearer JWT on every request.
48
52
  *
49
- * On success, populates req.user = { id, workspaceId, roles } and calls next().
53
+ * On success, populates req.user = { id, workspaceId, roles, permissions, jti } and calls next().
54
+ * Also validates the session against the DB (stateful revocation support).
50
55
  * On failure, responds 401 Unauthorized.
51
- *
52
- * Reads jwtSecret from req.scope (the request-scoped DI container that is
53
- * attached by the framework before any middleware runs).
54
56
  */
55
57
  declare function authenticateJWT(req: any, res: Response, next: NextFunction): void;
56
58
 
@@ -64,6 +66,19 @@ declare function authenticateJWT(req: any, res: Response, next: NextFunction): v
64
66
  * { matcher: "/admin/settings", middlewares: [authenticateJWT, requireRoles("admin")] }
65
67
  */
66
68
  declare function requireRoles(...roles: string[]): (req: any, res: Response, next: NextFunction) => void;
69
+ /**
70
+ * Permission guard — allows the request if `req.user.roles` includes "super-admin"
71
+ * (full bypass) or if `req.user.permissions` contains at least one of the listed
72
+ * permissions.
73
+ *
74
+ * Must be used after `authenticateJWT` so that `req.user` is populated.
75
+ *
76
+ * @example
77
+ * export const POST = async (req, res) => {
78
+ * requirePermission("project:create")(req, res, async () => { ... })
79
+ * }
80
+ */
81
+ declare function requirePermission(...permissions: string[]): (req: any, res: Response, next: NextFunction) => void;
67
82
  /**
68
83
  * Workspace isolation guard — rejects requests where the `workspace_id` query
69
84
  * param or body field does not match the authenticated user's workspace.
@@ -78,4 +93,4 @@ declare function requireWorkspace(req: any, res: Response, next: NextFunction):
78
93
  declare const AUTH_MODULE = "authModuleService";
79
94
  declare const _default: _meridianjs_types.ModuleDefinition;
80
95
 
81
- export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requireRoles, requireWorkspace };
96
+ export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requirePermission, requireRoles, requireWorkspace };
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  AuthModuleService: () => AuthModuleService,
35
35
  authenticateJWT: () => authenticateJWT,
36
36
  default: () => index_default,
37
+ requirePermission: () => requirePermission,
37
38
  requireRoles: () => requireRoles,
38
39
  requireWorkspace: () => requireWorkspace
39
40
  });
@@ -44,8 +45,10 @@ var import_framework_utils2 = require("@meridianjs/framework-utils");
44
45
  var import_framework_utils = require("@meridianjs/framework-utils");
45
46
  var import_bcrypt = __toESM(require("bcrypt"));
46
47
  var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
48
+ var import_crypto = require("crypto");
47
49
  var BCRYPT_ROUNDS = 12;
48
50
  var JWT_EXPIRES_IN = "7d";
51
+ var JWT_EXPIRES_MS = 7 * 24 * 60 * 60 * 1e3;
49
52
  var AuthModuleService = class extends (0, import_framework_utils.MeridianService)({}) {
50
53
  container;
51
54
  constructor(container) {
@@ -74,7 +77,10 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
74
77
  role,
75
78
  is_active: true
76
79
  });
77
- const token = this.signToken(user.id, null, [user.role], config.projectConfig.jwtSecret);
80
+ const permissions = await this.resolvePermissions(user.app_role_id);
81
+ const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role], permissions, config.projectConfig.jwtSecret);
82
+ await userService.createSession(jti, user.id, expiresAt).catch(() => {
83
+ });
78
84
  return {
79
85
  user: {
80
86
  id: user.id,
@@ -102,7 +108,10 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
102
108
  }
103
109
  await userService.recordLogin(user.id).catch(() => {
104
110
  });
105
- const token = this.signToken(user.id, null, [user.role ?? "member"], config.projectConfig.jwtSecret);
111
+ const permissions = await this.resolvePermissions(user.app_role_id);
112
+ const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role ?? "member"], permissions, config.projectConfig.jwtSecret);
113
+ await userService.createSession(jti, user.id, expiresAt).catch(() => {
114
+ });
106
115
  return {
107
116
  user: {
108
117
  id: user.id,
@@ -117,10 +126,23 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
117
126
  verifyToken(token, secret) {
118
127
  return import_jsonwebtoken.default.verify(token, secret);
119
128
  }
120
- signToken(userId, workspaceId, roles, secret) {
121
- return import_jsonwebtoken.default.sign({ sub: userId, workspaceId, roles }, secret, {
129
+ /** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
130
+ async resolvePermissions(appRoleId) {
131
+ if (!appRoleId) return [];
132
+ try {
133
+ const appRoleService = this.container.resolve("appRoleModuleService");
134
+ return await appRoleService.getPermissionsForRole(appRoleId);
135
+ } catch {
136
+ return [];
137
+ }
138
+ }
139
+ signToken(userId, workspaceId, roles, permissions, secret) {
140
+ const jti = (0, import_crypto.randomUUID)();
141
+ const expiresAt = new Date(Date.now() + JWT_EXPIRES_MS);
142
+ const token = import_jsonwebtoken.default.sign({ sub: userId, workspaceId, roles, permissions, jti }, secret, {
122
143
  expiresIn: JWT_EXPIRES_IN
123
144
  });
145
+ return { token, jti, expiresAt };
124
146
  }
125
147
  };
126
148
 
@@ -133,25 +155,42 @@ function authenticateJWT(req, res, next) {
133
155
  res.status(401).json({ error: { message: "Unauthorized \u2014 Bearer token required" } });
134
156
  return;
135
157
  }
136
- let config;
137
- try {
138
- const scope = req.scope;
139
- config = scope.resolve("config");
140
- } catch {
141
- res.status(500).json({ error: { message: "Server misconfiguration" } });
142
- return;
143
- }
144
- try {
145
- const payload = import_jsonwebtoken2.default.verify(token, config.projectConfig.jwtSecret);
146
- req.user = {
147
- id: payload.sub,
148
- workspaceId: payload.workspaceId ?? null,
149
- roles: Array.isArray(payload.roles) ? payload.roles : []
150
- };
151
- next();
152
- } catch {
153
- res.status(401).json({ error: { message: "Invalid or expired token" } });
154
- }
158
+ ;
159
+ (async () => {
160
+ let config;
161
+ try {
162
+ const scope = req.scope;
163
+ config = scope.resolve("config");
164
+ } catch {
165
+ res.status(500).json({ error: { message: "Server misconfiguration" } });
166
+ return;
167
+ }
168
+ try {
169
+ const payload = import_jsonwebtoken2.default.verify(token, config.projectConfig.jwtSecret);
170
+ if (payload.jti) {
171
+ try {
172
+ const scope = req.scope;
173
+ const userService = scope.resolve("userModuleService");
174
+ const valid = await userService.isSessionValid(payload.jti);
175
+ if (!valid) {
176
+ res.status(401).json({ error: { message: "Session revoked or expired" } });
177
+ return;
178
+ }
179
+ } catch {
180
+ }
181
+ }
182
+ req.user = {
183
+ id: payload.sub,
184
+ workspaceId: payload.workspaceId ?? null,
185
+ roles: Array.isArray(payload.roles) ? payload.roles : [],
186
+ permissions: Array.isArray(payload.permissions) ? payload.permissions : [],
187
+ jti: payload.jti ?? null
188
+ };
189
+ next();
190
+ } catch {
191
+ res.status(401).json({ error: { message: "Invalid or expired token" } });
192
+ }
193
+ })();
155
194
  }
156
195
 
157
196
  // src/guards.ts
@@ -162,6 +201,15 @@ function requireRoles(...roles) {
162
201
  res.status(403).json({ error: { message: "Forbidden" } });
163
202
  };
164
203
  }
204
+ function requirePermission(...permissions) {
205
+ return (req, res, next) => {
206
+ const userRoles = req.user?.roles ?? [];
207
+ if (userRoles.includes("super-admin")) return next();
208
+ const userPermissions = req.user?.permissions ?? [];
209
+ if (permissions.some((p) => userPermissions.includes(p))) return next();
210
+ res.status(403).json({ error: { message: "Forbidden \u2014 insufficient permissions" } });
211
+ };
212
+ }
165
213
  function requireWorkspace(req, res, next) {
166
214
  const workspaceId = req.query?.workspace_id ?? req.body?.workspace_id;
167
215
  if (workspaceId && req.user?.workspaceId && req.user.workspaceId !== workspaceId) {
@@ -180,6 +228,7 @@ var index_default = (0, import_framework_utils2.Module)(AUTH_MODULE, {
180
228
  AUTH_MODULE,
181
229
  AuthModuleService,
182
230
  authenticateJWT,
231
+ requirePermission,
183
232
  requireRoles,
184
233
  requireWorkspace
185
234
  });
package/dist/index.mjs CHANGED
@@ -5,8 +5,10 @@ import { Module } from "@meridianjs/framework-utils";
5
5
  import { MeridianService } from "@meridianjs/framework-utils";
6
6
  import bcrypt from "bcrypt";
7
7
  import jwt from "jsonwebtoken";
8
+ import { randomUUID } from "crypto";
8
9
  var BCRYPT_ROUNDS = 12;
9
10
  var JWT_EXPIRES_IN = "7d";
11
+ var JWT_EXPIRES_MS = 7 * 24 * 60 * 60 * 1e3;
10
12
  var AuthModuleService = class extends MeridianService({}) {
11
13
  container;
12
14
  constructor(container) {
@@ -35,7 +37,10 @@ var AuthModuleService = class extends MeridianService({}) {
35
37
  role,
36
38
  is_active: true
37
39
  });
38
- const token = this.signToken(user.id, null, [user.role], config.projectConfig.jwtSecret);
40
+ const permissions = await this.resolvePermissions(user.app_role_id);
41
+ const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role], permissions, config.projectConfig.jwtSecret);
42
+ await userService.createSession(jti, user.id, expiresAt).catch(() => {
43
+ });
39
44
  return {
40
45
  user: {
41
46
  id: user.id,
@@ -63,7 +68,10 @@ var AuthModuleService = class extends MeridianService({}) {
63
68
  }
64
69
  await userService.recordLogin(user.id).catch(() => {
65
70
  });
66
- const token = this.signToken(user.id, null, [user.role ?? "member"], config.projectConfig.jwtSecret);
71
+ const permissions = await this.resolvePermissions(user.app_role_id);
72
+ const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role ?? "member"], permissions, config.projectConfig.jwtSecret);
73
+ await userService.createSession(jti, user.id, expiresAt).catch(() => {
74
+ });
67
75
  return {
68
76
  user: {
69
77
  id: user.id,
@@ -78,10 +86,23 @@ var AuthModuleService = class extends MeridianService({}) {
78
86
  verifyToken(token, secret) {
79
87
  return jwt.verify(token, secret);
80
88
  }
81
- signToken(userId, workspaceId, roles, secret) {
82
- return jwt.sign({ sub: userId, workspaceId, roles }, secret, {
89
+ /** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
90
+ async resolvePermissions(appRoleId) {
91
+ if (!appRoleId) return [];
92
+ try {
93
+ const appRoleService = this.container.resolve("appRoleModuleService");
94
+ return await appRoleService.getPermissionsForRole(appRoleId);
95
+ } catch {
96
+ return [];
97
+ }
98
+ }
99
+ signToken(userId, workspaceId, roles, permissions, secret) {
100
+ const jti = randomUUID();
101
+ const expiresAt = new Date(Date.now() + JWT_EXPIRES_MS);
102
+ const token = jwt.sign({ sub: userId, workspaceId, roles, permissions, jti }, secret, {
83
103
  expiresIn: JWT_EXPIRES_IN
84
104
  });
105
+ return { token, jti, expiresAt };
85
106
  }
86
107
  };
87
108
 
@@ -94,25 +115,42 @@ function authenticateJWT(req, res, next) {
94
115
  res.status(401).json({ error: { message: "Unauthorized \u2014 Bearer token required" } });
95
116
  return;
96
117
  }
97
- let config;
98
- try {
99
- const scope = req.scope;
100
- config = scope.resolve("config");
101
- } catch {
102
- res.status(500).json({ error: { message: "Server misconfiguration" } });
103
- return;
104
- }
105
- try {
106
- const payload = jwt2.verify(token, config.projectConfig.jwtSecret);
107
- req.user = {
108
- id: payload.sub,
109
- workspaceId: payload.workspaceId ?? null,
110
- roles: Array.isArray(payload.roles) ? payload.roles : []
111
- };
112
- next();
113
- } catch {
114
- res.status(401).json({ error: { message: "Invalid or expired token" } });
115
- }
118
+ ;
119
+ (async () => {
120
+ let config;
121
+ try {
122
+ const scope = req.scope;
123
+ config = scope.resolve("config");
124
+ } catch {
125
+ res.status(500).json({ error: { message: "Server misconfiguration" } });
126
+ return;
127
+ }
128
+ try {
129
+ const payload = jwt2.verify(token, config.projectConfig.jwtSecret);
130
+ if (payload.jti) {
131
+ try {
132
+ const scope = req.scope;
133
+ const userService = scope.resolve("userModuleService");
134
+ const valid = await userService.isSessionValid(payload.jti);
135
+ if (!valid) {
136
+ res.status(401).json({ error: { message: "Session revoked or expired" } });
137
+ return;
138
+ }
139
+ } catch {
140
+ }
141
+ }
142
+ req.user = {
143
+ id: payload.sub,
144
+ workspaceId: payload.workspaceId ?? null,
145
+ roles: Array.isArray(payload.roles) ? payload.roles : [],
146
+ permissions: Array.isArray(payload.permissions) ? payload.permissions : [],
147
+ jti: payload.jti ?? null
148
+ };
149
+ next();
150
+ } catch {
151
+ res.status(401).json({ error: { message: "Invalid or expired token" } });
152
+ }
153
+ })();
116
154
  }
117
155
 
118
156
  // src/guards.ts
@@ -123,6 +161,15 @@ function requireRoles(...roles) {
123
161
  res.status(403).json({ error: { message: "Forbidden" } });
124
162
  };
125
163
  }
164
+ function requirePermission(...permissions) {
165
+ return (req, res, next) => {
166
+ const userRoles = req.user?.roles ?? [];
167
+ if (userRoles.includes("super-admin")) return next();
168
+ const userPermissions = req.user?.permissions ?? [];
169
+ if (permissions.some((p) => userPermissions.includes(p))) return next();
170
+ res.status(403).json({ error: { message: "Forbidden \u2014 insufficient permissions" } });
171
+ };
172
+ }
126
173
  function requireWorkspace(req, res, next) {
127
174
  const workspaceId = req.query?.workspace_id ?? req.body?.workspace_id;
128
175
  if (workspaceId && req.user?.workspaceId && req.user.workspaceId !== workspaceId) {
@@ -141,6 +188,7 @@ export {
141
188
  AuthModuleService,
142
189
  authenticateJWT,
143
190
  index_default as default,
191
+ requirePermission,
144
192
  requireRoles,
145
193
  requireWorkspace
146
194
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meridianjs/auth",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Meridian auth module — JWT authentication and middleware",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",